Architecture
Module layout
Section titled “Module layout”Trurlic is a single crate with eight modules. Visibility enforces boundaries — pub(crate) on everything except cli and store.
┌──────────────────────────────────────────────────────────────────┐│ CLI / COMMANDS ││ commands → store, session, config ││ CLI command handlers — the entry point for all user actions │└──────────────┬───────────────────────┬───────────────────────────┘ │ │ ▼ ▼┌─────────────────────────┐ ┌─────────────────────────────────────┐│ SESSION │ │ MCP SERVER ││ session → store, │ │ mcp → store, workflow ││ workflow, provider │ │ ││ │ │ JSON-RPC stdio, tool dispatch, ││ CLI design sessions, │ │ context assembly, file watcher ││ bootstrap driver, │ │ ││ LLM extraction │ │ Arc<RwLock<ProjectState>> │└──────────┬───────────────┘ └──────────┬────────────────┬────────┘ │ │ │ ▼ ▼ │┌─────────────────────────┐ ┌─────────────────────────┐ ││ PROVIDER │ │ WORKFLOW │ ││ provider → (leaf) │ │ workflow → store │ ││ │ │ │ ││ Anthropic, OpenAI, │ │ Step deduction, │ ││ OpenRouter, Gemini, │ │ concern tracking, │ ││ Custom, Ollama │ │ prompt generation │ ││ clients + SSE streaming │ │ Pure functions, no I/O │ │└──────────────────────────┘ │ │ │ └──────────┬───────────────┘ │┌─────────────────────────┐ │ ││ CONFIG │ ▼ ▼│ config → (leaf) │ ┌────────────────────────────────────┐│ │ │ STORE ││ Provider resolution, │ │ store → (no internal deps) ││ API key handling │ │ │└──────────────────────────┘ │ Decision graph: TOML files, │ │ graph index, validation, │┌─────────────────────────┐ │ atomic writes, file locking ││ MAP │ │ ││ map → store │ │ The foundation. Never imports ││ │ │ from any other module. ││ Interactive graph viz, │ └────────────────────────────────────┘│ WebSocket live sync, ││ REST API, embedded HTML │└──────────────────────────┘Provider clients
Section titled “Provider clients”The provider module contains four client implementations:
anthropic.rs— native Anthropic Messages API client.openai.rs— OpenAI-compatible client, also used for OpenRouter (viaopenrouter.ai/api/v1), Custom (user-specified base URL), and Ollama (local, no API key).gemini.rs— native Google Gemini API client.sse.rs— shared SSE streaming parser used by all clients.
Boundary rules
Section titled “Boundary rules”- store is the foundation. It never imports from any other module. Every write goes through
Storemethods withStoreLockproof parameters. - workflow is pure computation. It never calls an LLM, never touches the filesystem, never allocates beyond the response JSON.
advance()is a deterministic function of graph state plus inputs. - mcp never writes to the graph directly. It calls
Storewrite methods. It never runs LLM calls. Prompt generation comes fromworkflow::steps. - session is the only module that calls LLM APIs. It owns the CLI dialogue loop and the bootstrap driver.
- provider and config are leaf modules. They do not import from any other internal module.
- map depends only on
store. It embeds its frontend at compile time viarust-embed.
Data flow
Section titled “Data flow”Write path
Section titled “Write path”CLI / MCP / Map │ ▼ Store::write_*(&StoreLock) │ ├─ Validate full graph ├─ Serialize to TOML ├─ Write to temp file ├─ Verify round-trip parse ├─ Rename into place └─ Rename graph.toml last (commit point)Every write is atomic. If the process crashes between any two steps, the incomplete write is cleaned up on next startup. graph.toml is renamed last — it is the commit point. If it was not renamed, the node file is orphaned and reconciled on trurlic check.
Read path (MCP)
Section titled “Read path (MCP)”Agent calls MCP tool │ ▼ mcp::dispatch │ ├─ Read tools: acquire RwLock read lock │ └─ Query ProjectState (in-memory graph) │ └─ Write tools: acquire RwLock write lock ├─ Acquire file lock (StoreLock) ├─ Validate + commit └─ Release locksRead tools hold the read lock for microseconds — they query in-memory data structures. Write tools acquire an exclusive write lock, then a file lock, validate, and commit. The file lock prevents concurrent mutations from CLI, MCP, and map.
Thread model
Section titled “Thread model”The MCP server holds Arc<RwLock<ProjectState>>. Three concurrent actors access it:
MCP tool calls — the primary consumer. Read tools acquire the read lock (concurrent). Write tools acquire the write lock (exclusive), then the file lock.
File watcher thread — a notify-based filesystem watcher detects external changes (CLI commands, git checkout, manual edits). On change, it reloads state from disk under the write lock. The swap takes microseconds.
Map WebSocket — the map server reads state for API responses and pushes diffs over WebSocket. It acquires the read lock for queries.
The write lock is never held across LLM calls. Session module (which calls LLMs) operates independently — it acquires the file lock only for the atomic write at the end.
Dependencies
Section titled “Dependencies”| Dependency | Purpose | Justification |
|---|---|---|
clap | CLI argument parsing | Standard, derive-based |
serde + toml + serde_json | Serialization | Core data format (TOML files, JSON MCP protocol) |
thiserror | Error types | Compile-time derive, no runtime proc macro |
chrono | Timestamps | Decision created field (UTC, RFC 3339) |
blake3 | Content hashing | Pure Rust, no C/OpenSSL dependency |
rayon | Parallel file I/O | load_state reads node files concurrently |
fs2 | File locking | Cross-platform flock |
notify | File system watcher | Live reload for MCP server and map |
reqwest | HTTP client for LLM APIs | rustls-tls feature — pure Rust TLS, no OpenSSL |
tokio | Async runtime | MCP server, map server, LLM streaming |
axum | HTTP server for map | WebSocket support via ws feature |
tower-http | HTTP middleware | CORS and security headers for map |
rust-embed | Embedded assets | Map frontend compiled into binary |
opener | Browser launch | trurlic map opens browser cross-platform |
rand | Token generation | Map authentication token |
zeroize | Secret erasure | API keys zeroed from memory on drop |
Every dependency is justified. No proc macros at runtime — serde derive and thiserror are compile-time only.
Build profiles
Section titled “Build profiles”| Profile | Settings | Purpose |
|---|---|---|
dev | opt-level = 0, debug = true, deps at opt-level = 2 | Fast compile, debuggable |
release | strip, lto = "fat", codegen-units = 1, overflow-checks = true, panic = "abort" | Minimal binary, maximum performance |
bench | Inherits release, strip = false, debug = 2 | Profiling with symbols |
unsafe_code = "deny" is enforced at the crate level. unwrap() and expect() are denied outside #[cfg(test)].
For the integrity guarantees on every write, see Integrity Model. For the advance loop and workflow engine, see Core Concepts. For the full module-by-module description, see CLAUDE.md.