ADR-0001 — Crate split & one-way dependency direction (ADR-A)
-
Status: Accepted (ratified, enforced by Story 1.1)
-
Date: 2026-06-17
-
Enforced by:
tests/dep_direction.rs -
Source:
docs/planning-artifacts/architecture.md§"Complete Workspace Tree" (#872—998), §Architectural Boundaries (#994—998);project-context.mdL40—43, L69—74; NFR-10/11.
Context
The Life Suite is one product family (Organizer now, Nutrition later) over a single shared core. The core (substrate) must stay extractable — each crate may one day move to its own repository — and the WASM frontend must stay light (no native inference/storage crates leaking into the browser build). A monorepo makes cross-crate edits cheap but also makes it trivially easy to draw a dependency arrow the wrong way and quietly couple a "shared" crate to an app. Once that happens, extraction is no longer possible and the coupling is brutal to unwind later.
Decision
Adopt a single Cargo workspace with this member split:
-
crates/substrate— owns all trait boundaries; depends on nothing internal. -
crates/domain-core— shared entities; no IO; depends on nothing internal. -
crates/ui— Leptos component library + design tokens; depends ondomain-core, neversubstrate. -
apps/organizer— a crate pair: the Leptos/WASM frontend and the native Tauri host (apps/organizer/src-tauri). -
services/relay— the zero-knowledge sync relay; build-deferred stub.
Enforce a one-way dependency rule, build-checked:
apps → {ui, domain-core, substrate}; within librariesui → domain-core → substrate; never upward, neverui → substrate.
tests/dep_direction.rs parses cargo metadata, classifies members by their manifest path relative to the workspace root, and treats the internal-edge graph as deny-by-default: only explicitly-legal edges pass, every other internal edge fails the build. The legal shapes are (a) a library→library edge that is a strict one-rank-down step (rank substrate=0, domain-core=1, ui=2); (b) the wasm frontend (apps/<name>/) depending on ui/domain-core but never substrate; (c) the native host (apps/<name>/src-tauri/) or a service depending on any library. The frontend and host halves of the crate pair are classified distinctly (by the src-tauri path segment) so the frontend-can’t-reach-substrate rule is real, not incidental. Dev-dependencies are excluded (a test-only edge does not couple shipped artifacts). Nothing may depend on an app half or on services/relay (a leaf reached over the network, not as a crate dep). The test’s own doc-comment is the human-readable statement of the rule, and it unit-tests its failure path against synthetic graphs — including a frontend→substrate edge — so the guard is proven, not just asserted.
The workspace root [workspace.dependencies] pins all stack versions; member crates opt in per-dependency (workspace = true). Heavy native crates are pinned-but-unused until their stories so the wasm build stays small.
Consequences
-
Positive: extraction stays possible indefinitely; the wasm frontend provably cannot pull
substrate; a wrong-direction edge fails CI on the first commit that introduces it, with a self-explanatory message; versions live in exactly one place. -
Negative / cost: adding a new library means assigning it a rank in the test’s classifier (a deliberate speed-bump). The strict one-rank-down rule forbids level-skipping even when it might be convenient — by design.
-
Neutral: the workspace root is itself a tiny test-host package so the guardrail can live at
tests/dep_direction.rs;--workspaceis therefore explicit in the justfile/CI.