# Release v0.3.0 — Verification & Encoder Unification
**Date:** 2026-05-20
**Compare:** `v0.2.1...v0.3.0`

## Summary

The verification release. `v0.2.x` shipped the chain — `Chain::append` could
*write* a tamper-evident log, but the crate had no way to *prove* it was
untampered. As of `0.3.0`, [`Verifier`] is the proof side of that contract.

A `Verifier<H>` replays records in chain order and rejects any of four
classes of tampering:

| Mutation                                | Surfaced as            |
|-----------------------------------------|------------------------|
| A field byte was changed                | `Error::HashMismatch`  |
| A record's `prev_hash` was rewritten    | `Error::LinkMismatch`  |
| A record was skipped, reordered, or duplicated | `Error::IdMismatch` |
| A timestamp regressed (strict mode)     | `Error::NonMonotonicClock` |

Each variant carries the failing [`RecordId`], so callers know exactly
where the chain broke. The verifier's cursor stops at the failing record,
so post-mortem inspection of `Verifier::next_id` / `Verifier::last_hash`
points at the boundary between trusted and untrusted records.

In service of verification, the canonical record encoding — id || ts ||
actor || action || target || outcome || prev_hash, separated by `0x1f` —
is now lifted out of `Chain::append` into a crate-private
[`canonical`](src/canonical.rs) module. `Chain` and `Verifier` share one
encoder, so the producer and consumer of the hash cannot drift apart.
Any future change to the encoding is now an explicit, single-file edit.

This release also clears the lingering Node 20 deprecation warning by
bumping `actions/cache@v4` → `@v5`. All cache, checkout, and setup-node
actions are now on Node 24.

## Added

- **`Verifier<H>`** — the chain-replay proof type. Generic over the same
  `Hasher` trait the chain uses; two different hash algorithms will not
  interoperate, by design.
  - `Verifier::new(hasher)` — start from genesis.
  - `Verifier::resume(hasher, next_id, last_hash, last_timestamp)` —
    pick up mid-chain from a previously verified checkpoint.
  - `Verifier::with_strict_timestamps(bool)` — fluent toggle for the
    monotonic-timestamp check (default: on).
  - `Verifier::verify(&Record<'_>) -> Result<()>` — verify one record;
    on failure, the cursor does **not** advance.
  - `Verifier::next_id`, `Verifier::last_hash`,
    `Verifier::last_timestamp` — inspect the verifier's running state.
  - `Verifier::into_hasher()` — recover the hasher.
- **`Record::with_hash(self, Digest) -> Self`** — supports the
  draft-then-hash construction pattern used internally by
  `Chain::append` and externally by anyone reconstructing records from
  storage.
- **Three new `Error` variants**, all `#[non_exhaustive]`-safe:
  - `Error::HashMismatch(RecordId)`
  - `Error::LinkMismatch(RecordId)`
  - `Error::IdMismatch(RecordId)`
- **`tests/verify.rs`** — 7 integration tests:
  - clean-chain accept,
  - mutated-field detection,
  - broken-link detection,
  - skipped-id detection,
  - mid-chain resume,
  - strict-mode timestamp regression rejection,
  - relaxed-mode duplicate-timestamp acceptance.

## Changed

- **Canonical encoding centralised.** New crate-private `canonical` module
  owns `FIELD_SEP` and the `compute(hasher, record) -> Digest` function.
  `Chain::append` rewrote to draft a record with `Digest::ZERO`, hash via
  `canonical::compute`, then swap the real hash in via
  `Record::with_hash`. `Verifier::verify` calls the same `canonical::compute`
  and compares against the record's stored hash.
- **CI: `actions/cache@v4` → `@v5`** in [.github/workflows/ci.yml](.github/workflows/ci.yml).
  Clears the GitHub "Node.js 20 actions are deprecated" annotation. The
  `actions/checkout@v5` and `actions/setup-node@v5` steps were already on
  Node 24.

## Removed

- Duplicate `FIELD_SEP` constants and inline hashing protocols from
  `src/chain.rs` and `src/verify.rs`. Both now defer to `canonical`.

## Fixed

- Nothing user-visible. (See "Changed" for the deduplication that
  pre-emptively closes a class of future bugs: producer/consumer
  encoding drift.)

## Performance

No behavior changes on the append hot path:

- Still allocation-free (records borrow, digests are stack-resident).
- Still single-pass over field bytes through the hasher.
- The new `Record::with_hash` is a `const fn` field swap — no copy
  beyond the existing `Record` value semantics. `Record` is `Copy`.

`Verifier::verify` is also allocation-free and single-pass per record;
its cost is exactly one hash computation plus three constant-time
comparisons (id, prev_hash, timestamp).

## Compatibility

- **Source compatible** with `0.2.x`: no existing public item changed
  signature or was removed.
- **Wire compatible** with `0.2.x`: the canonical encoding bytes are
  byte-identical to what `Chain::append` produced in `0.2.x`. A chain
  written by `0.2.x` verifies under `0.3.0`.

## Verified

- `cargo build --all-features` — clean.
- `cargo build --no-default-features` — clean (no_std build verified).
- `cargo test --all-features` — 5 smoke + 7 verify + 17 doctests, all
  green.
- `cargo clippy --all-targets --all-features -- -D warnings` — clean.
- `cargo doc --no-deps --all-features` with `RUSTDOCFLAGS=-D warnings` —
  clean.

## Next

Phase 0.5.0 — Implementation. See `.dev/ROADMAP.md`. The likely next
deliverables are a reference `Sha256Hasher` behind a feature flag, a
file-backed `Sink`, and the first benchmark suite.

[`Verifier`]: https://docs.rs/audit-trail/0.3.0/audit_trail/struct.Verifier.html
[`RecordId`]: https://docs.rs/audit-trail/0.3.0/audit_trail/struct.RecordId.html
