Vapora/adrs/adr-002-cargo-workspace.ncl

87 lines
4.9 KiB
Text
Raw Normal View History

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,
},
}