87 lines
4.9 KiB
Text
87 lines
4.9 KiB
Text
|
|
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,
|
||
|
|
},
|
||
|
|
}
|