terseid
Adaptive-length, collision-resistant short IDs for Rust.
For: Applications that need compact, human-typeable identifiers with automatic collision avoidance. Not for: Cryptographic identifiers, UUIDs, or globally unique IDs across distributed systems.
Example
use ;
let gen = new;
let id = gen.generate;
// => "bd-a7x" (3 chars suffices for 42 items)
IDs grow automatically as your collection grows:
| Items | Hash length | ID space |
|---|---|---|
| 0-100 | 3 chars | 46,656 |
| ~200 | 4 chars | 1,679,616 |
| ~7,000 | 5 chars | 60,466,176 |
| ~250,000 | 6 chars | 2.18 billion |
Status
Experimental (v0.1.0). API may change before 1.0.
- Platforms: anywhere Rust compiles (no platform-specific code)
- Dependencies:
sha2,thiserror(2 runtime deps, no std feature gates) - Security: not for cryptographic use; hashes are truncated SHA256 for distribution, not security
Non-Goals
- Global uniqueness without a collision check function
- Cryptographic strength or unguessability
- Encoding arbitrary data in IDs
- Serde/serialization support (bring your own)
Mental Model
Seed bytes → SHA256 → first 8 bytes → base36 → truncate to length
↑
birthday problem picks this
- ID format:
<prefix>-<hash>[.<child>.<path>](e.g.,bd-a7x3q9,tk-r2m.1.3) - Adaptive length: hash length chosen by birthday problem math so collision probability stays below a threshold (default 25%)
- Collision avoidance: 4-tier fallback — nonce escalation, length extension, long fallback, desperate fallback
- Storage-agnostic: caller provides an
existsclosure; the generator never touches storage directly
Quick Start
Add to Cargo.toml:
[]
= "0.1"
Generate an ID:
use ;
let gen = new;
let id = gen.generate;
assert!;
Parse an ID:
use parse_id;
let parsed = parse_id.unwrap;
assert_eq!;
assert_eq!;
assert_eq!;
assert_eq!;
assert_eq!;
Resolve partial input (for CLIs):
use ;
let resolver = new;
let known_ids = vec!;
let resolved = resolver.resolve.unwrap;
assert_eq!;
Usage
Configuration
new // defaults: 3-8 chars, 25% threshold
new.min_hash_length // start at 4 chars
new.max_collision_prob // tighter threshold
Standalone hash
When you don't need collision avoidance:
use hash;
let h = hash; // deterministic 6-char base36 string
Child IDs
use ;
let child = child_id; // "bd-a7x.1"
let nested = child_id; // "bd-a7x.1.3"
assert!; // true
assert_eq!; // 2 levels deep
Parsing rules
- Last dash separates prefix from hash (supports
my-proj-a7x3q9) - 3-char hashes accept any base36; 4+ chars require at least one digit (avoids English word false positives)
- Child path segments are dot-separated u32 integers
Error handling
All fallible operations return terseid::Result<T>:
InvalidId— malformed formatPrefixMismatch— wrong namespaceAmbiguousId— multiple substring matches during resolutionNotFound— no match at any resolution stage
For AI Agents
- All functions are pure or take closures for side effects — no hidden I/O.
- The test suite (173 unit tests + 3 doc-tests) is the source of truth for behavior.
- Safe edit zones: individual modules in
src/are independent.lib.rsis just re-exports. - Required tooling:
cargo test,cargo clippy. No formatters enforced yet.
References
- spec.md — full specification with algorithm details, API docs, and migration guide
- beads_rust — the project this was extracted from