Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

ADR-0002: CLI Product vs Library

Status: Accepted
Date: 2026-04
Context: Rivet ships as a single crate (rivet-cli on crates.io) that produces both a library target (rivet) and a binary target (rivet). The default Rust project layout creates accidental public API surface — any module marked pub in lib.rs is reachable by external consumers. This ADR decides intentional product boundaries.


Decision

Rivet is a CLI-first product. The library crate (rivet) is not a stable public API.

Rivet’s primary deliverable is the rivet binary: end users invoke it from the command line to export data from PostgreSQL/MySQL databases to Parquet/CSV files. No embedding contract, no programmatic API stability guarantee, no semver guarantee on internal types.

The library target exists solely to enable Rust’s integration test harness (tests/*.rs must link against a library crate). It is an implementation artifact, not a product surface.


Rationale

Why CLI-first, not library

  1. Use case fit: The tool solves a concrete operational task (export data). Embedding it in other Rust programs is not a stated use case and adds maintenance overhead (API stability, semver discipline, docs).
  2. Crate name signals intent: The crate is published as rivet-cli, not rivet. The -cli suffix is the standard Rust convention for CLI tools that are not intended as embeddable libraries.
  3. Binary is the integration point: All known consumers use the binary — via shell scripts, Docker images, CI pipelines. No known Rust consumer imports the library crate.
  4. Internal types are not API-stable: ResolvedRunPlan, ExtractionStrategy, StateStore, SourceTuning and similar types evolve to serve the pipeline’s execution model. Treating them as public API would force design compromises on internal evolution.

Why the library crate still exists

Rust’s integration tests (tests/ directory) must link against a library target. There is no way to run integration tests against a binary-only crate. The library crate is the Rust mechanism that grants tests/*.rs access to internal implementations.


Module Visibility Rules

Reflects src/lib.rs as of v0.8.0. pub modules are reachable cross-crate only so tests/*.rs (and the in-crate MCP surface) can link them — none carry a stability guarantee (see Consequences #1). pub here means “the test harness needs it”, not “public API”.

Modulelib.rs visibilityReason
configpubIntegration tests import config types (Config, ExportMode, …)
errorpubResult alias surfaced for the test harness
formatpubIntegration tests validate format output (CsvFormat, ParquetFormat, …)
journalpubRunJournal event log — trust-contract type asserted in tests
manifestpubRunManifest wire schema (ADR-0012) — asserted in trust-artifact tests
pipelinepubIntegration tests call pipeline functions (generate_chunks, classify_error, …)
preflightpubIntegration tests exercise diagnostics / type-report
resourcepubIntegration tests verify memory utilities (get_rss_mb, check_memory, …)
sourcepubLive integration tests construct ExportRequest / introspection directly
statepubIntegration tests verify state invariants (StateStore, SchemaColumn)
tuningpubGovernor / adaptive tuning tests link it (ADR-0019)
typespubType-roundtrip tests assert RivetType / fidelity mappings (ADR-0014)
mcppubRivet’s read-only DB-introspection MCP server (run_stdio) — public so the rivet-mcp binary in src/bin/rivet-mcp.rs can link it
clipubThe rivet binary’s entry point (run_binary) — public so src/main.rs can link it, like mcp
redactpubCross-cutting credential-redaction helper, asserted in tests
destination_for_testspubThin test-only shim over the pub(crate) destination module
destinationpub(crate)Internal write backends — exercised via destination_for_tests
enrichpub(crate)Internal pipeline module
notifypub(crate)Internal notification module
planpub(crate)Internal execution contract — consumed by pipeline, not by tests
qualitypub(crate)Internal quality gate
sqlpub(crate)SQL identifier quoting (quote_ident) — internal utility, not a product surface
test_hookpub(crate)Internal fault-injection points for tests

Consequences

  • No stability guarantee: Consumers who depend on internal modules (any non-pub module above, or sub-items of pub modules not explicitly documented) accept breakage at any patch release.
  • Docs reflect intent: cargo doc will not generate docs for pub(crate) modules, reducing confusion about the intended API surface.
  • Binary compilation path: src/main.rs declares all modules privately via mod — it never uses the library crate. The two targets are independent compilation units that happen to share source files.
    • Amended 2026-09-27: src/main.rs now calls rivet::cli::run_binary() and declares no modules. Two compilation units compiled every module twice and ran each unit test twice (3,196 lib + 3,379 bin tests from the same sources). Every CI job and every mutation build paid that twice. The CLI-first decision above is unchanged: cli is pub only so the binary links it, as mcp is for rivet-mcp.
  • Future library path: If Rivet ever offers a stable embedding API, a separate rivet-engine crate should be extracted with its own semver-tracked surface, rather than promoting internal types to pub.
    • Amended by ADR-0026: a minimal first-party extension seam (the types/types::target resolution items) is now stability-tracked in-crate for the private rivet-pro companion. The full rivet-engine extraction is deferred until a non-first-party external consumer appears.

Alternatives Considered

Make everything pub(crate), move tests inline

Moving tests/*.rs into the library as #[cfg(test)] mod tests would allow all modules to be pub(crate). This was rejected because:

  • Integration tests (especially chunk/state invariants) benefit from the clean external-crate perspective
  • tests/ layout is idiomatic and easier to locate

Extract a rivet-engine crate now

Premature. No known consumers exist. The extraction cost (separate crate, two Cargo.toml files, re-exports) is not justified until there is a concrete embedding use case.