feat: griff ingest — bulk-build corpus chunks from a folder of tabs - #134
Conversation
A bulk tab ingest must decide which tracks of a multitrack file become corpus
chunks. The target collection is two-independent-guitar writing (both parts are
wanted), plus bass (wanted separately, never mixed with guitar), over drums and
vocals (not ingested). The inventory pass also showed 164 files carry a track
whose tuning imports as all-`C-1` (MIDI 0) — a non-fretted placeholder that must
not be mistaken for an instrument.
`classify_track_role(&Track) -> TrackRole { Guitar, Bass, Other }` is the pure
seam for that decision, testable on synthetic tracks with no import. Eleven
tests fix the contract: six-and-seven-string tunings are guitars, four- and
low-five-string are bass, an all-one-pitch or empty tuning is Other, an explicit
name overrides structure (bass before guitar, so "Bass Guitar" is bass; a named
guitar beats an odd tuning; drums/vocals are Other), and the GM percussion
channel is Other.
The function is a stub returning `Other`, so the six guitar/bass cases fail; the
five genuinely-Other cases pass against it and stand as GREEN regression guards.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NkqJUU6d1sW1RAfvyHrqVM
`classify_track_role` reads the three signals already on an imported track, most reliable first: an explicit part name (bass before guitar, so "Bass Guitar" is bass; drums/vocals route to Other), then the GM percussion channel 9, then the open-string tuning. A placeholder tuning — empty, or every string at one pitch, the all-`C-1` shape a non-fretted part imports as — is Other; otherwise four or fewer strings is a bass, six or more a guitar, and a five-string is a bass only if its lowest string reaches bass range (<= E1). Pure and side-effect free; the eleven RED tests pass. No caller yet — the bulk `ingest` command that selects guitar tracks with this is the next slice. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NkqJUU6d1sW1RAfvyHrqVM
`select_ingest_tracks(&Score, include_bass)` is the policy the arbiter set: take every guitar (the target collection is two-independent-guitar writing, both parts wanted), add the bass parts only when asked, and skip everything else. An empty result is a skip, not an error — a file with no fretted part contributes nothing. Four tests: two guitars + bass + drums selects the two guitars by default and adds the bass under the flag (drums always out); a drums+vocals file selects nothing; a lone guitar is selected. The stub returns an empty Vec, so the three that expect a selection fail. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NkqJUU6d1sW1RAfvyHrqVM
`select_ingest_tracks` filters a score's tracks through `classify_track_role`: guitars always, bass under the flag, everything else out; order preserved. The four RED tests pass. Still no caller — the `griff ingest` command that walks a directory and assembles chunks from these tracks is the next slice. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NkqJUU6d1sW1RAfvyHrqVM
Ingest fills `ChunkMeta.tuning`, which by convention holds a name (`"standard_e"`, `"drop_d"`), not raw pitches. `tuning_label` turns a track's open strings into that: the common tunings resolve to their conventional name, and anything else spells its open strings low-to-high (`c2_g2_c3_f3_a3_d4`, sharps as `s`) so an unusual tuning is recorded exactly and never lost. Preserving the real per-track tuning matters for later fingering-aware generation — the model already keeps each note's explicit GP string/fret, and the tuning is the frame that makes those positions mean something; it cannot be backfilled from pitches, so it is captured at ingest. Six tests: standard E, drop D, seven-string standard B, four-string bass standard, an unrecognised drop-C that spells out, and the sharp spelling. The stub returns an empty string, so all six fail. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NkqJUU6d1sW1RAfvyHrqVM
`tuning_label` recognises the tunings this corpus is thick with — standard E, drop D, seven-string standard B, four-string bass standard — and returns their conventional name; every other tuning spells its open strings low-to-high (`c2_g2_c3_f3_a3_d4`, sharps as `s`), so nothing is ever lost to an unrecognised shape. The named set is intentionally small and grows as the corpus turns up more. Six RED tests pass. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NkqJUU6d1sW1RAfvyHrqVM
…chunks `assemble_ingest_group` is the heart of `griff ingest`: it phrase-splits every selected track of one source file, links each phrase chunk to a per-file ensemble group (schema v4) so a reader can tell which chunks came from one tab, and stamps the default community-tab rights on all of them. Relations stay empty — this records provenance, not measured inter-part dependencies (the arbiter's A2 choice). The test builds a two-guitar score (standard E and drop D, so the tuning labels differ), assembles it, and asserts: at least one phrase per guitar, a group id shared by every chunk's ensemble link, part indices 0 and 1, default rights (copyrighted composition / community tab site / not redistributable), and the two distinct tuning labels. The stub returns no chunks, so it fails. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NkqJUU6d1sW1RAfvyHrqVM
… a folder `griff ingest <dir>` walks a directory of MIDI / Guitar Pro files and, for each: imports it, selects the guitar (and, with --with-bass, bass) tracks, phrase- splits every one, links all phrases to a per-file ensemble group so a reader can tell which chunks came from one tab, and stamps community-tab rights on each. Chunks and the group land in the output dir (default `corpus/`), then the manifest is rebuilt and a skip report lists every file with no guitar, an import error, or no phrase that survived splitting. `assemble_ingest_group` is the tested heart; the command is thin I/O around it. Composition, not new machinery: `build_chunk_meta` already accepts the ensemble link and the rights, so `phrase_chunks_for_track` (a `griff split` that takes an explicit track instead of the first note-bearing one) sets the group link on each phrase after the split — no split-path signature changed, no existing test touched. `default_ingest_inputs` supplies the non-interactive defaults: copyrighted-composition / community-tab-site / not-redistributable rights, the real tuning via `griff_core::ingest::tuning_label`, empty tags and reviewer for the later cockpit curation pass. `slugify` derives a stable group id from the filename stem. Chunks are uncurated candidates by design — the ≥100-phrase S12 gate wants curated phrases, so a human still tags and reviews them in the cockpit; this just turns 400 scattered tabs into that candidate pile in one pass. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NkqJUU6d1sW1RAfvyHrqVM
Manual acceptance over the real 407-tab collection ingested 324 files but wrote only 320 group records: several sources share a stem (e.g. one song present as both `.gp5` and `.gpx`), so `slugify(stem)` collides and one file's chunks silently overwrite another's. `unique_group_id` must hand out a distinct id per collision (`shark_dad`, `shark_dad_2`, …). The function does not exist yet. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NkqJUU6d1sW1RAfvyHrqVM
`unique_group_id` disambiguates a slug already used this run (`shark_dad` -> `shark_dad_2` -> …) and `cmd_ingest` threads a used-id set through the file loop, so two sources sharing a stem no longer overwrite each other's chunk and group records. The RED test passes. Found by manual acceptance, not reasoning: the run reported 324 files ingested but wrote 320 group files — the four-file gap was silent overwrites. Re-running after this fix is part of the acceptance evidence. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NkqJUU6d1sW1RAfvyHrqVM
Manual ingest of the real collection showed one physical tuning getting two different labels: 2627 chunks read `standard_e` but another 664 of the same tuning spelled out as `e4_b3_g3_d3_a2_e2`, and drop D split 2979/325 the same way. Guitar Pro files store the open strings low-first in some files and high-first in others, and `tuning_label` matched only the high-first order, so the low-first files fell through to a mirror-image spelling. Two tests pin order-independence: standard E given low-string-first is still `standard_e`, and drop C spelled either way round yields one label. Both fail against the current order-sensitive match. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NkqJUU6d1sW1RAfvyHrqVM
…agnostic `tuning_label` now sorts the open strings low-to-high before matching and spelling, so a tuning gets one label whatever order the source stored its strings in. The two RED order tests pass, and the six earlier ones still do — sorting a high-first constant yields the same ascending set. The named tunings are re-keyed to ascending pitches; the spelling reads low string to high. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NkqJUU6d1sW1RAfvyHrqVM
📝 WalkthroughWalkthroughAdds a bulk ChangesBulk ingest and provenance
Estimated code review effort: 4 (Complex) | ~60 minutes Sequence Diagram(s)sequenceDiagram
participant cmd_ingest
participant select_ingest_tracks
participant assemble_ingest_group
participant phrase_chunks_for_track
participant cmd_manifest
cmd_ingest->>select_ingest_tracks: Select eligible guitar and optional bass tracks
cmd_ingest->>assemble_ingest_group: Assemble chunks and ensemble group
assemble_ingest_group->>phrase_chunks_for_track: Split tracks and attach ensemble references
assemble_ingest_group-->>cmd_ingest: Return linked records
cmd_ingest->>cmd_manifest: Refresh manifest after writing outputs
Possibly related PRs
Suggested reviewers: 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 8
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@cli/src/main.rs`:
- Line 1552: Update the ingest flow surrounding used_ids so it is initialized
with IDs from existing groups before generating or collision-checking new IDs.
Ensure source-to-ID mapping is idempotent across repeated ingest runs, reusing
the existing group ID for an already-ingested source while assigning unique IDs
to new sources.
- Around line 1520-1535: Update slugify so stems containing no ASCII
alphanumeric characters return a stable, nonempty fallback ID instead of an
empty string, while preserving existing normalization for valid characters. Add
or update tests covering inputs such as "曲" and "---", including the resulting
group filename or chunk ID behavior.
- Around line 1483-1485: Update the track ID and title construction around the
visible id and title fields so optional bass tracks selected by --with-bass are
not labeled as guitars. Derive the suffix and display label from the track’s
TrackRole, while preserving the existing guitar naming for guitar parts and
using consistent bass or neutral naming for bass parts.
- Around line 1441-1450: Update the loop around phrase_chunks_for_track so
part_index values are assigned only to tracks that produce surviving chunks,
rather than using the selected iterator index. Track the next compact part index
and increment it after a track contributes chunks; preserve the shared index for
all chunks from that track. Add a regression test covering an empty first track
and verifying the next track receives part_index 0.
- Around line 1447-1449: Ensure the bulk-ingestion path around
phrase_chunks_for_track persists records with empty tags, despite
build_chunk_meta deriving technique, harmony, and syncopation tags. Add an
ingest-specific policy or clear those generated tags before records.push, while
preserving the existing metadata and member ID handling.
- Line 1587: Update the bulk ingestion loop around assemble_ingest_group to
handle per-file assembly errors like existing read/import failures: report the
failure for the current path, record it for the skip report, and continue
processing remaining files instead of propagating with ?. Preserve successful
record/group handling unchanged.
In `@core/src/ingest.rs`:
- Around line 49-64: Update the role classification logic around TrackRole to
tokenize normalized track names rather than using substring checks, ensuring
“bassoon” does not match bass. Evaluate explicit percussion and other
non-fretted tokens before bass so names such as “Bass Drum” return
TrackRole::Other, while preserving guitar and valid bass classification.
- Around line 71-90: The track-role classification around open_strings must not
use fallback Standard E tuning as guitar evidence. Update the ingest
classification flow to distinguish genuine/source-provided tuning from
Track.tuning’s default, and classify unnamed or unrecognized MIDI piano tracks
without tuning as Other (or preserve the appropriate source-aware role); retain
guitar classification only when genuine fretted evidence is available.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: e1c40567-ed7e-48c6-9994-81bf153d4066
📒 Files selected for processing (3)
cli/src/main.rscore/src/ingest.rscore/src/lib.rs
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 65e1d7b233
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
Brings PR #133's Guitar Pro 7/8 support and the cockpit capture provenance fix onto this branch, so griff ingest reads the 79 .gp files in the target collection instead of skipping them as unrecognised. No conflicts: #133 touched core/src/gp.rs, core/src/corpus.rs (the SourceFormat::Gp variant), cli/src/main.rs (the source_format arm) and ui-core/src/capture.rs; this branch's ingest work is in core/src/ingest.rs and separate cli functions. This branch's 12 commits and their hashes are unchanged.
…a v9) The two P1 review findings on #134 both trace to one gap: a corpus chunk points into its source by filename + bar_range but not *which track*, and prepare_chunk picks the first note-bearing track in the slice. Single-track griff split is self-consistent that way, but the multi-guitar ingest cuts track 1/2, so a second-guitar chunk reloads as the first — silent musical substitution, the worst kind of success, across the 291 two-guitar files. Schema v9: SourceRef gains optional `track_index` and `sha256` (both additive, skipped when unset, so pre-v9 records round-trip byte-identically). This commit adds the fields and pins prepare_chunk's contract; the honouring is the GREEN. Four tests: a `track_index = Some(1)` chunk loads track 1's notes, never track 0's; a record with no `track_index` keeps the legacy first-note-bearing fallback; a named track that is silent in the slice, or out of range, FAILS rather than substitute another track. The last is the point — falling back when a track was named turns corpus corruption into a quiet wrong-part load. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NkqJUU6d1sW1RAfvyHrqVM
`prepare_chunk` now branches on `SourceRef.track_index`: when a record names its source track (schema v9), that track is used and only that — a silent or out-of-range named track returns None (a reported load failure), never a fall back to another part. A record without it keeps the legacy first-note-bearing selection, so every pre-v9 chunk loads exactly as before. The four RED tests pass. The two new SourceRef fields are threaded through every existing construction (`build_chunk_meta`, the capture and dock paths, and the schema/similarity/ curation test fixtures) as `None`, so nothing but the ingest path — which sets them next — changes behaviour. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NkqJUU6d1sW1RAfvyHrqVM
…hash The assemble seam now takes the source's SHA-256, and each chunk must persist both `source.track_index` (the exact track it was cut from) and `source.sha256` so the schema-v9 loader reloads the right part from the right bytes. The stub ignores the hash and leaves both fields None, so the assemble test fails on the missing track_index. Adds the `sha2` dependency and `source_sha256`; `cmd_ingest` now computes the hash per file and threads it in. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NkqJUU6d1sW1RAfvyHrqVM
… role `assemble_ingest_group` now stamps each chunk's `source.track_index` with the exact track it was cut from and `source.sha256` with the file's content hash, so the schema-v9 loader reloads the right part from the right bytes rather than the first note-bearing track. The two RED tests pass. Three review findings fixed in the same seam: - **Compact part indices** (CodeRabbit): a selected track that survives no phrase no longer consumes a part index, so the group never names a part with no members. Regression test: an empty first track frees part 0 for the next. - **Bass is not labelled a guitar** (CodeRabbit): `default_ingest_inputs` derives the id tag and title from `TrackRole` — `_b`/"(bass N)" for a bass part admitted by --with-bass, `_g`/"(guitar N)" otherwise. - `source_sha256` builds hex without `format!`-collect. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NkqJUU6d1sW1RAfvyHrqVM
Review finding (CodeRabbit): substring name matching mislabels tracks. "bassoon" contains "bass" and so is forced to Bass, and "Bass Drum" matches bass before the drum check, so a bass drum enters the corpus as a bass under --with-bass. Two tests: a "Bassoon" track must not be a Bass on its name (it reads Guitar from its tuning here), and a "Bass Drum" is percussion, not bass. Both fail against the current substring match. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NkqJUU6d1sW1RAfvyHrqVM
`classify_track_role` now splits the name into alphanumeric tokens and matches whole words, and tests the non-fretted roles (drums, percussion, vocals) before bass. So "bassoon" no longer reads as a bass, and "Bass Drum" is percussion, not a bass admitted under --with-bass. The two RED tests pass; the existing name-based cases still hold. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NkqJUU6d1sW1RAfvyHrqVM
Two review findings, pinned:
- slugify returns "" for a stem with no ASCII alphanumerics ("曲", "---"), so
chunk ids would start with `_g`. It must fall back to a stable nonempty id.
- placing a source beside its chunks needs a collision policy: write when
absent, reuse when byte-identical, and refuse — never overwrite — when a
different file already claims the name (`source_copy_decision`, stubbed to
always Write here).
Both tests fail against the stub/current behaviour.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NkqJUU6d1sW1RAfvyHrqVM
…ingest Closes the two P1 review findings and three robustness ones: - **Sources are placed in the corpus** (P1): `cmd_ingest` copies each source beside its chunks under the collision policy — write when absent, reuse when byte-identical, refuse (never overwrite) when a different file claims the name, skipping that file. Without this the loader's `dir.join(filename)` finds nothing and `griff generate --corpus` silently drops every ingested chunk. - **The loader verifies the hash** (P1): `load_chunk` checks the source's SHA-256 against the record's `sha256` before importing, so a same-named but different file cannot supply the notes. - **Per-file assembly errors skip, not abort** the run, like read/import errors. - **Cross-run group ids**: `used_ids` is seeded from the corpus's existing `*.group.json`, so re-ingesting into a populated directory never reuses an id. - **slugify** falls back to `untitled` for a stem with no ASCII alphanumerics. `source_sha256` moved to `griff_core::corpus` (with the `sha2` dep) so the ingest that writes the hash and the loader that verifies it share one implementation; `sha2` is dropped from the CLI. Two RED tests pass; the pure `source_copy_decision` policy is unit-tested, the I/O around it by the manual acceptance re-run. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NkqJUU6d1sW1RAfvyHrqVM
Review addressed — HEAD
|
| # | finding | resolution |
|---|---|---|
| part_index gap | a selected track with no surviving phrase left a hole | compact the index; regression test for an empty first track (859ea15) |
| bass labelled guitar | _g/"(guitar)" applied to --with-bass parts |
id tag + title derived from TrackRole (859ea15) |
| role by substring | "bassoon" → bass, "Bass Drum" → bass | whole-word tokens, non-fretted roles tested first (core, red+green) |
| per-file abort | one assembly error killed the run | caught and skipped like read/import errors (3381e97) |
| cross-run collisions | used_ids empty each run |
seeded from the corpus's existing *.group.json (3381e97) |
| slugify "" | non-ASCII / punctuation stems | falls back to untitled + dedup (3381e97) |
| source hash | filename ≠ identity | sha256 pinned and verified on load (3381e97) |
One claim corrected, not code
"Empty tags" was wrong — thank you. build_chunk_meta auto-derives technique tags (palm-mute, hammer-on, …) from the notation, so ingested chunks carry those. They are facts read from the tab, not curatorial choices, and I'd rather keep them than strip a free, correct signal. What the "uncurated candidate" claim actually means is no curatorial tags and no reviewer decision — the swancore style tags and the accept/reject a human picks are empty. I've corrected the PR wording accordingly.
One deferred, with a reason
MIDI without a tuning defaults to Standard E, so an unnamed MIDI piano track classifies as a guitar. Real: but out of this scope. Ingest targets Guitar Pro tabs, which carry genuine per-track tunings (the classifier already routes an unreadable/placeholder tuning to Other and skips it). A MIDI-safe classifier needs source-format-aware evidence (e.g. require explicit NotePosition), which is a separate refinement, not a fix to this bulk-GP path. Filed as a follow-up.
Validation
fmt · clippy -D warnings · test --workspace --exclude griff-cli (core 239, cli-lib 24) · cargo doc 15 = baseline · MSRV 1.92 · fuzz nightly — all clean. Schema v9 is additive: pre-v9 records round-trip byte-identically and load with the legacy fallback (pinned by a test).
The schema bump to v9 broke `cockpit-web-test`: `cockpit.capture.test.js` asserts the OPFS-built manifest is schema-v8, so it failed `9 !== 8`. The local validation matrix cannot run this workflow (it needs a headless browser over the wasm build, which does not compile here for lack of clang), so the stale assertion only surfaced on CI — the whole point of that gate. Updates the web assertion to v9 and renames the misnamed core `schema_version_is_8` test (its assertion was already 9) to match. No production change. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NkqJUU6d1sW1RAfvyHrqVM
griff ingest— bulk-build corpus chunks from a folder of tabsTurns a directory of scattered Guitar Pro / MIDI tabs into linked, rights-stamped phrase chunks in one pass — the candidate pile the corpus is curated from. Built toward the S12 precondition (a corpus of ≥ ~100 phrases), though those chunks are uncurated candidates: the curatorial tags (swancore style) and the reviewer decision are filled later in the cockpit. Chunks do carry the auto-derived technique tags
build_chunk_metareads from the notation (palm-mute, hammer-on, …) — facts from the tab, not curatorial choices.18ec861(main)922dfcbWhat it does
griff ingest <dir> [--output corpus] [--with-bass]— for each file:import_score_auto);--with-bass, skipping drums/vocals;group.json, rebuilds the manifest, and prints a skip report (no guitar, import error, or no phrase survived splitting).The heart, and the design that kept it small
The genuinely new logic is three pure, unit-tested
griff_core::ingestfunctions:classify_track_role— role from name, channel 9, and open-string tuning; an all-C-1placeholder tuning is not an instrumentselect_ingest_tracks— guitars always, bass under the flag, order preservedtuning_label— common tunings by name (standard_e,drop_d, …), the rest spelledc2_g2_c3_f3_a3_d4so nothing is lostThe CLI side is composition, not new machinery:
build_chunk_metaalready accepts an ensemble link and a rights record, sophrase_chunks_for_track(agriff splitthat takes an explicit track instead of the first note-bearing one) sets the group link on each phrase after the split. No split-path signature changed and no existing test was touched — the first design threaded a newensembleparam throughchunks_for_segments, which trippedtoo_many_arguments; setting the publicChunkMeta.ensemblefield post-split is cleaner and inert to the existinggriff split.assemble_ingest_groupis the tested seam;cmd_ingestis thin I/O around it.The classifier also protects the corpus: a track whose tuning can't be read (the
C-1placeholder) is classifiedOtherand skipped, so a guitar chunk is never ingested with a bogus tuning. Those placeholder tunings are exactly the non-guitar tracks we drop.RED → GREEN
f2958d12ba4b4aa4164e064c28dd49cc5775d2eae7e6f78aedea1880griff ingestcommand wired on top56950e5d38644666c884465e1d7bc324b0ef20cdd0track_index— a chunk reloads its exact source track, never substitutesa866df5859ea15track_index/sha256; part-index compaction; bass ≠ guitar namingb87c10e4a9d9d07fb3bb03381e97The two acceptance-found pairs came from the first manual run, and the last four from the CodeRabbit + Codex review — both the sign of the process catching what reasoning didn't.
Why the fingering data matters, and is already safe
Preserving the real per-track tuning is not cosmetic: the model keeps each note's explicit Guitar Pro
(string, fret)asNotePosition::explicit(gp.rs), and the tuning is the frame that makes those positions mean a pitch. That is exactly what a future fingering-aware generator needs, and it cannot be backfilled from pitches — so it is captured at ingest even though today's pitch-space S6/S7 generation does not read it.tuning_labelis what records it legibly.Manual acceptance (real run, files not committed)
griff ingest <tabs> --output <dir>over the real 410-file collection. GP7.gpsupport is in this branch (via the merge of #133), so the.gpfiles ingest too:9,909 phrases is far past the ~100-phrase S12 gate — the room is for curating down, not scraping for more. 359 of the 401 files yielded two or more guitar parts (the two-independent-guitar writing this targets).
A sample chunk (part 1 of a two-guitar tab) carries exactly what ingest promises — including the exact source track and content hash the loader needs (schema v9):
Round-trip verified — the acceptance that was missing the first time: after ingest,
griff generate --corpus <dir>loads the chunks (corpus: 38 chunks, 0 skipped on a 3-tab corpus) where before the source-copy + schema-v9 fixes it skipped every one. Named tunings resolve order-agnostically:drop_dandstandard_edominate; the rest are extended-range tunings spelled consistently low-to-high.Three defects surfaced only by the manual run — each fixed with its own RED → GREEN pair:
.gp5and.gpx) overwrote chunks untilunique_group_id;tuning_labelsorted ascending;track_index+sha256) and the source-copy — see the review-response comment.The corpus is copyrighted community tabs (
redistributable: false), so no tab and none of these derived chunks are committed —corpus/is git-ignored (ADR-0005). The end-to-end run is manual evidence, not a committed fixture test; the pureingestlogic and the strict loader are unit-tested on synthetic scores.Validation
cargo fmt --all --checkcargo clippy --workspace --all-targets -- -D warningscargo test --workspace --exclude griff-clicargo doc --no-deps --workspace18ec861cargo +1.92 check --workspace --all-targets(MSRV)cargo test -p griff-clifails onlymissing_file_golden— the pre-existing Russian-locale golden (os error 2renders localised on this machine), identical at base, green on CI's English runner.Scope / not in this PR
PairRelations is a trivial follow-up if wanted..gpsupport (PR feat(core): import Guitar Pro 7/8 (.gp) #133) is already in this branch via the main merge, so the 79.gpfiles ingest here — no ordering dependency remains.Not self-accepted, not merged.
🤖 Generated with Claude Code
https://claude.ai/code/session_01NkqJUU6d1sW1RAfvyHrqVM
Summary by CodeRabbit
New Features
Bug Fixes
Refactor