cargo-coverage-gate 0.2.0

A cargo subcommand that gates pull requests on per-package line coverage measured by cargo-llvm-cov
Documentation
# cargo-coverage-gate — Implementation Plan 0000

> Tracks the initial implementation of the design in
> `docs/design/main.md`. Update this document as work lands; keep
> commit hashes against finished items so the trail through the git
> log is recoverable.
>
> Status: **Draft**.

## 1. Scope

Deliver a v1 of `cargo-coverage-gate` that matches the design:

- A single command (no subcommands): `cargo coverage-gate` gates.
  The tool is read-only.
- Reads cargo-llvm-cov lcov tracefiles.
- Three-layer threshold resolution: per-crate → workspace →
  built-in `100.0`.
- Text and Markdown output; auto-detected step-summary file on
  GitHub Actions; explicit `--summary-file` everywhere else.
- Exit codes `0` / `1` / `2` per the design.
- Loud-failure on zero data for a gated crate.

Out of scope for this plan: function/region thresholds, cobertura
input, per-file thresholds, baseline-trend reporting. These are
called out in §11 of the design and stay deferred.

## 2. Crate layout

```
crates/cargo-coverage-gate/
├── Cargo.toml
├── README.md                 # auto-generated by `just readme`
├── docs/
│   ├── design/main.md
│   └── implementation-plans/0000.md
└── src/
    ├── lib.rs                # library surface (used by integration tests)
    ├── bin/
    │   └── cargo-coverage-gate/
    │       ├── main.rs       # cargo subcommand entry point
    │       └── cli.rs        # clap definitions, argument parsing
    ├── error.rs              # crate-local error type
    ├── lcov_cov.rs           # lcov tracefile parser (wraps `lcov` crate)
    ├── workspace.rs          # workspace discovery, member enumeration
    ├── threshold.rs          # three-layer resolution, metadata schema
    ├── attribute.rs          # file-to-crate longest-prefix matching
    ├── aggregate.rs          # per-crate line totals
    ├── verdict.rs            # compare measured vs threshold; exit code
    └── render/
        ├── mod.rs
        ├── text.rs           # stdout table
        └── markdown.rs       # summary-file table
```

Mirrors the `cargo-heather` shape (binary under `src/bin/<name>/`,
library at `src/lib.rs`, integration tests under `tests/`).

## 3. Dependencies

All declared in `[workspace.dependencies]` with `default-features =
false` per workspace policy. Reuse existing entries where possible.

| Dependency        | Use                                                |
|-------------------|----------------------------------------------------|
| `clap`            | CLI parsing (derive)                               |
| `cargo_metadata`  | Workspace + member discovery                       |
| `lcov`            | cargo-llvm-cov lcov tracefile parsing              |
| `ohno`            | Library error type (`#[ohno::error]`)              |
| `serde_json`      | Reading `[package.metadata.coverage-gate]` blocks  |
| `tempfile`        | Integration test scaffolding (dev)                 |
| `assert_cmd`      | CLI integration tests (dev)                        |
| `predicates`      | Output assertions (dev)                            |

The tool is read-only, so `toml_edit` is not needed.

## 4. Phased work

Phases are sized so each lands as one PR. Each phase ends with green
`just check` (build + clippy + tests) on the affected crate.

### Phase 1 — Skeleton and CI plumbing

- Add `crates/cargo-coverage-gate/Cargo.toml` with workspace inherit
  blocks (`edition`, `rust-version`, `authors`, `license`,
  `homepage`, `lints`) matching `cargo-heather`.
- `src/lib.rs` exposing a placeholder `pub fn run(...) -> ...`.
- `src/bin/cargo-coverage-gate/main.rs` with a clap skeleton that
  parses the top-level flags and returns "not implemented". Handle
  the cargo-subcommand convention: when invoked as `cargo
  coverage-gate ...`, cargo passes `coverage-gate` as `argv[1]`; the
  binary must accept and skip that token before parsing flags.
- Add the crate to the root `Cargo.toml` `[workspace.dependencies]`
  block.
- Wire `just readme` to generate `README.md` from `lib.rs` docs.

Exit: crate builds, binary runs `--help`, `just check` passes.

### Phase 2 — lcov model

- Module `lcov_cov.rs` wrapping the `lcov` crate. Flattens
  `lcov::Report` into a `CoverageReport { files: Vec<FileEntry> }`
  where `FileEntry { filename, lines_total, lines_covered }` is
  computed from each `SF:` section's `DA:` records.
- Unit tests with golden fixtures stored in
  `tests/fixtures/lcov/*.info`. Cover: empty file, single source
  section, multi-source section, mixed-coverage, malformed input.

Exit: deserialization round-trips fixtures; warnings exercised in
tests.

### Phase 3 — Workspace discovery + member enumeration

- Module `workspace.rs` wrapping `cargo_metadata` to:
  - Locate the workspace root from CWD.
  - List workspace members using the manifest directories as reported
    by `cargo_metadata` (no further canonicalization).
  - Read each member's `[package.metadata.coverage-gate]` and the
    root's `[workspace.metadata.coverage-gate]`.
- Unit tests with `tempfile` fixtures: workspace with no metadata,
  with workspace-level default, with mixed per-crate overrides.

### Phase 4 — Threshold resolution

- Module `threshold.rs` implementing the three-layer rule:
  per-crate → workspace → `100.0`. Record the resolution source
  (`Crate` / `Workspace` / `Default`) for the verdict table's
  `Source` column.
- Schema: accept `min-lines-percent` as either integer or float TOML value
  via a custom deserializer; range-validate `[0.0, 100.0]`.

### Phase 5 — Attribution + aggregation + verdict

- `attribute.rs`: longest-prefix match of file paths against member
  manifest directories as reported by `cargo_metadata`; drop unmatched
  files with a single aggregated warning.
- `aggregate.rs`: sum `lines.count` and `lines.covered` per crate;
  order-independent (integer summation, no pre-sort required).
- `verdict.rs`: for each gated crate, compare measured vs threshold
  after rounding both sides to one decimal place (matching the
  displayed precision, so the rendered Δ always agrees with the
  verdict — see design §10.5); classify as `Ok` / `Fail` / `NoData`;
  derive process exit code (`0` all-ok, `1` any-fail, `2`
  any-no-data when that crate was in the gated set).

Exit: end-to-end library call `run(lcov_text, workspace) -> Verdict`
working with fixtures; full unit-test coverage.

### Phase 6 — Renderers

- `render/text.rs`: deterministic stdout table matching §5.4 of the
  design (columns, separators, totals line, `Source` column).
- `render/markdown.rs`: GFM table matching §6.5, leading
  `### coverage-gate` header.
- `--summary-file` writes the Markdown output. When unset, the tool
  detects `GITHUB_STEP_SUMMARY` and `COVERAGE_GATE_SUMMARY` in that
  order and writes there.
- `--quiet` suppresses stdout but never the summary file.
- Unit tests assert renderer output via direct string assertions
  (substring checks against expected column values and summary lines).

### Phase 7 — End-to-end CLI tests

- `tests/cli.rs` modelled on `cargo-heather`'s. For each scenario:
  build a temp workspace, drop a fixture lcov tracefile, run the binary via
  `assert_cmd`, assert exit code + table output.
- Scenarios to cover:
  - all-pass with mixed sources (package / workspace / default)
  - one package below threshold (exit 1)
  - one gated package with no data (exit 2)
  - `--package` (`-p`) restricting scope (literal name, glob pattern,
    and repeated form all covered)
  - `--summary-file` written; `GITHUB_STEP_SUMMARY` auto-detected
  - lcov with TN:test_name records parses cleanly
  - Malformed lcov exits 2 with a clear message

### Phase 8 — Documentation and release prep

- Crate-level rustdoc covering: public library surface, minimal
  example, links to `docs/design/main.md`.
- Regenerate `README.md` via `just readme`.
- Add the crate to the workspace `cargo publish` story if there's a
  release script (mirror what `cargo-heather` does — verify before
  copying).
- Update root `README.md` to list the new crate.
- Add any words triggered by the spellchecker to `.spelling`.

## 5. Risks and open decisions

1. **`cargo_metadata` cost.** Calls `cargo` on the user's
   workspace, which is slow on large repos. Acceptable for CI; for
   local repeated runs, consider caching the resolved member list
   keyed on workspace root mtime. Defer to a follow-up if it bites.
2. **Workspace-default merge semantics.** Decision: per-crate `min-lines-percent` fully replaces the workspace value (no field-level
   merge, since the schema has one field). Document if/when more
   fields land.
3. **CI integration in this repo.** Out of scope for the
   implementation plan, but worth opening a follow-up issue once the
   crate is publishable: dogfood the gate on `ox-tools` itself.

## 6. Tracking

Each phase becomes one TODO under `todo/` once work starts, with the
PR linked back here on completion. Leave items here as the canonical
narrative; treat TODOs as the day-to-day work queue.