let d = import "adr-defaults.ncl" in d.make_adr { id = "adr-002", title = "Single Cargo Workspace with Specialized Crates", status = 'Accepted, date = "2024-11-01", context = "Vapora is a multi-domain platform spanning REST API, agent orchestration, LLM routing, knowledge graph, WASM frontend, and protocol servers. The initial 13-crate workspace (as of ADR creation) has grown to 17 crates. The cargo workspace monorepo approach centralizes dependency management, enables parallel test execution, and enforces explicit inter-crate boundaries via Cargo.toml dependencies.", decision = "All vapora code lives in a single Cargo workspace. Each architectural layer is a separate crate under crates/. Shared types live in vapora-shared. Workspace-level dependency versions are pinned in the root Cargo.toml [workspace.dependencies] table. No crate may depend on another vapora crate not declared in Cargo.toml.", rationale = [ { claim = "Separate crates enforce architectural boundaries at the compiler level", detail = "Accidental coupling between (e.g.) vapora-frontend and vapora-agents is caught at compile time, not code review. This prevents the boundary erosion that happens in a single-crate monolith.", }, { claim = "Centralized workspace dependency versions prevent version skew", detail = "[workspace.dependencies] in root Cargo.toml is the single source of truth for axum, surrealdb, tokio, rig-core versions. Individual crates inherit versions without pinning, making coordinated upgrades a single-file change.", }, { claim = "vapora-shared as the single shared types boundary prevents circular deps", detail = "All domain models (Project, Task, Agent, etc.) live in vapora-shared. No domain crate depends on another domain crate — only on vapora-shared. This tree structure is enforced by the Cargo dependency graph.", }, ], consequences = { positive = [ "cargo test --workspace runs all 316 tests with full parallelism", "Inter-crate API changes surface as compile errors before runtime", "New crates added without modifying existing crates' Cargo.toml", "cargo build --release builds all crates with LTO across the entire workspace", ], negative = [ "Full workspace builds are slower than single-crate builds (partial mitigation via incremental compilation)", "Adding a new crate requires updating root Cargo.toml workspace.members", ], }, alternatives_considered = [ { option = "Single-crate monolith", why_rejected = "No compiler-enforced boundaries, inevitable coupling between layers, impossible to build only the backend binary without compiling frontend WASM dependencies.", }, { option = "Multi-repository (separate Git repos per crate)", why_rejected = "Cross-crate refactors require multi-repo PRs. Integration testing requires local checkouts. Versioning inter-crate interfaces becomes a published API problem.", }, ], constraints = [ { id = "all-code-in-crates", claim = "All vapora source code must live under crates/ in the workspace", scope = "vapora (root Cargo.toml)", severity = 'Hard, check = { tag = 'FileExists, path = "crates/vapora-shared/src/lib.rs", present = true }, rationale = "Code outside the workspace cannot benefit from shared dependency versions, cross-crate type checking, or unified test runs.", }, { id = "workspace-dep-versions", claim = "All shared dependency versions must be declared in root Cargo.toml [workspace.dependencies]", scope = "vapora (all crates)", severity = 'Hard, check = { tag = 'Grep, pattern = "workspace\\.dependencies", paths = ["Cargo.toml"], must_be_empty = false }, rationale = "Per-crate version pinning leads to version skew and is the root cause of diamond dependency failures.", }, { id = "no-direct-cross-domain-deps", claim = "Domain crates (vapora-backend, vapora-agents, vapora-llm-router) must not depend directly on each other; they share only through vapora-shared", scope = "vapora (all domain crates)", severity = 'Soft, check = { tag = 'NuCmd, cmd = "let r = (do { cargo tree -p vapora-backend } | complete); if $r.exit_code != 0 { exit 1 }; let lines = ($r.stdout | lines | where { |l| ($l | str contains 'vapora-agents') and not ($l | str contains 'vapora-shared') }); if ($lines | is-empty) { exit 0 } else { exit 1 }", expect_exit = 0 }, rationale = "Direct cross-domain deps create coupling that prevents independent crate evolution and break the layered architecture.", }, ], related_adrs = ["adr-001"], ontology_check = { decision_string = "single cargo workspace; all code in crates/; shared deps in root Cargo.toml; vapora-shared as the single shared-types boundary", invariants_at_risk = [], verdict = 'Safe, }, }