fallow-cov-protocol 0.8.0

Versioned JSON envelope types shared between the fallow CLI and Fallow Runtime's production-coverage sidecar.
Documentation
# Contributing to fallow-cov-protocol

This crate is the wire contract between the OSS [`fallow`](https://github.com/fallow-rs/fallow) CLI and the closed-source `fallow-cov` production-coverage sidecar. The whole reason it exists is to keep the two binaries from drifting, so the contribution bar is higher than a typical utility crate: every change to the public surface is a promise.

## Quick start

```bash
cargo build                                             # Library build
cargo test                                              # All unit tests
cargo clippy --all-targets -- -D warnings               # Lints
cargo fmt --all -- --check                              # Formatting
cargo doc --no-deps --document-private-items            # Rustdoc (no broken links)
typos                                                   # Spellcheck
```

CI runs `cargo-audit`, `cargo-deny`, `cargo-shear`, `zizmor`, the MSRV job, and `cargo publish --dry-run` on top of that. Run the supply-chain tools locally before opening a PR if you changed `Cargo.toml`.

### Local pre-push hook

Once per clone, opt into the same gates locally:

```bash
git config core.hooksPath .githooks
```

`.githooks/pre-push` runs `cargo fmt --check`, `cargo clippy --all-targets -- -D warnings`, and `typos .` on every push. `RUN_TESTS=1 git push` also runs `cargo test`. `SKIP_PRE_PUSH=1 git push` bypasses it (use sparingly, e.g. WIP branches for early CI signal).

## Lint baseline

- `clippy::{all, pedantic, nursery, cargo}` at `warn` with a short, documented allow-list in `Cargo.toml`.
- `#![forbid(unsafe_code)]` at the top of `lib.rs`; also declared in `[lints.rust]`.
- MSRV is `1.85`, pinned in `rust-toolchain.toml` and verified by the `msrv` CI job. New language/stdlib features that require a newer toolchain must bump the MSRV in the same PR with a changelog entry.
- Suppress a specific lint with `#[expect(clippy::..., reason = "...")]`, not `#[allow]`, so the suppression fails if the lint becomes unnecessary.

## Semver and the wire contract

`protocol_version` (exposed as `PROTOCOL_VERSION`) is a full semver string. The discipline below is load-bearing:

- **Major bumps** are breaking. Renaming a field's serde name, changing a wire type, removing a field, or changing the canonical `finding_id` / `hot_path_id` input order is a major — even if the Rust signature looks the same.
- **Minor bumps** add optional fields (with `#[serde(default)]`) or enum variants (only when the enum already carries `#[serde(other)] Unknown`).

Every new wire field requires:

- `#[serde(default)]` (or `default = "..."`) so old encoders stay valid.
- `#[serde(skip_serializing_if = "Option::is_none")]` on `Option<T>` when absent-is-observably-different-from-`None`.
- Rustdoc describing the intended semantics, not just the type.
- A matching round-trip + forward-compat test in the same PR (see the patterns under "Testing conventions" below).

The cross-repo release dance for a wire change is: publish this crate, ship a sidecar release that accepts both old and new envelopes, then ship a CLI release that depends on the new version. Never reverse that order, the CLI shipping first breaks users who have not updated their sidecar yet.

## Testing conventions

Tests live in the single `#[cfg(test)] mod tests` block at the bottom of `src/lib.rs`. Required patterns:

- **Forward-compat**: every enum with `#[serde(other)]` has an `unknown_<enum>_round_trips` test.
- **Unknown top-level fields**: the `Response` envelope accepts unknown JSON fields without erroring.
- **Casing**: non-default `rename_all` casings (kebab/snake) are exercised explicitly.
- **Stable IDs**: determinism, distinctness from sibling helpers, sensitivity to every hashed input, and format/length are all asserted.
- **`skip_serializing_if`**: both present-and-serialized and absent-and-omitted cases are tested.

Do not add `insta`, `proptest`, `rstest`, or similar — tests are small and literal on purpose.

## Commit and PR hygiene

- Conventional commits: `feat:`, `fix:`, `chore:`, `refactor:`, `test:`, `docs:`. Breaking wire changes use `feat!:` / `fix!:`. Enforced on push/PR by the `commitlint` workflow against `commitlint.config.mjs` (header <= 100 chars, lower-case type and scope, conventional type set).
- Signed commits (`git commit -S`) and signed pushes. No AI attribution.
- One concern per PR. A wire change + an unrelated refactor go in separate PRs.
- PRs that touch the public surface must update `CHANGELOG.md` under `[Unreleased]`.

## Dependencies

The dependency surface is intentionally minimal: `serde`, `serde_json`, `sha2`. Adding a new dependency requires a justification in the PR description and, if it's transitive-heavy, an update to the clippy allow-list. Every downstream binary that depends on this crate inherits the cost.

Avoid time / path abstraction crates (`chrono`, `camino`, etc.) — the wire is stringly typed on purpose.

## Review

Wire-contract changes (new fields, new enum variants, new ID helpers, `PROTOCOL_VERSION` bumps, serde attribute changes) need a closer read than internal refactors. An unannounced breaking change to the public surface is a hard reject; bump the major and call it out in the PR description and CHANGELOG.