id-forge 1.0.0

Typed, high-performance unique ID generation for Rust. UUID v4/v7, ULID, Snowflake, NanoID, and custom epoch-based schemes in one zero-dependency library. Monotonic, distributed-safe, sortable variants.
Documentation
# Changelog

All notable changes to this project will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

## [1.0.0] - 2026-05-20

### Stable API

The `0.9.x` cycle delivered every algorithm; `1.0.0` is the
**stability commitment**. Every public item is now committed
under strict SemVer per [`docs/STABILITY.md`](docs/STABILITY.md).
Consumers can pin `id-forge = "1"` and expect minor / patch
updates within `1.x` to remain backwards-compatible.

### Added

- `docs/STABILITY.md` enumerating the frozen public surface, the
  behavioural contracts, and the items that are explicitly NOT
  part of the SemVer promise (internal PRNG choice, error `Display`
  text, transitive deps).
- `docs/API.md` — full API reference mirroring the `metrics-lib`
  format: example-pointers index, table of contents, installation
  + feature-flag matrix, an error-handling and panic-guarantees
  table, a section per public type (constructors, accessors,
  parsing, behavioural contract, multiple per-method examples),
  five real-world walkthroughs (time-ordered DB primary keys, URL
  shortener, distributed worker IDs, clock-skew recovery,
  migration from the `uuid` / `ulid` crates), and a performance
  summary.
- Rustdoc examples on `Uuid::as_bytes`, `Uuid::version`,
  `Ulid::as_bytes`, `Snowflake::worker_id`, and `Snowflake::epoch_ms`.
  Every public function and type now has an example per the
  pre-`1.0` directive.
- Seven new per-scheme examples in `examples/`:
  `uuid_v4`, `uuid_v7`, `ulid_monotonic`,
  `snowflake_distributed`, `snowflake_clock_skew`,
  `nanoid_short_url`, `nanoid_validate`. Each declares its
  `required-features` in `Cargo.toml` so `--no-default-features`
  CI runs skip them cleanly.

### Changed

- No source change to algorithm behaviour. Bytes a `Uuid::v4()`
  emits are identical to `0.9.3`.

### Frozen surface — summary

* **uuid**: `Uuid` + `nil/max/v4/v7/from_bytes/as_bytes/version/parse_str`
  + `ParseError`. Implements `Debug, Clone, Copy, PartialEq, Eq,
  Hash, PartialOrd, Ord, Default, Display, FromStr`.
* **ulid**: `Ulid` + `nil/max/new/from_bytes/as_bytes/timestamp_ms/parse_str`
  + `ParseError`. Implements `Debug, Clone, Copy, PartialEq, Eq,
  Hash, PartialOrd, Ord, Default, Display, FromStr`.
* **snowflake**: `Snowflake` + `new/with_epoch/worker_id/epoch_ms/try_next_id/next_id/parts`
  + `ClockSkew` + constants `DEFAULT_EPOCH_MS`, `SEQUENCE_BITS`,
  `WORKER_BITS`, `TIMESTAMP_BITS`.
* **nanoid**: `generate/with_length/custom/try_custom/validate_alphabet`
  + `AlphabetError` + constants `DEFAULT_ALPHABET`, `DEFAULT_LENGTH`.

### MSRV

Rust **1.75**, frozen at `1.0.0`. Within `1.x`, MSRV bumps are
advertised but not treated as breaking.

### Verification

Run on Windows x86_64, rustc 1.95; the full CI matrix
(ubuntu-latest, macos-latest, windows-latest) passes the same
gate:

```
cargo fmt --all -- --check
cargo clippy --all-targets --all-features    -- -D warnings
cargo clippy --all-targets --no-default-features -- -D warnings
cargo build  --verbose
cargo build  --all-features --verbose
cargo build  --no-default-features --verbose
cargo test   --verbose
cargo test   --all-features --verbose
RUSTDOCFLAGS="-D warnings" cargo doc --all-features --no-deps
cargo +1.75 build --all-features --verbose
```

Test coverage at this release:

* 73 unit tests across `rng`, `uuid`, `ulid`, `snowflake`, `nanoid`.
* 16 smoke tests covering every public constructor.
* 35 doctests on the public API examples (every public function /
  type now has at least one).

## [0.9.3] - 2026-05-20

### Added

- `nanoid::try_custom(length, alphabet) -> Result<String, AlphabetError>`
  — strict counterpart to `custom`. Validates the alphabet (non-empty,
  no duplicate bytes) before generating.
- `nanoid::validate_alphabet(&[u8]) -> Result<(), AlphabetError>` so
  callers can vet a configuration alphabet once at startup.
- `nanoid::AlphabetError` enum (`Empty`, `Duplicate(u8)`) implementing
  `Display` and `std::error::Error`.
- `examples/bench.rs` — dep-free single-thread throughput harness for
  all four schemes. Run with `cargo run --release --example bench`.
  Uses `std::time::Instant`; no Criterion, no external dependency.

### Changed

