@pepperhorn/chordl-guitar 0.2.0 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,82 @@
1
+ # Guitar voicing generation and review
2
+
3
+ `guitarPositionFor(spelledChord, options?)` first validates chords-db positions,
4
+ then tries the regular power-chord shape, then ranks an exhaustive playable
5
+ candidate set. `generateGuitarVoicings(notes, options?)` exposes that ranked set.
6
+
7
+ ```ts
8
+ const position = guitarPositionFor(
9
+ { root: "A", pitchClasses: ["A", "C#", "Eb", "G", "Bb", "F#"] },
10
+ { level: 3 },
11
+ );
12
+ // A13(b9,b5): a rootless grip can retain 3, b5, b7, b9, 13.
13
+ // position.review.reasons includes "omits:1" and "rootless".
14
+ ```
15
+
16
+ The default teaching level is **2**. Levels 0–2 always retain the root. At
17
+ level 3, the root may be omitted from chords with six or more required tones,
18
+ from five-tone upper chords over a slash bass (including the formerly missing
19
+ Ab9/Eb), or from any five-tone chord with an explicit `{ comping: true }`.
20
+ A rootless grip must put a third/seventh lowest or sound the written slash bass
21
+ lowest. A rooted grip still puts the root/written slash bass lowest. A slash
22
+ bass equal to the root cannot be omitted. `rootless` and `omitted` disclose
23
+ root omissions; a renderer should explain “root left out; the bass player has
24
+ it”. Thirds, sevenths and named alterations are preserved in dense chords.
25
+
26
+ Natural 9 remains required under #11 alone. It is optional under a natural 11
27
+ or 13. These are spelling-aware rules: #11 differs from b5 even when the pitch
28
+ classes coincide.
29
+
30
+ Ranking penalizes register-dependent low seconds/thirds and minor ninths,
31
+ inner-string basses, wide finger reaches, long barres, high positions (convex
32
+ cost above fret 9), and opens under a high hand. Five-tone and larger chords
33
+ have no six-string bonus. These costs rank compromises; they do not certify
34
+ playability for a particular hand or enforce the future level-specific tiers.
35
+ The top tension need not be the highest voice.
36
+
37
+ Every returned position, including validated library shapes, carries `review`:
38
+
39
+ - `origin`: `chords-db`, `template:power-chord`, or `search`.
40
+ - `level`: requested teaching level, default 2.
41
+ - `reasons`: omission, register, dissonance, hand geometry and rejected-library
42
+ reason codes (`ReasonCode` is exported). Empty means no measured warning.
43
+ - `confidence`: deterministic attention heuristic in 0–1, **not a probability**
44
+ or teacher approval. Unflagged search starts at 0.88, library/template at 0.98.
45
+ - `needsReview`: `reasons.length > 0 || confidence < 0.8`.
46
+ - `reviewKey`: stable chord-constraint/level/absolute-grip/fingering identity.
47
+ Group by this key to review enharmonic twins once (Bb9/C# and Bb9/Db, for
48
+ example). Different fingerings or levels remain separate review items.
49
+
50
+ The key deduplicates review work without requiring a catalogue slug or a
51
+ stateful registry, and without marking otherwise clean twins as warnings.
52
+ The root's pitch class and each tone's degree/role remain in the key; merely sharing
53
+ an unordered pitch set does not conflate different bass or omission policies.
54
+ A non-chord bass is distinct from a seventh (which may double), even if both
55
+ spellings produce the same pitch set. No template origin is claimed for brute-force search results.
56
+
57
+ This package supplies metadata. Card/manifest consumers must forward it and
58
+ pass the teaching level; existing card rendering is unchanged.
59
+
60
+ ## Reproduce the independent-oracle comparison
61
+
62
+ From the repository root, with the existing review outputs available:
63
+
64
+ ```sh
65
+ node_modules/.bin/tsx packages/chordl-guitar/scripts/compare-voicings.mts \
66
+ --plain /path/to/plain/verdicts.json \
67
+ --slash /path/to/slash/verdicts.json \
68
+ --missing /path/to/missing38.json \
69
+ --out /path/to/generator-comparison \
70
+ --baseline-ref d307e3b378c6864cd61fc39f6571cf0ee1a81586
71
+ ```
72
+
73
+ Both versions regenerate all 3,157 oracle rows. The script refuses baseline
74
+ shape/fingering mismatches, validates new pitch coverage, reports changed
75
+ `replace` and `accept` rows separately, counts all 38 recoveries, and writes
76
+ review distributions, raw JSON and an HTML contact sheet. Comparison uses
77
+ explicit level 3; the default remains root-preserving level 2. Original verdicts
78
+ judge the old shapes: a changed `accept` is a regression watch item, and a
79
+ changed `replace` still needs a teacher's playing/listening check.
80
+
81
+ Package checks: `pnpm --filter @pepperhorn/chordl-guitar test:run` and
82
+ `pnpm --filter @pepperhorn/chordl-guitar build`.
@@ -1,5 +1,6 @@
1
1
  import type { Chord } from "svguitar";
