Releases: noahbaculi/guitar-tab-generator
v2.1.0
Maintenance release: no new features and no public API change, so the Rust and .d.ts surfaces stay byte-identical to 2.0.0. It swaps tsify-next -> tsify to clear RUSTSEC-2025-0048, raises the MSRV to Rust 1.87, and lands a batch of allocation and pathfinding cleanups that speed up arrangement generation and rendering.
Changed
- Raised the minimum supported Rust version to 1.87, up from 1.86 (
rust-versioninCargo.toml). This followspathfinding4.15.0, which moved to Rust edition 2024 and so requires 1.87. The bump carries no functional change for this crate. The onlyyen-related fix in 4.15.0 is a panic whenk == 0, which is unreachable here becausenum_arrangementsis a validatedNonZeroU8, so thekpassed toyenis always at least 1. Consumers on a toolchain below 1.87 need to runrustup update.
Internal
-
Moved the TypeScript-surface derive from
tsify-nextback totsify. RUSTSEC-2025-0048 flagstsify-nextas unmaintained, and its own final 0.5.6 release carries a deprecation notice pointing totsify. Both crates sit at 0.5.6 with the samejsfeature and#[derive(Tsify)]attributes, so the change is the crate name inCargo.tomlplus threeusestatements. The generated.d.tspublic surface is unchanged except for one doc comment that named the old crate, pinned by thetests/snapshots/wasm.d.tssnapshot. See ADR-0010. -
Index
yen()successors by node group instead of re-scanning the full node list.calc_next_nodesnow looks up the one contiguous beat group throughpath_node_groupsrather than filtering a flattenedVec<Node>, which drops the adjacency term from roughly O(k * N^2) toward O(group size) on multi-arrangement and long inputs. Arrangement output is byte-for-byte unchanged. In CI thefur_elise_3_arrangementsandfur_elise_5_arrangementsbenches fell about 22% (roughly 21.6 ms to 16.7 ms) while the untouchedparse_linescontrol held flat. The single-arrangement long-input bench moved only 1 to 3%, matching the O(k * N^2) shape where the win scales with the arrangement count k. (Indicative cross-run CI figures.) -
Dropped the
averagedependency and compute the non-zero-fret mean inline. This removes eight crates from the non-dev dependency graph (average,rayon,rayon-core, the threecrossbeam-*crates,easy-cast,float-ord), so Rayon no longer compiles into the production or WASM build. That restores ADR-0009's no-Rayon state and slightly shrinks the size-optimized WASM artifact. The mean is arithmetically identical, pinned by the existingcalc_avg_non_zero_frettests. -
Compile the pitch regex once into a process-lifetime
LazyLock<Regex>static instead of rebuilding it on every cache-missingparse_linescall. Behavior is unchanged. Theparse_linesbench, which reparses each iteration, fell from roughly 285 µs to 77 µs in CI (about a 3.7x speedup), since regex compilation dominated the per-parse cost and now runs once for the process. (Indicative cross-run CI figure.) -
Pad a rendered row's trailing dashes in place with
pushinstead of building and copying a throwaway"-".repeat(..)String. The row buffer already reserves the full width, so the bytes have a home. Render output is unchanged, pinned by the render snapshot tests. Therender_tabbench fell roughly 4 to 6% against the prior commit in CI, while the untouchedparse_linesandfur_elise_1_arrangementbenches held flat, so the gain tracks this change rather than runner variance. (Indicative cross-run CI figure, not a persisted baseline.) -
Measure each fret's display width in
calc_fret_width_maxwith a digit-count check instead of allocating aStringper fingering to read its length. The width is identical for any fret and reuses the same digit rulerender_fretalready applies. Therender_tabbench fell a further 3.5 to 4.5% in CI, roughly 8 to 9% cumulative across the two render cleanups. -
Build the rendered tab body directly into a single
Stringinstead of cloning each row into aVec<String>and joining it.render_string_outputnow pushes each borrowed row and writes the▼/▲playback lines in place, dropping the per-row clone, the intermediate vector, the second copy fromjoin, and the" ".repeat(..)playback-indent temp string. Render output is unchanged (PRD finding F6), pinned by the render snapshot tests. Therender_tabbench fell roughly 11% against the prior commit in CI (about 56.4 µs to 50.2 µs across the parameter sweep), while the untouchedparse_linesandfur_elise_1_arrangementbenches held flat. (Indicative cross-run CI figure, not a persisted baseline.)
Full Changelog: v2.0.0...v2.1.0
v2.0.0
2.0.0 -- 2026-06-05
Breaking changes
wasm_create_guitar_compositions(input)replaced bygenerateArrangements(input)(JS) /generate_arrangements(input)(Rust).- JS export
get_tuning_names()renamed togetTuningNames()and now returns the typedTuningName[]union instead ofstring[]. The Rust crate-root re-export keeps the nameget_tuning_names; its return type is nowVec<TuningName>. - Output shape:
Composition[]replaced by a singleArrangementSethandle. Access pattern:for (let i = 0; i < set.len; i++) set.render(i, width, padding, playback). - Input field
pitchesrenamed toinput. - Input field renames flow to the JS side as
numArrangements,tuningName,guitarNumFrets,guitarCapo,maxFretSpanFilter. - Render parameters (
width,padding,playback) moved from input bundle toArrangementSet.render(...). normalized_inputsentinels ("REST","MEASURE_BREAK") replaced by tagged variants ({ kind: "rest" },{ kind: "measureBreak" }).- Errors are typed: throws
TabErrorwithkinddiscriminator.Parsecarrieserrors[].lineanderrors[].textfor inline editor highlights. TuningNamewire serialization switched from PascalCase ("OpenG") to camelCase ("openG"). Input parsing remains case-insensitive.- Renamed
CompositionInputtoTabInputto align with the domain glossary; the old name was a 1.x holdover. create_arrangementstakesNumArrangementsinstead ofu8fornum_arrangements; construct viaNumArrangements::try_new(n)?. Direct Rust callers no longer validate the range themselves.memoized_original_create_arrangementsandmemoized_original_parse_linesmoved from the crate root to the__bench_internalsnamespace. The namespace is#[doc(hidden)], not part of the stable 2.x API, and may be removed without a major version bump.Arrangement::linesis now a getter returning&[Line<BeatVec<PitchFingering>>]instead of apubfield. Direct Rust consumers callarrangement.lines()instead of&arrangement.lines.difficultyandmax_fret_spanwere already getters.parse_tuning,create_string_tuning_offset, andSTD_6_STRING_TUNING_OPEN_PITCHESare no longer re-exported from the crate root. They were leaked from a 1.x composition pattern thatgenerate_arrangementsand thetuning_namefield onTabInputmake redundant; non-preset tunings continue to flow throughcreate_string_tuning(&[Pitch])plusGuitar::new. The two helpers remain reachable from criterion benches via__bench_internalsand may be removed without a major version bump.wrapper_create_arrangementsis renamed togenerate_arrangements. The Rust function and the JS function (generateArrangements) now share a single implementation and parallel names; the#[wasm_bindgen]wrapper is gone. Direct Rust callers update their imports.TabErrorflattened: the umbrellaGuitar { message },Arrangement { message }, andInvalidInput { field, message }variants are removed. Each failure mode now has its own variant with structured payload. See MIGRATION.md for the full mapping and ADR-0007 for the rationale.TabError::NoArrangementsFound(payloadless) reports the case where every input pitch reaches the guitar but no valid arrangement exists. Reachable from inputs like duplicate pitches in a single beat (e.g."E2E2"), which theno_duplicate_stringsconstraint filters to zero candidate fingerings. JS callers handling an exhaustiveswitch (err.kind)must add a"noArrangementsFound"arm.UnplayablePitchis now a public type carried byTabError::UnplayablePitches. Replaces the prose error string with structured{ value, line }records.StringNumber::new,Guitar::new, andcreate_string_tuningreturnResult<_, TabError>instead ofanyhow::Result. Direct Rust callers must update error handling.Pitch::plus_offsetreturnsOption<Pitch>instead ofanyhow::Result<Pitch>. Callers replace?with.ok_or_else(...).Guitar::newvalidates thatcapo <= num_fretsbefore computingplayable_frets. The previous code underflowed; the new behavior isTabError::CapoExceedsFrets.tuningName: ""no longer means standard tuning. Pass"standard"(case-insensitive) explicitly.Guitar::MAX_NUM_FRETSandGuitar::MAX_CAPOare nowpub constonGuitar, alongside the existingNumArrangements::MAX. (Additive.)StringNumber::MAXis nowpub constonStringNumber. (Additive.)TabInputis now#[non_exhaustive]. Construct it withTabInput::new(input, tuningName, guitarNumFrets, guitarCapo, numArrangements)and set the optional filter via.with_max_fret_span_filter(n). JS callers are unaffected; the deserialized wire shape is unchanged. See ADR-0008.- Crate edition upgraded to 2024; minimum supported Rust version is now 1.86, declared via
rust-versioninCargo.toml.
Added
TabInput.maxFretSpanFilter: Option<u8>filters arrangements by maximum non-zero fret span. Emitted asmaxFretSpanFilter?: numberin the TypeScript surface, so TS-strict callers may omit the key.PitchFingering::string_number(),::fret(), and::pitch()getters give Rust callers structured read access to the fingerings returned byArrangement::lines(), replacing the previousDebug-only path. The fields staypub(crate); the getters are the stable surface.UnplayablePitchis re-exported from the crate root, so direct Rust callers can name the type in signatures. It was previously reachable only as aTabError::UnplayablePitchesfield value.TabError::RenderWidthTooSmall { width, min }is returned byArrangementSet::renderwhenwidthis below the minimum needed to lay out one beat at the givenpadding(min_render_width(padding)). A too-small width previously underflowed the renderer's column math (debug panic, release allocation blow-up) or stalled its wrap loop. JS callers with an exhaustiveswitch (err.kind)may add a"renderWidthTooSmall"arm; the existing default arm already covers it.- Input longer than 65,535 lines returns
TabError::InputTooManyLines { max }(wheremaxis the inclusive line limit) rather than overflowing the internalu16beat index. JS callers with an exhaustiveswitch (err.kind)may add an"inputTooManyLines"arm; the existing default arm already covers it. A real transcription is far below this bound, so this only rejects pathological input. TuningNamenow derivesClone, Copy, PartialEq, Eq, HashandNormalizedBeatderivesPartialEq, Eq, so Rust callers can compare the values returned byget_tuning_names()andArrangementSet::normalized_input()with==rather thanmatches!.
Internal
- Adopted
tsify-nextfor typed TypeScript bindings.serde-wasm-bindgenis now a transitive dependency only; no consumer-visible change beyondcargo treeordering. - Parser returns structured
Vec<ParseError>internally; the wire format reuses the same struct viacrate::error::ParseError. - Released
CONTEXT.md(domain glossary) and the architecture decision records indocs/adr/(ADR-0001 through ADR-0008), beginning with the opaque-handle pattern in ADR-0001. TabErrornow derivesPartialEq, Eqand carries#[non_exhaustive]so future structured-error variants land non-breakingly.- Dropped the unused
serde"rc"feature.