- `nanoid::custom`, `with_length`, and `generate` now draw from the
  shared `crate::rng` xoshiro256\*\* generator instead of the 0.1.0
  LCG placeholder.
- Character selection switched from `byte % alphabet.len()` to a
  bitstream with a smallest-power-of-two mask and rejection sampling.
  Result: every character in any non-power-of-two alphabet has the
  same probability of being chosen. The 0.1.0 placeholder was
  measurably biased on a 17-character alphabet; the new
  implementation passes a ±12 % uniformity band on 170 000 samples.
- A length of `0` now short-circuits to the empty string instead of
  entering the generation loop.
- A single-character alphabet is treated specially (every output
  character is that single byte) instead of falling into a no-op
  loop.

### Tests

- 10 000-ID uniqueness sweep on the default 21-character alphabet.
- Bias check: 170 000 characters drawn over a 17-char alphabet, each
  position's frequency stays within ±12 % of the uniform expectation.
- `try_custom` rejects empty alphabets and the first duplicate byte
  encountered.
- `validate_alphabet` exposed as a public helper with its own tests.
- `mask_bits` lookup table verified for 2, 8, 64, 65, 256.
- Length round-trip across alphabet sizes {2, 7, 16, 33, 64, 65, 128, 200}.

### Notes

`nanoid::custom` remains permissive: empty alphabet returns `""`,
duplicate bytes are tolerated (and bias the distribution toward the
repeated bytes — by design, since this is the unchecked entry point).
Callers who want validation use `try_custom`.

## [0.9.2] - 2026-05-20

### Added

- `Snowflake::try_next_id() -> Result<u64, ClockSkew>` — explicit
  clock-skew handling. Returns `Err` when the wall clock has moved
  backward since the most recent ID; the existing `next_id()` keeps
  its `-> u64` signature and panics on the same condition (panic
  message names the millisecond offsets involved).
- `Snowflake::parts(id) -> (timestamp_offset_ms, worker_id, sequence)`
  `const fn` decomposition for any Snowflake-layout ID, no matter
  which generator produced it. The timestamp is the offset from the
  generator's epoch; callers add `Snowflake::epoch_ms()` to get the
  wall-clock millisecond.