2
2
  import type { ChordsDbPosition, InstrumentId } from "./instruments.js";
3
+ import type { ExperienceLevel } from "./experience.js";
3
4
  export interface GuitarChordResult {
4
5
  /** The chord label as given. */
5
6
  label: string;
@@ -8,6 +9,8 @@ export interface GuitarChordResult {
8
9
  positions: ChordsDbPosition[];
9
10
  /** svguitar Chord objects, one per position, ready to render. */
10
11
  shapes: Chord[];
12
+ /** How hard each shape is to play, parallel to `positions`/`shapes`. */
13
+ levels: ExperienceLevel[];
11
14
  }
12
15
  /**
13
16
  * Look up all stored shapes for a chord label. Returns null when the chord or
@@ -21,8 +21,11 @@ import guitarDb from "./generated/guitar.slim.json" with { type: "json" };
21
21
  import ukuleleDb from "./generated/ukulele.slim.json" with { type: "json" };
22
22
  import { INSTRUMENTS, dbPositionToChord, } from "./instruments.js";
23
23
  import { rootPitchClass } from "./pitch.js";
24
+ import { splitLabel, toDbSuffix } from "./chordNames.js";
24
25
  import { powerChordPosition } from "./powerChords.js";
25
- import { GUITAR_TOP3_PRESETS } from "./staticPresets.js";
26
+ import { findTop3Preset } from "./staticPresets.js";
27
+ import { positionFacts } from "./voicingFacts.js";
28
+ import { levelForFacts } from "./experience.js";
26
29
  const GUITAR_DB = guitarDb;
27
30
  const UKULELE_DB = ukuleleDb;
28
31
  // chords-db ships guitar and ukulele voicing libraries.
@@ -51,43 +54,6 @@ const ROOT_TO_DB_KEYS = {
51
54
  "A#": ["Bb", "Asharp"], Bb: ["Bb", "Asharp"],
52
55
  B: ["B"], Cb: ["B"],
53
56
  };
54
- // chordl suffix → chords-db suffix. Most match verbatim; only the triads and a
55
- // couple of aliases need translating.
56
- function toDbSuffix(suffix) {
57
- const s = suffix.trim();
58
- if (s === "" || s === "maj" || s === "M")
59
- return "major";
60
- if (s === "m" || s === "min" || s === "-")
61
- return "minor";
62
- if (s === "min7")
63
- return "m7";
64
- if (s === "M7")
65
- return "maj7";
66
- if (s === "°")
67
- return "dim";
68
- if (s === "°7")
69
- return "dim7";
70
- if (s === "ø" || s === "ø7")
71
- return "m7b5";
72
- // chords-db spells the 6/9 voicings without the slash.
73
- if (s === "6/9")
74
- return "69";
75
- if (s === "m6/9")
76
- return "m69";
77
- // Bare "sus" (and "7sus") carry no extension number; sus4 is the convention.
78
- if (s === "sus")
79
- return "sus4";
80
- if (s === "7sus")
81
- return "7sus4";
82
- return s; // m7, maj7, 7, dim7, sus4, 6, 9, 11, 13, m7b5, ... match verbatim
83
- }
84
- function splitLabel(label) {
85
- const m = label.trim().match(/^([A-Ga-g][#b]?)(.*)$/);
86
- if (!m)
87
- return null;
88
- const root = m[1].charAt(0).toUpperCase() + m[1].slice(1);
89
- return { root, suffix: m[2] };
90
- }
91
57
  function findEntry(db, label) {
92
58
  const parsed = splitLabel(label);
93
59
  if (!parsed)
@@ -126,24 +92,15 @@ function powerChordPositions(label) {
126
92
  .filter((p) => p !== null);
127
93
  }
128
94
  // ── Top-3-string presets ────────────────────────────────────────────────────
129
- /**
130
- * The preset table spells qualities in words where chords-db uses symbols, so a
131
- * chordl label has to be translated before lookup. Only the suffixes the table
132
- * actually carries are mapped; anything else finds nothing, which is correct —
133
- * falling back to a six-string shape under a three-string label would be a lie.
134
- */
135
- function toPresetSuffix(suffix) {
136
- const s = suffix.trim();
137
- if (s === "")
138
- return "major";
139
- if (s === "m" || s === "min")
140
- return "minor";
141
- return s; // 7, m7, maj7 match the table verbatim
142
- }
143
95
  /**
144
96
  * Presets are authored as svguitar shapes, but callers expect chords-db-style
145
97
  * positions (that is what carries `baseFret` for the "fret N" caption). Convert
146
98
  * back: svguitar numbers strings high-to-low from 1, positions run low-to-high.
99
+ *
100
+ * The window is already decided — `toPreset` put it on `chord.position`, so
101
+ * every consumer of the preset table sees the same one — and `chord` frets are
102
+ * relative to it, which is exactly what `baseFret` means. So there is nothing to
103
+ * recompute here, only to rename.
147
104
  */
148
105
  function presetToPosition(preset) {
149
106
  const frets = new Array(6).fill(-1);
@@ -161,16 +118,19 @@ function presetToPosition(preset) {
161
118
  const n = Number(label);
162
119
  fingers[idx] = Number.isFinite(n) ? n : 0;
163
120
  }
164
- // Every preset is written in absolute frets from the nut, all within reach of
165
- // first position, so the diagram window always starts at the nut.
166
- return { frets, fingers, baseFret: 1, barres: [] };
121
+ return { frets, fingers, baseFret: preset.chord.position ?? 1, barres: [] };
167
122
  }
123
+ /**
124
+ * The table resolves both halves of the label itself — root by pitch class, and
125
+ * suffix through the same `toDbSuffix` the six-string path uses — so there is
126
+ * nothing to translate here beyond finding where the root stops.
127
+ *
128
+ * A miss still returns null. Falling back to a six-string shape under a
129
+ * three-string label would be a lie.
130
+ */
168
131
  function findPreset(label) {
169
132
  const parsed = splitLabel(label);
170
- if (!parsed)
171
- return null;
172
- const suffix = toPresetSuffix(parsed.suffix);
173
- return (GUITAR_TOP3_PRESETS.find((p) => p.key === parsed.root && p.suffix === suffix) ?? null);
133
+ return parsed ? findTop3Preset(parsed.root, parsed.suffix) : null;
174
134
  }
175
135
  /**
176
136
  * Look up all stored shapes for a chord label. Returns null when the chord or
@@ -180,19 +140,26 @@ export function lookupGuitarChord(label, instrument = "guitar") {
180
140
  const strings = INSTRUMENTS[instrument]?.strings;
181
141
  if (strings == null)
182
142
  return null;
183
- const toResult = (positions) => positions.length === 0
143
+ const openMidi = INSTRUMENTS[instrument].openMidi;
144
+ const rootPc = rootPitchClass(label);
145
+ // chords-db and generated positions have no stored level: derive it from the
146
+ // same facts levelForFacts always ranks on.
147
+ const derivedLevels = (positions) => positions.map((pos) => levelForFacts(positionFacts(pos, openMidi, rootPc)));
148
+ const toResult = (positions, levels = derivedLevels(positions)) => positions.length === 0
184
149
  ? null
185
150
  : {
186
151
  label,
187
152
  instrument,
188
153
  positions,
189
154
  shapes: positions.map((pos) => dbPositionToChord(pos, strings, label)),
155
+ levels,
190
156
  };
191
157
  // 1. Hand-authored beats generated: the preset table is the only source of
192
- // three-string voicings, and it does not fall back to chords-db.
158
+ // three-string voicings, and it does not fall back to chords-db. Its
159
+ // level is precomputed and stored on the preset — see src/experience.ts.
193
160
  if (instrument === "guitar-top3") {
194
161
  const preset = findPreset(label);
195
- return preset ? toResult([presetToPosition(preset)]) : null;
162
+ return preset ? toResult([presetToPosition(preset)], [preset.level]) : null;
196
163
  }
197
164
  // 2. chords-db, for the instruments that ship a library.
198
165
  const db = dbFor(instrument);
@@ -1 +1 @@
1
- {"version":3,"file":"chordLookup.js","sourceRoot":"","sources":["../src/chordLookup.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"chordLookup.js","sourceRoot":"","sources":["../src/chordLookup.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Chord-label spelling, resolved once for every lookup path.
3
+ *
4
+ * A user types `Db`, `C#`, `Cø`, `CM7`, `Csus`; chords-db stores `Csharp`,
5
+ * `m7b5`, `maj7`, `sus4`. Both the six-string library and the top-3 table need
6
+ * the same translation, and two copies of it drift — the top-3 path used to
7
+ * carry a three-line version of `toDbSuffix` that knew only `""`, `m` and `min`,
8
+ * which left the generated table addressable for a fraction of what it holds.
9
+ *
10
+ * Roots are resolved by pitch class rather than by spelling wherever the target
11
+ * is keyed that way; `rootPitchClass` in pitch.ts is the one parser for that.
12
+ * This module carries what pitch classes cannot express: where the suffix ends
13
+ * and, for chords-db, which of its inconsistent root spellings to try.
14
+ */
15
+ /**
16
+ * chordl suffix → chords-db suffix. Most match verbatim; only the triads and a
17
+ * handful of aliases need translating.
18
+ */
19
+ export declare function toDbSuffix(suffix: string): string;
20
+ /** Split a label into its root spelling and everything after it. */
21
+ export declare function splitLabel(label: string): {
22
+ root: string;
23
+ suffix: string;
24
+ } | null;
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Chord-label spelling, resolved once for every lookup path.
3
+ *
4
+ * A user types `Db`, `C#`, `Cø`, `CM7`, `Csus`; chords-db stores `Csharp`,
5
+ * `m7b5`, `maj7`, `sus4`. Both the six-string library and the top-3 table need
6
+ * the same translation, and two copies of it drift — the top-3 path used to
7
+ * carry a three-line version of `toDbSuffix` that knew only `""`, `m` and `min`,
8
+ * which left the generated table addressable for a fraction of what it holds.
9
+ *
10
+ * Roots are resolved by pitch class rather than by spelling wherever the target
11
+ * is keyed that way; `rootPitchClass` in pitch.ts is the one parser for that.
12
+ * This module carries what pitch classes cannot express: where the suffix ends
13
+ * and, for chords-db, which of its inconsistent root spellings to try.
14
+ */
15
+ /**
16
+ * chordl suffix → chords-db suffix. Most match verbatim; only the triads and a
17
+ * handful of aliases need translating.
18
+ */
19
+ export function toDbSuffix(suffix) {
20
+ const s = suffix.trim();
21
+ if (s === "" || s === "maj" || s === "M")
22
+ return "major";
23
+ if (s === "m" || s === "min" || s === "-")
24
+ return "minor";
25
+ if (s === "min7")
26
+ return "m7";
27
+ if (s === "M7")
28
+ return "maj7";
29
+ if (s === "°")
30
+ return "dim";
31
+ if (s === "°7")
32
+ return "dim7";
33
+ if (s === "ø" || s === "ø7")
34
+ return "m7b5";
35
+ // chords-db spells the 6/9 voicings without the slash.
36
+ if (s === "6/9")
37
+ return "69";
38
+ if (s === "m6/9")
39
+ return "m69";
40
+ // Bare "sus" (and "7sus") carry no extension number; sus4 is the convention.
41
+ if (s === "sus")
42
+ return "sus4";
43
+ if (s === "7sus")
44
+ return "7sus4";
45
+ return s; // m7, maj7, 7, dim7, sus4, 6, 9, 11, 13, m7b5, ... match verbatim
46
+ }
47
+ /** Split a label into its root spelling and everything after it. */
48
+ export function splitLabel(label) {
49
+ const m = label.trim().match(/^([A-Ga-g][#b]?)(.*)$/);
50
+ if (!m)
51
+ return null;
52
+ const root = m[1].charAt(0).toUpperCase() + m[1].slice(1);
53
+ return { root, suffix: m[2] };
54
+ }
55
+ //# sourceMappingURL=chordNames.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"chordNames.js","sourceRoot":"","sources":["../src/chordNames.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH;;;GAGG;AACH,MAAM,UAAU,UAAU,CAAC,MAAc;IACvC,MAAM,CAAC,GAAG,MAAM,CAAC,IAAI,EAAE,CAAC;IACxB,IAAI,CAAC,KAAK,EAAE,IAAI,CAAC,KAAK,KAAK,IAAI,CAAC,KAAK,GAAG;QAAE,OAAO,OAAO,CAAC;IACzD,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC,KAAK,KAAK,IAAI,CAAC,KAAK,GAAG;QAAE,OAAO,OAAO,CAAC;IAC1D,IAAI,CAAC,KAAK,MAAM;QAAE,OAAO,IAAI,CAAC;IAC9B,IAAI,CAAC,KAAK,IAAI;QAAE,OAAO,MAAM,CAAC;IAC9B,IAAI,CAAC,KAAK,GAAG;QAAE,OAAO,KAAK,CAAC;IAC5B,IAAI,CAAC,KAAK,IAAI;QAAE,OAAO,MAAM,CAAC;IAC9B,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC,KAAK,IAAI;QAAE,OAAO,MAAM,CAAC;IAC3C,uDAAuD;IACvD,IAAI,CAAC,KAAK,KAAK;QAAE,OAAO,IAAI,CAAC;IAC7B,IAAI,CAAC,KAAK,MAAM;QAAE,OAAO,KAAK,CAAC;IAC/B,6EAA6E;IAC7E,IAAI,CAAC,KAAK,KAAK;QAAE,OAAO,MAAM,CAAC;IAC/B,IAAI,CAAC,KAAK,MAAM;QAAE,OAAO,OAAO,CAAC;IACjC,OAAO,CAAC,CAAC,CAAC,kEAAkE;AAC9E,CAAC;AAED,oEAAoE;AACpE,MAAM,UAAU,UAAU,CAAC,KAAa;IACtC,MAAM,CAAC,GAAG,KAAK,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,uBAAuB,CAAC,CAAC;IACtD,IAAI,CAAC,CAAC;QAAE,OAAO,IAAI,CAAC;IACpB,MAAM,IAAI,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,WAAW,EAAE,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;IAC1D,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;AAChC,CAAC"}
@@ -0,0 +1,129 @@
1
+ /**
2
+ * How hard a shape is to play, as three rungs a learner moves through.
3
+ *
4
+ * Derived from the facts, never stored: a level that can be hand-edited drifts
5
+ * from the shape it describes. `ShapeClass` in voicingSelect.ts filters on the
6
+ * same facts; this ranks on them. They are deliberately separate — a filter
7
+ * answers "show me the open ones", a rank answers "can I play this yet".
8
+ */
9
+ import type { ChordsDbPosition } from "./instruments.js";
10
+ import type { PositionFacts } from "./voicingFacts.js";
11
+ import type { ShapeClass } from "./voicingSelect.js";
12
+ export type ExperienceLevel = "beginner" | "emerging" | "established";
13
+ /** Easiest first, so `indexOf` gives the rung number and +1 widens. */
14
+ export declare const EXPERIENCE_LADDER: ExperienceLevel[];
15
+ /**
16
+ * An open shape is barre-free and at the nut — nothing to hold down across
17
+ * strings and no hand position to find. Barre-free but moved up the neck is
18
+ * the next step. A barre is the rung most beginners stall on, so it anchors
19
+ * the top.
20
+ */
21
+ export declare function levelForFacts(facts: PositionFacts): ExperienceLevel;
22
+ /**
23
+ * Rank a three-string shape. `PositionFacts` cannot: it assumes six strings
24
+ * and real barres, and a top-3 shape has neither.
25
+ *
26
+ * The axes are the ones the generator already ranks candidates by — stretch,
27
+ * finger count, open strings, distance from the nut. A shape a learner can
28
+ * play without moving out of first position is the dividing line, because
29
+ * finding a hand position is the skill that separates the first two rungs.
30
+ *
31
+ * Accepts either of two representations of the same shape, and returns the
32
+ * same level for both:
33
+ *
34
+ * - absolute frets measured from the nut, with `position` pinned at 1 —
35
+ * what the generated table stores (`Top3GeneratedEntry.frets`);
36
+ * - window-relative frets paired with the diagram's real `position` —
37
+ * what `staticPresets.top3Window()` produces for rendering.
38
+ *
39
+ * They agree because the function only ever compares
40
+ * `position - 1 + max(fretted)` to the nut: sliding a window down by
41
+ * `position - 1` and adding it back is the same transform run forward and
42
+ * back, so the recovered highest fret — and therefore the level — is
43
+ * identical either way. A caller holding a rendered preset (windowed frets
44
+ * plus its real position) can pass those straight in rather than
45
+ * reconstructing absolute frets by hand, which is exactly the arithmetic that
46
+ * has produced off-diagram bugs elsewhere in this codebase.
47
+ */
48
+ export declare function levelForTop3(frets: number[], position: number): ExperienceLevel;
49
+ export interface ExperienceQuery {
50
+ level: ExperienceLevel;
51
+ /**
52
+ * Optional refinement within the level. Default "any".
53
+ *
54
+ * Published API, kept for when the level control gains a paired shape-class
55
+ * control — deliberately not wired into any UI yet (the only level control
56
+ * shipped so far is `dev/App.tsx`'s radio group; `GuitarChordPanel` draws
57
+ * no control of its own). Under cumulative level matching it now genuinely
58
+ * refines at the `emerging` and `established` rungs (it can drop shapes
59
+ * those levels would otherwise include), so do not delete this as dead —
60
+ * only its UI is deferred, not the mechanism.
61
+ */
62
+ shapeClass?: ShapeClass;
63
+ }
64
+ export interface ExperienceSelection {
65
+ /** Indices into the input array, in input order. Never empty when input is non-empty. */
66
+ indices: number[];
67
+ /** The level actually served — differs from the request when it widened. */
68
+ level: ExperienceLevel;
69
+ /** True when the shape-class refinement had to be dropped to find anything. */
70
+ droppedShapeClass: boolean;
71
+ /** The level originally asked for, when it had to widen. Absent otherwise. */
72
+ widenedFrom?: ExperienceLevel;
73
+ }
74
+ /**
75
+ * Filter by level, refine by shape class, and relax rather than show nothing.
76
+ *
77
+ * Level matching is CUMULATIVE, not exclusive: `query.level` answers "can I
78
+ * play this yet", so a level matches every shape ranked at or below it on
79
+ * `EXPERIENCE_LADDER` — `established` matches everything, `emerging` matches
80
+ * beginner + emerging, `beginner` matches only beginner (there is nothing
81
+ * below it to include). An established player can certainly play an open C;
82
+ * excluding the open shape from an "established" query was the bug, not a
83
+ * feature.
84
+ *
85
+ * Emptiness is still possible at the bottom rung: 220 of 529 chords have no
86
+ * open shape at all and 88 have only barre shapes, so a beginner filter finds
87
+ * nothing for nearly half the corpus. Returning an empty frame there would
88
+ * punish exactly the learner the filter is for, so relaxation still widens
89
+ * one rung at a time rather than giving up.
90
+ *
91
+ * Relaxation order is deliberate. The refinement goes first because the level
92
+ * is what the user asked for and the shape class only narrows it; widening the
93
+ * level changes the answer to the question they asked.
94
+ *
95
+ * @param levels - Optional, parallel to `facts`. When an entry is present it
96
+ * is used verbatim instead of `levelForFacts(facts[i])`. This is for
97
+ * shapes whose facts cannot support the six-string derivation — top-3
98
+ * presets never carry barres, so `levelForFacts` can never call one
99
+ * "established" even when its authoritative, stored level (`levelForTop3`)
100
+ * says otherwise. Shape-class matching still reads `facts[i]` either way:
101
+ * only the level lookup is overridable. Missing entries (a shorter array,
102
+ * or `undefined` at an index) fall back to the derived level. Prefer
103
+ * `selectForResult` when a `GuitarChordResult` (or equivalent
104
+ * positions+levels pair) is in hand — it threads this argument for you so a
105
+ * caller cannot forget it and silently reintroduce derived-only ranking.
106
+ */
107
+ export declare function selectForExperience(facts: PositionFacts[], query: ExperienceQuery, levels?: ExperienceLevel[]): ExperienceSelection;
108
+ /**
109
+ * `selectForExperience`, but for a `GuitarChordResult` (or any duck-typed
110
+ * `{ positions, levels }` pair) instead of a bare facts array.
111
+ *
112
+ * `selectForExperience`'s third argument is what stops top-3 shapes being
113
+ * mis-ranked: the six-string derivation (`levelForFacts`) is structurally
114
+ * unable to call a top-3 shape "established" because top-3 positions never
115
+ * carry barres, so a caller that forgets to pass `levels` silently gets a
116
+ * shape re-ranked one or more rungs too easy. Threading it by hand is exactly
117
+ * the kind of thing a future caller forgets, so this wrapper does it for you:
118
+ * it derives `facts` from `result.positions` and always passes
119
+ * `result.levels` through, leaving nowhere for the argument to get dropped.
120
+ *
121
+ * Prefer this whenever a full result (not just bare facts) is in hand. The
122
+ * bare-facts form of `selectForExperience` stays published for callers that
123
+ * only ever have `PositionFacts[]` — most tests, and anything working one
124
+ * level below a `GuitarChordResult`.
125
+ */
126
+ export declare function selectForResult(result: {
127
+ positions: ChordsDbPosition[];
128
+ levels: ExperienceLevel[];
129
+ }, openMidi: number[], rootPc: number | null, query: ExperienceQuery): ExperienceSelection;
@@ -0,0 +1,164 @@
1
+ import { positionFacts } from "./voicingFacts.js";
2
+ import { matchesShapeClass } from "./voicingSelect.js";
3
+ /** Easiest first, so `indexOf` gives the rung number and +1 widens. */
4
+ export const EXPERIENCE_LADDER = [
5
+ "beginner",
6
+ "emerging",
7
+ "established",
8
+ ];
9
+ /**
10
+ * An open shape is barre-free and at the nut — nothing to hold down across
11
+ * strings and no hand position to find. Barre-free but moved up the neck is
12
+ * the next step. A barre is the rung most beginners stall on, so it anchors
13
+ * the top.
14
+ */
15
+ export function levelForFacts(facts) {
16
+ if (facts.isOpenShape)
17
+ return "beginner";
18
+ if (!facts.hasBarre)
19
+ return "emerging";
20
+ return "established";
21
+ }
22
+ /**
23
+ * Rank a three-string shape. `PositionFacts` cannot: it assumes six strings
24
+ * and real barres, and a top-3 shape has neither.
25
+ *
26
+ * The axes are the ones the generator already ranks candidates by — stretch,
27
+ * finger count, open strings, distance from the nut. A shape a learner can
28
+ * play without moving out of first position is the dividing line, because
29
+ * finding a hand position is the skill that separates the first two rungs.
30
+ *
31
+ * Accepts either of two representations of the same shape, and returns the
32
+ * same level for both:
33
+ *
34
+ * - absolute frets measured from the nut, with `position` pinned at 1 —
35
+ * what the generated table stores (`Top3GeneratedEntry.frets`);
36
+ * - window-relative frets paired with the diagram's real `position` —
37
+ * what `staticPresets.top3Window()` produces for rendering.
38
+ *
39
+ * They agree because the function only ever compares
40
+ * `position - 1 + max(fretted)` to the nut: sliding a window down by
41
+ * `position - 1` and adding it back is the same transform run forward and
42
+ * back, so the recovered highest fret — and therefore the level — is
43
+ * identical either way. A caller holding a rendered preset (windowed frets
44
+ * plus its real position) can pass those straight in rather than
45
+ * reconstructing absolute frets by hand, which is exactly the arithmetic that
46
+ * has produced off-diagram bugs elsewhere in this codebase.
47
+ */
48
+ export function levelForTop3(frets, position) {
49
+ const fretted = frets.filter((f) => f > 0);
50
+ const opens = frets.filter((f) => f === 0).length;
51
+ const span = fretted.length ? Math.max(...fretted) - Math.min(...fretted) : 0;
52
+ const highest = position - 1 + (fretted.length ? Math.max(...fretted) : 0);
53
+ // Away from the nut is established regardless of how few fingers it takes:
54
+ // the hand has to be placed before it can be shaped.
55
+ if (position > 1 || highest > 4)
56
+ return "established";
57
+ // At the nut: open strings or at most two fingers, with no stretch.
58
+ if (span <= 1 && (opens > 0 || fretted.length <= 2))
59
+ return "beginner";
60
+ return "emerging";
61
+ }
62
+ /**
63
+ * Filter by level, refine by shape class, and relax rather than show nothing.
64
+ *
65
+ * Level matching is CUMULATIVE, not exclusive: `query.level` answers "can I
66
+ * play this yet", so a level matches every shape ranked at or below it on
67
+ * `EXPERIENCE_LADDER` — `established` matches everything, `emerging` matches
68
+ * beginner + emerging, `beginner` matches only beginner (there is nothing
69
+ * below it to include). An established player can certainly play an open C;
70
+ * excluding the open shape from an "established" query was the bug, not a
71
+ * feature.
72
+ *
73
+ * Emptiness is still possible at the bottom rung: 220 of 529 chords have no
74
+ * open shape at all and 88 have only barre shapes, so a beginner filter finds
75
+ * nothing for nearly half the corpus. Returning an empty frame there would
76
+ * punish exactly the learner the filter is for, so relaxation still widens
77
+ * one rung at a time rather than giving up.
78
+ *
79
+ * Relaxation order is deliberate. The refinement goes first because the level
80
+ * is what the user asked for and the shape class only narrows it; widening the
81
+ * level changes the answer to the question they asked.
82
+ *
83
+ * @param levels - Optional, parallel to `facts`. When an entry is present it
84
+ * is used verbatim instead of `levelForFacts(facts[i])`. This is for
85
+ * shapes whose facts cannot support the six-string derivation — top-3
86
+ * presets never carry barres, so `levelForFacts` can never call one
87
+ * "established" even when its authoritative, stored level (`levelForTop3`)
88
+ * says otherwise. Shape-class matching still reads `facts[i]` either way:
89
+ * only the level lookup is overridable. Missing entries (a shorter array,
90
+ * or `undefined` at an index) fall back to the derived level. Prefer
91
+ * `selectForResult` when a `GuitarChordResult` (or equivalent
92
+ * positions+levels pair) is in hand — it threads this argument for you so a
93
+ * caller cannot forget it and silently reintroduce derived-only ranking.
94
+ */
95
+ export function selectForExperience(facts, query, levels) {
96
+ if (facts.length === 0) {
97
+ return { indices: [], level: query.level, droppedShapeClass: false };
98
+ }
99
+ const cls = query.shapeClass ?? "any";
100
+ const levelOf = (i) => levels?.[i] ?? levelForFacts(facts[i]);
101
+ const rank = (level) => EXPERIENCE_LADDER.indexOf(level);
102
+ // Cumulative: everything ranked at or below `level` counts as a match.
103
+ const at = (level, withClass) => facts
104
+ .map((f, i) => i)
105
+ .filter((i) => rank(levelOf(i)) <= rank(level) &&
106
+ (!withClass || matchesShapeClass(facts[i], cls)));
107
+ // 1. level + refinement
108
+ const exact = at(query.level, true);
109
+ if (exact.length > 0) {
110
+ return { indices: exact, level: query.level, droppedShapeClass: false };
111
+ }
112
+ // 2. drop the refinement, same level
113
+ const noClass = at(query.level, false);
114
+ if (noClass.length > 0) {
115
+ return { indices: noClass, level: query.level, droppedShapeClass: cls !== "any" };
116
+ }
117
+ // 3. widen the level, one rung at a time, refinement already gone. This
118
+ // loop always returns before exhausting the ladder: matching is cumulative
119
+ // (step 2 above), so `at("established", false)` — the last rung — matches
120
+ // every index whenever `facts` is non-empty, which the guard at the top of
121
+ // this function guarantees. A fourth "take everything" fallback after this
122
+ // loop used to exist for the case where even that failed; it was dead code
123
+ // that could never run, and it lied about `level` when it did (claiming to
124
+ // serve `query.level` while actually serving every shape). Deleted rather
125
+ // than kept honest, because there is nothing truthful left for it to say —
126
+ // the loop already covers the one case it was for.
127
+ const start = EXPERIENCE_LADDER.indexOf(query.level);
128
+ for (let i = start + 1; i < EXPERIENCE_LADDER.length; i++) {
129
+ const wider = at(EXPERIENCE_LADDER[i], false);
130
+ if (wider.length > 0) {
131
+ return {
132
+ indices: wider,
133
+ level: EXPERIENCE_LADDER[i],
134
+ droppedShapeClass: cls !== "any",
135
+ widenedFrom: query.level,
136
+ };
137
+ }
138
+ }
139
+ throw new Error("selectForExperience: unreachable — cumulative matching guarantees the " +
140
+ "widening loop returns by \"established\" whenever facts is non-empty");
141
+ }
142
+ /**
143
+ * `selectForExperience`, but for a `GuitarChordResult` (or any duck-typed
144
+ * `{ positions, levels }` pair) instead of a bare facts array.
145
+ *
146
+ * `selectForExperience`'s third argument is what stops top-3 shapes being
147
+ * mis-ranked: the six-string derivation (`levelForFacts`) is structurally
148
+ * unable to call a top-3 shape "established" because top-3 positions never
149
+ * carry barres, so a caller that forgets to pass `levels` silently gets a
150
+ * shape re-ranked one or more rungs too easy. Threading it by hand is exactly
151
+ * the kind of thing a future caller forgets, so this wrapper does it for you:
152
+ * it derives `facts` from `result.positions` and always passes
153
+ * `result.levels` through, leaving nowhere for the argument to get dropped.
154
+ *
155
+ * Prefer this whenever a full result (not just bare facts) is in hand. The
156
+ * bare-facts form of `selectForExperience` stays published for callers that
157
+ * only ever have `PositionFacts[]` — most tests, and anything working one
158
+ * level below a `GuitarChordResult`.
159
+ */
160
+ export function selectForResult(result, openMidi, rootPc, query) {
161
+ const facts = result.positions.map((pos) => positionFacts(pos, openMidi, rootPc));
162
+ return selectForExperience(facts, query, result.levels);
163
+ }
164
+ //# sourceMappingURL=experience.js.map