Thank you for considering a contribution. StringCheese aims to be the canonical Rust library for sequence comparison, which means the bar for correctness, documentation, and API coherence is high. This document describes how to work in the repository so a pull request lands with minimum friction.
Clone and build:
git clone https://github.com/tegmentum/stringcheese.git
cd stringcheese
cargo test --workspace --all-featuresA first-time build compiles roughly a dozen crates. When the run
completes you should see a series of green test result: ok. … lines
— one per crate's unit-test binary and one per crate's doc tests —
concluding with a final zero-failure summary. If any of those turn red,
stop and open an issue; the tree is expected to be green on main.
The workspace pins its minimum supported Rust version (MSRV) at
1.85 and uses edition 2024. Any Rust toolchain at or above that
version is fine for development.
The workspace is split into small, focused crates. The map is
maintained in README.md; the short version is:
stringcheese-core— traits, result types,AlgorithmDescriptor,MetricProperties, workspace and sequence abstractions. Every other crate depends on this one.stringcheese-corpus— golden-case schema, oracle framework, exhaustive generators, differential vocabulary. Test-only for the algorithm crates but a first-class published deliverable in its own right.stringcheese-compare— the consolidated comparison-kernel crate. One top-level module per algorithm family (levenshtein,hamming,jaro,damerau,lcs,ngram,search,set_similarity,minhash); each module is ownership-scoped so it can evolve independently within a single unit of publication.stringcheese-<subsystem>— sibling crates for the non-comparison subsystems:stringcheese-unicode,stringcheese-phonetic,stringcheese-cdc,stringcheese-index,stringcheese-align,stringcheese-bench.stringcheese— a thin facade that re-exports the stable public API.
Design context lives under docs/:
docs/DESIGN.md— project vision, CI requirements, release gates.docs/design/type-system.md— how descriptors, metric properties, and result types compose.docs/design/preprocessing-pipeline.mddocs/design/phonetic-subsystem.mddocs/design/ngram-and-fingerprinting.mddocs/design/wasm-and-wit-interface.md
Read the relevant design doc before touching a subsystem — most API questions are already answered there.
This is the workflow most contributions follow. The order matters: skipping steps causes rework.
-
Pick a family. Every algorithm belongs to an
AlgorithmFamilyinstringcheese-core::descriptor. If your algorithm does not fit an existing family, propose adding a variant to that enum in a separate PR before starting on the implementation.AlgorithmFamilyis#[non_exhaustive]specifically so new variants are additive. -
Pick a variant slug. Slugs are the stable string keys used in the golden corpus and public reporting. Convention is
<family>-<qualifier>, lowercase, hyphenated:levenshtein-unit,hamming-byte,jaro-winkler-default. Once published a slug is API and cannot be renamed without a deprecation cycle. -
Decide DistanceMetric vs SimilarityMetric vs both.
stringcheese-coreexposes distinctDistanceMetricandSimilarityMetrictraits, plus normalized counterparts. Distance is not similarity; do not paper over the distinction by picking one and casting. If your algorithm is fundamentally a distance, implementDistanceMetricand let a caller convert. If it is fundamentally a similarity (Jaro is), implementSimilarityMetric. Some families support both — implement both if so, and document the relationship between them. -
Decide MetricProperties. Every descriptor declares the mathematical properties its metric satisfies: reflexivity, symmetry, triangle inequality, boundedness, and so on. Be conservative — a claimed property becomes a property test. Refer to
docs/design/type-system.mdfor the semantics of each flag. -
Write the
AlgorithmDescriptorconst. Put it in the crate'salgorithmmodule,pub const, one per variant. This is the registry entry the corpus references and the release report cites. -
Wire golden cases to the corpus schema. Add cases to the crate's
golden.rs(orgolden/submodule). Cases must use the schema fromstringcheese_corpus, must reference the descriptor's variant slug, and must include enough coverage to demonstrate every documented edge case — empty inputs, identical inputs, maximally different inputs, unicode boundary cases where applicable. -
Add property tests for the metric axioms. Use
proptest. Every property yourMetricPropertiesclaims must be exercised: symmetry, identity of indiscernibles, triangle inequality, etc. Also add a differential test against the internal oracle if the crate has multiple kernels — the whole point of the oracle-plus-optimized split is that they must agree.
Each of the three shipped algorithms illustrates a different point on the spectrum:
crates/stringcheese-compare/src/hamming— the minimal case: single kernel, single descriptor. Start here to understand the smallest possible shape a module can take.crates/stringcheese-compare/src/levenshtein— the multi-kernel case:full_matrix.rs(oracle),rolling_rows.rs(production),banded.rs(Ukkonen cutoff variant), plus a sharedworkspace.rsfor scratch-buffer reuse. Study the module split before splitting your own algorithm across files.crates/stringcheese-compare/src/jaro— theFloatExpectationcase: because Jaro is a floating-point similarity, its golden cases and property tests useFloatExpectationto control comparison tolerance rather than exact equality.
- Documentation first. The workspace enables
missing_docs = warn. Everypubitem — module, type, function, const, variant — needs rustdoc. Reviewers will bounce PRs that add new public API without documentation. - No emoji. In code, comments, commit messages, docs, changelog entries, or PR descriptions. This project treats prose as a load- bearing surface and emoji tend to erode it.
#![forbid(unsafe_code)]. Every crate that ships an algorithm should forbid unsafe code at the crate root. If an implementation genuinely requires unsafe (SIMD intrinsics, for example), it belongs in a dedicated module gated behind a feature flag and reviewed separately.- Workspace lints.
[workspace.lints]in the rootCargo.tomlenables the pedantic clippy pass and warns onmissing_docsandrust_2018_idioms. Do not silence workspace lints in a crate; if a lint fires on a legitimate pattern, use#[allow(…, reason = "…")]with a real reason string at the smallest scope that works. - Conventional Commits. Commit subjects follow
Conventional Commits 1.0:
feat(jaro): …,fix(levenshtein): …,docs: …,chore: …,refactor(core): …. Scope in parentheses is the crate short name (core,hamming,corpus, etc.) or a broader area (docs,ci). - Do not mention Claude, Anthropic, or any AI assistant in code, commits, PR descriptions, changelogs, or docs. Contributions stand on their content, not their tooling.
- Default:
#![forbid(unsafe_code)]in every crate. Every algorithm crate, every substrate crate, and every future addition starts with a crate-levelforbid(unsafe_code). That default is not aspirational — it is enforced at compile time, and CI will refuse to merge a crate that quietly removes it. - The one exception:
component/rust-host. The Wasm component-host crate uses# because the ABI plumbingwit-bindgengenerates for loweringlist<u8>and friends across the component boundary isunsafeat the language level. Every line of hand-written code in that crate is still safe Rust; the exception exists only so that macro-generated ABI code can compile without sprayingunsafe fnwrappers through the surface. A file-level comment incomponent/rust-host/src/lib.rsrecords the rationale next to the attribute. - Rule for future work. Any new crate defaults to
forbid(unsafe_code). If a genuine need for unsafe arises (SIMD intrinsics, FFI stubs, additional ABI plumbing, a hot path that must bypass a bounds check for measurable reason), the exception must be:- Scoped — behind a dedicated feature flag when the unsafe is
optional, or crate-scoped when it is intrinsic to the crate's
purpose. Do not sprinkle
unsafeblocks through an otherwise-safe crate. - Documented in-file — a file-level comment above the crate-root attribute explains why the exception exists and what invariant the unsafe code upholds.
- Documented here — add a bullet to this list naming the crate, the attribute, and a one-line reason. A reviewer who has never seen the codebase should be able to enumerate every unsafe-using crate from this file alone.
- Scoped — behind a dedicated feature flag when the unsafe is
optional, or crate-scoped when it is intrinsic to the crate's
purpose. Do not sprinkle
Every algorithm PR should include:
- Unit tests. Small, targeted, colocated with the code they exercise. Doc-tests count as unit tests for coverage purposes.
- Property tests via
proptest. One test per declared metric property. Seecrates/stringcheese-compare/src/hamming/property_tests.rsfor the pattern. - Differential tests (oracle vs optimized). When a module has more
than one kernel, one is the oracle and the others must agree with it
on generated inputs. See
crates/stringcheese-compare/src/levenshteinfor the reference layout. - Golden cases. Fixed input-and-expected-output records that
survive refactors and document the intended behavior. Use the
stringcheese-corpusschema so a shared runner can execute them. - Cross-target verification. New algorithms must build under
--no-default-featuresand--no-default-features --features alloc, and — untildocs/design/wasm-and-wit-interface.mdsays otherwise — should cross-compile towasm32-unknown-unknownandwasm32-wasip1. ThewasmCI job runs these checks withcontinue-on-error: trueuntil the whole workspace is clean; aiming for green on your PR is nonetheless the expected default. no_stdcompatibility. If a crate declaresalloc, its kernel must work withoutstd. Compile withcargo check -p <crate> --no-default-features --features allocbefore opening the PR.
CI runs these commands, in this order. Run them locally before you open the PR:
cargo test --workspace --all-features --locked
cargo clippy --workspace --all-features --all-targets --locked -- -D warnings
cargo fmt --all -- --check
RUSTDOCFLAGS="-D warnings" cargo doc --workspace --all-features --no-deps --lockedFor each crate that declares an alloc feature:
cargo check -p <crate> --no-default-features --locked
cargo check -p <crate> --no-default-features --features alloc --lockedFor the wasm cross-compile checks (requires rustup target add wasm32-unknown-unknown wasm32-wasip1 once):
cargo check --workspace --exclude stringcheese-bench \
--all-features --target wasm32-unknown-unknown --locked
cargo check --workspace --exclude stringcheese-bench \
--all-features --target wasm32-wasip1 --lockedIf any of these fail on main in a fresh clone, that is a bug worth
an issue — the tree should be clean.
- Branch off
main. Never commit tomaindirectly. - Keep commits focused. One logical change per commit. Squash fixup commits before opening the PR.
- One algorithm per PR when possible. A new
AlgorithmDescriptor, its kernel(s), its golden cases, and its property tests form a natural unit. Bundling two algorithms doubles review time and halves the chance the second one lands. - Keep PRs reviewable in a single sitting. A rough ceiling is about 500 lines net. Longer PRs need a strong argument for why they cannot be split.
- Update the changelog. Add an entry under
[Unreleased]inCHANGELOG.mdif the change is user-visible. - Fill in the PR template. The template exists so reviewers can find the important bits without reading the whole diff.
StringCheese is dual-licensed under MIT and Apache-2.0. By submitting a contribution you agree that it may be distributed under the same terms.