- `Snowflake::worker_id()` and `Snowflake::epoch_ms()` accessors.
- `ClockSkew` error type carrying `last_ms` and `now_ms` (offsets
  from the generator's epoch), implementing `Display` and
  `std::error::Error`.
- Public constants `SEQUENCE_BITS`, `WORKER_BITS`, `TIMESTAMP_BITS`
  so callers can spell the layout instead of hard-coding `12`, `10`,
  `41` in their own decoders.

### Changed

- `Snowflake::next_id` is no longer a per-call counter+time
  approximation. It runs a lock-free CAS loop over a packed
  `(last_ms, next_seq)` atomic word: monotonic within a worker, no
  duplicate IDs even under heavy multi-thread contention, sequence
  resets to 0 at each new millisecond.
- Sequence exhaustion (4096 IDs in a single millisecond) now blocks
  the calling thread in a microsleep loop until the wall clock
  advances, instead of issuing duplicate sequence numbers. Wait is
  bounded by one millisecond minus elapsed time in the current ms.
- `Snowflake::new` and `Snowflake::with_epoch` are now `const fn`.

### Tests

- 10 000-ID monotonic burst on a single generator.
- 50 000-ID uniqueness sweep.
- 8 threads × 2 000 IDs = 16 000 IDs under contention, all unique.
- Forced clock-skew scenario via direct state manipulation:
  `try_next_id` returns `Err(ClockSkew)`, `next_id` panics.
- Sequence-exhaustion scenario: pre-load the state to mark a
  millisecond exhausted and confirm the next call blocks until the
  next millisecond and starts the new ms at sequence 0.
- `parts` round-trip and per-field extraction against a hand-built
  ID.

## [0.9.1] - 2026-05-20

### Added

- `Ulid::new` real implementation per the [ULID spec]: 48-bit
  big-endian millisecond timestamp prefix + 80 bits of randomness
  from the shared xoshiro256\*\* generator.
- **Monotonic factory.** Two ULIDs minted in the same millisecond by
  the same process are strictly byte-wise ordered: the second one is
  the first one's random suffix plus one. Cross-millisecond, fresh
  randomness is drawn. Implements the "monotonic" guarantee from the
  spec.
- `Ulid::nil()` (all-zeros) and `Ulid::max()` (`7ZZZZZZZZZZZZZZZZZZZZZZZZZ`,
  the largest valid 128-bit encoded value).
- `Ulid::from_bytes(&[u8; 16])` to wrap an existing big-endian
  representation.
- `Ulid::parse_str(&str)` and `impl FromStr for Ulid`. Case-insensitive,
  honours Crockford substitutions (`I`/`L` -> `1`, `O` -> `0`); `U`
  is reserved and rejected. Returns `ParseError` with the failing
  byte position or an `Overflow` variant when the leading character
  exceeds 7 (which would set the 129th bit).
- `Ulid::timestamp_ms()` accessor for the 48-bit millisecond prefix.
- `Default` for `Ulid` (returns `nil`).
- `PartialOrd` / `Ord` on `Ulid`.

### Changed

- ULID random source: 0.1.0 counter+time placeholder -> shared
  xoshiro256\*\* PRNG. The 80-bit suffix is now genuinely random across
  milliseconds; within a millisecond it advances by +1 for strict
  monotonicity.
- Crockford base32 `Display` rewritten as a `u128`-shift loop — the
  previous implementation was already correct but built per-nibble on
  every iteration; the new one is one branch-free pass.
- Internal: extracted `Xoshiro256SS` into a private `crate::rng`
  module now that both `uuid` and `ulid` consume it. UUID generation
  is byte-identical to 0.9.0.

### Tests

- ULID spec round-trip on `01ARZ3NDEKTSV4RRFFQ69G5FAV`.
- Crockford substitutions: `I`, `L`, `O` decode to `1`/`1`/`0`; `U`
  is rejected.
- Leading-character overflow (`8…`, `Z…`) is reported as `Overflow`.
- 10 000-ULID uniqueness sweep.
- 1000-burst monotonicity check (strictly increasing within a process).

[ULID spec]: https://github.com/ulid/spec

## [0.9.0] - 2026-05-20

### Added

- `Uuid::v4` real implementation per RFC 9562 §5.4: 122 random bits with
  version `0100` and RFC 4122 variant bits.
- `Uuid::v7` real implementation per RFC 9562 §5.7: 48-bit big-endian
  millisecond timestamp prefix, 74 random bits, version `0111`, RFC 4122
  variant bits, byte-wise time-ordered across milliseconds.
- `Uuid::nil()` (all-zeros, §5.9) and `Uuid::max()` (all-ones, §5.10).
- `Uuid::from_bytes(&[u8; 16])` to wrap an existing big-endian
  representation without touching version/variant bits.
- `Uuid::parse_str(&str)` and `impl FromStr for Uuid` for the canonical
  36-character hyphenated form, case-insensitive. Errors expose the
  failing byte position via `ParseError`.
- `Uuid::version()` accessor for the high nibble of byte 6.
- `Default` for `Uuid` (returns `nil`).
- `PartialOrd` / `Ord` on `Uuid` so v7 IDs sort byte-wise by timestamp.
- Inline xoshiro256\*\* PRNG seeded from process ID, wall-clock
  nanoseconds, and a per-process counter via SplitMix64. Thread-local,
  no syscall after the first draw of a thread.

### Changed

- UUID random source switched from the 0.1.0 counter+time placeholder to
  the xoshiro256\*\* generator. Two consecutive `Uuid::v4` calls now
  differ in all 122 random bits, not just the trailing counter.

### Tests

- RFC 9562 Appendix A.2 (v4) and A.6 (v7) example strings parse and
  round-trip with the correct version/variant bits.
- 10 000-UUID uniqueness sweep on `v4`.
- v7 IDs generated 2ms apart compare strictly byte-wise.
- `parse_str` rejects wrong length, missing hyphens, and non-hex digits
  with the failing position reported.

### Notes

The randomness is fast non-cryptographic. For session tokens or API
keys, compose with a CSPRNG for the random portion — `id-forge` itself
intentionally has no `getrandom` dependency.

### CI

- `Cargo.toml` declares `required-features` on the `basic` example and
  the `smoke` integration test (all four scheme features). This makes
  `cargo clippy --all-targets --no-default-features` pass: Cargo
  simply skips the targets whose feature set is not active, instead of
  trying to compile them against missing modules.

## [0.1.0] - 2026-05-11

### Added

- Initial crate skeleton.
- `uuid` module: `Uuid::v4` and `Uuid::v7` (placeholder implementations).
- `ulid` module: `Ulid::new` with Crockford-base32 Display impl
  (placeholder timestamp/randomness mix).
- `snowflake` module: `Snowflake::new`, `with_epoch`, `next_id`
  (placeholder per-ms sequence logic).
- `nanoid` module: `generate`, `with_length`, `custom` (placeholder
  randomness).
- Feature flags: `std` (default), `uuid`, `ulid`, `snowflake`, `nanoid`.
- Smoke tests for each scheme.

### Note

This is the name-claim release. Real implementations follow RFC 9562
for UUIDs, the ULID spec, and the Twitter Snowflake design. Production
randomness lands in `0.9.x`.

[Unreleased]: https://github.com/jamesgober/id-forge/compare/v1.0.0...HEAD
[1.0.0]: https://github.com/jamesgober/id-forge/releases/tag/v1.0.0
[0.9.3]: https://github.com/jamesgober/id-forge/releases/tag/v0.9.3
[0.9.2]: https://github.com/jamesgober/id-forge/releases/tag/v0.9.2
[0.9.1]: https://github.com/jamesgober/id-forge/releases/tag/v0.9.1
[0.9.0]: https://github.com/jamesgober/id-forge/releases/tag/v0.9.0
[0.1.0]: https://github.com/jamesgober/id-forge/releases/tag/v0.1.0