Skip to content

validate/list (CLI hot path) force a cold build of the entire serve+MCP+LSP stack — no minimal-build feature #456

Closed

Description

@avrabe

Finding (dogfooding friction)

rivet validate is the hook-critical and CI-critical hot path: the project's own pre-commit hook runs it on every commit, and CI runs it as a required gate. Yet the CLI has no way to build a minimal validate/list/commit-msg-check binary — every build unconditionally compiles the full web-server + MCP-server + LSP stack.

Evidence (rivet-cli/Cargo.toml)

[features]
default = []
wasm = ["rivet-core/wasm"]   # the only non-trivial feature

[dependencies]
# ...
axum = { workspace = true }            # serve
tokio = { workspace = true }           # async runtime
tower-http = { workspace = true }      # serve
lsp-server = "0.7"                     # LSP
lsp-types = "0.97"                     # LSP
notify = "7"                           # serve --watch / lsp
rmcp = { version = "1.3.0", features = ["server", "transport-io", "macros", "transport-streamable-http-server"] }  # MCP

There is no serve/mcp/lsp feature — default = [] and wasm are the only features. So axum, tower-http, rmcp (full streamable-HTTP MCP server), lsp-server/lsp-types, and notify are compiled into every build, including one whose only job is to validate a one-line artifacts/*.yaml edit for the commit hook.

rivet-core already does this correctly — wasm, reqwest, and the spar-* analysis deps are all optional = true. The coupling is specific to rivet-cli.

Impact / friction

  • Every from-source build carries the whole web/MCP/LSP tree: cargo install rivet, CI's compile step, and a local cargo build to test a one-line validate change all compile axum/tokio/rmcp/lsp even though validate/list never use them. On a disk-constrained machine, a cold rebuild of that tree is multi-GiB.
  • Correction (verified): the project's pre-commit hook does NOT rebuild — it invokes the already-installed rivet on PATH via RIVET_BIN:-rivet. So the friction is on from-source builds, not the hook itself. (Original report overstated this; corrected for accuracy.)
  • Downstream packagers who want only the traceability core must still pull axum/tokio/rmcp.

Suggested fix (additive — preserves published behavior)

Introduce cargo features and keep them in default, so cargo install rivet / the released binary are byte-for-byte unchanged:

[features]
default = ["serve", "mcp", "lsp"]
serve = ["dep:axum", "dep:tower-http", "dep:notify"]
mcp   = ["dep:rmcp", "dep:tokio"]
lsp   = ["dep:lsp-server", "dep:lsp-types"]
wasm  = ["rivet-core/wasm"]

…with #[cfg(feature = "serve")] / "mcp" / "lsp" gating the corresponding command handlers and module declarations in main.rs. Then:

cargo build -p rivet-cli --no-default-features   # fast: parser + validate + list + commit checks

becomes available for hooks, CI's validate gate, and the dogfooding loop, while the default published binary is unchanged. No distribution-semantics change; purely an opt-in minimal build.

Caveat / scope

The code-side gating (#[cfg] across the serve/mcp/lsp command dispatch in main.rs) is the bulk of the work and needs care so --no-default-features still compiles cleanly. Filing as a friction report + design proposal; the default-feature set is a maintainer call. Reported from the hourly dogfooding loop (verified against rivet-cli/Cargo.toml at dedc8c1).

Activity

  1. avrabe commented on Jun 4, 2026

    @avrabe
    ContributorAuthor

    Triage pass — premise confirmed against rivet-cli/Cargo.toml on current main: default = [] and wasm are the only features; axum, tower-http, rmcp, lsp-server/lsp-types, and notify are unconditional dependencies, with no serve / mcp / lsp features carving them out. The "Suggested fix" sketch (introduce features, keep them in default, gate handlers with #[cfg]) is well-shaped, but it's a sketch and several decisions are still open (default-feature set, exactly which modules get gated, how serve --watch interacts with notify if notify becomes lsp-only, what the published binary's identity becomes). The "one PR per issue, keep PRs focused" rule means I need those decisions encoded before picking this up.

    Could you add an explicit ## Acceptance Criteria block to the issue body? Sketch — feel free to revise / re-scope:

    ## Acceptance Criteria
    - [ ] `rivet-cli/Cargo.toml` gains features `serve`, `mcp`, `lsp` (and keeps `wasm`); each is `default`-enabled so `cargo install rivet` and the released binary are **byte-for-byte unchanged** (verified by comparing `cargo build --release` artifact size / `--help` output before vs after on the same commit)
    - [ ] The corresponding deps move under `[dependencies]` with `optional = true` and the feature definitions use `dep:axum` / `dep:tower-http` / `dep:notify` (for `serve`), `dep:rmcp` (for `mcp`), `dep:lsp-server` / `dep:lsp-types` (for `lsp`); `tokio` stays unconditional (used by validate's async I/O path) — or, if gated, document why
    - [ ] `main.rs` command dispatch + module declarations for `serve`, `mcp serve`, `lsp` are `#[cfg(feature = "serve" / "mcp" / "lsp")]`-gated and route to a clear "this build was compiled without --features X" error when invoked on a stripped binary
    - [ ] **`cargo build -p rivet-cli --no-default-features` compiles cleanly** (this is the load-bearing test for the feature gating); a CI matrix job or a one-liner in CI exercises it so it doesn't bit-rot
    - [ ] `cargo test -p rivet-cli` green on default features; `cargo test -p rivet-cli --no-default-features` green for the validate/list/commit-msg-check subset
    - [ ] `clippy --all-targets -- -D warnings` clean on both feature configurations; `cargo fmt --check` clean
    - [ ] `rivet validate` on the rivet repo still PASS
    - [ ] `docs/` (or AGENTS.md) gains a one-paragraph note on `--no-default-features` as the recipe for hook / CI / packaging minimal builds
    - [ ] Commit trailer: `Implements: REQ-NNN` (file a new REQ for the build-surface ergonomics, e.g. "minimal validate-only build") + `Refs: REQ-029` if you want it tied to the incremental/build hot-path REQ
    - [ ] **Explicitly out of scope (file separately if wanted):** flipping `default` to drop `serve`/`mcp`/`lsp` from the published binary (distribution-semantics change); per-feature subcommands that need cross-feature plumbing (e.g. `serve --watch` if `notify` lands under `serve` only — verify it does); packaging-side `cargo install rivet --no-default-features` UX documentation downstream of this PR

    Borderline calls worth your input even before AC is final:

    • Where does notify go? It's used by both serve --watch and the LSP file-watch path. Two options: (a) put it under serve and have lsp depend transitively (cleanest), or (b) extract a small watch feature both serve and lsp enable. (a) is the simpler default unless LSP-without-serve is a real use case.
    • tokio — gate or leave? You correctly noted in the body that the only feature is wasm. tokio is currently used for the async I/O paths the serve/MCP/LSP commands need, but validate/list don't need it. If gating tokio under any-of(serve,mcp,lsp) is feasible without breaking validate/list, that's the bigger size win; if validate uses any async (e.g. for parallel walk), leave it unconditional and document.
    • rivet-core features? The body notes rivet-core already gates wasm, reqwest, and the spar-* deps. Are any of those also worth --no-default-features-ing on the rivet-cli side, or is the friction strictly CLI-local?

    Process side-rail (carried from prior triage threads on this repo): https://pulseengine.eu/blog/ is still HTTP 503 — same expired-TLS symptom flagged on #420. The binding workflow guidance can't be consulted, so no PR will land from this triage run regardless. Comment is the AC ask only.

    Not implemented this run.


    Generated by Claude Code — issue-triage agent run 2026-06-04.


    Generated by Claude Code

  2. avrabe commented on Jun 5, 2026

    @avrabe
    ContributorAuthor

    Triaged from the hourly dogfooding loop — valid, verified against rivet-cli/Cargo.toml. [features] is default = []; axum/tower-http/notify (serve), lsp-server/lsp-types (LSP), and rmcp (full streamable-HTTP MCP server) are all unconditional dependencies, so every from-source build compiles the entire web/MCP/LSP tree even for the validate/list/commit-msg-check hot path. rivet-core already gates its heavy deps (wasm/reqwest/spar-* are optional = true) — the coupling is rivet-cli-specific, as you noted. Your self-correction (the hook invokes the installed rivet on PATH and does not rebuild) is also accurate.

    Filed as REQ-202 (draft) in artifacts/requirements.yaml (PR #475), capturing your additive design: introduce serve/mcp/lsp cargo features and keep them in default so the published binary is byte-for-byte unchanged, enabling cargo build -p rivet-cli --no-default-features for hooks/CI/dogfooding. Filed draft rather than implemented because (a) the #[cfg] gating across the serve/mcp/lsp command dispatch in main.rs is the bulk of the work and needs care so --no-default-features still compiles cleanly, and (b) the default-feature set is — as you say — a maintainer call. Shell-testable acceptance criteria are in the REQ (features declared + in default; --no-default-features builds and runs validate/list/commit-msg-check; cargo tree shows axum/rmcp/lsp-server absent; default build unchanged).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions