Architecture
Trurlic is a single Rust crate. Visibility enforces the boundaries — everything is pub(crate) except cli and store. There is no LLM code anywhere in the binary: all reasoning runs on the coding agent’s own model, over MCP.
Module layout
Section titled “Module layout” cli │ parses args, dispatches ▼ commands ───────────────┐ │ │ │ │ ┌──────────┘ │ └──────────┐ │ ▼ ▼ ▼ ▼ mcp map workflow │ │ │ │ │ └──────┬───────┴──────────────┴─────┘ ▼ store (foundation — no internal deps)| Module | Depends on | Role |
|---|---|---|
store | — | The decision graph: TOML files, edge index, validation, atomic writes, file locking. Never imports another module. |
workflow | store | Step deduction, concern tracking, prompt generation. Pure functions — no I/O, no side effects. |
mcp | store, workflow | The MCP server: JSON-RPC over stdio, tool dispatch, context assembly, file watcher. |
map | store | Interactive graph visualization: WebSocket live sync, REST API, frontend embedded at compile time. |
commands | store, workflow, mcp, map | CLI command handlers, including install for IDE MCP config and serve. |
cli | commands | Argument parsing (clap) and dispatch — the entry point. |
Boundary rules:
- store is the foundation. Every write goes through a
Storemethod that requires aStoreLockproof parameter. - workflow is pure computation.
advance()is a deterministic function of graph state plus inputs — same inputs, same output. - mcp never writes the graph directly; it calls
Storewrite methods. Prompts come fromworkflow::steps. - map depends only on
storeand embeds its frontend viarust-embed.
Write path
Section titled “Write path”Every mutation — from the CLI, MCP, or map — takes the same atomic path:
Store::write_*(&StoreLock) ├─ validate the full graph (fail-closed — refuse invalid states) ├─ serialize to TOML ├─ write to a temp file ├─ verify round-trip parse ├─ rename node file into place └─ rename graph.toml last ← the commit pointIf the process crashes mid-write, the incomplete write is orphaned and reconciled on the next trurlic check. Because graph.toml is renamed last, the graph is never observed half-updated. See Integrity Model for the full guarantees.
Read path (MCP)
Section titled “Read path (MCP)”agent calls a tool → mcp::dispatch ├─ read tool: acquire RwLock read lock → query in-memory graph └─ write tool: acquire RwLock write lock → file lock → validate → commitRead tools hold the lock for microseconds — they query in-memory data structures. Write tools take an exclusive lock, then a file lock, before validating and committing.
Thread model
Section titled “Thread model”The MCP server holds Arc<RwLock<ProjectState>>. Three actors share it:
- Tool calls — read tools take the read lock (concurrent); write tools take the write lock (exclusive), then the file lock.
- File watcher — a
notify-based watcher detects external changes (CLI commands,git checkout, manual edits) and reloads state under the write lock. The swap takes microseconds. - Map server — takes the read lock for API responses and WebSocket diffs.
The file lock (fs2 flock) serializes mutations across the CLI, MCP, and map processes, so they can run against the same .trurlic/ safely.
Dependencies
Section titled “Dependencies”Every dependency is justified; no proc macros run at runtime (serde derive and thiserror are compile-time only).
| Dependency | Purpose |
|---|---|
clap | CLI argument parsing |
serde + toml + serde_json + serde_yaml_ng | Serialization: TOML graph files, JSON MCP protocol, JSON/YAML IDE configs |
thiserror | Error types (compile-time derive) |
chrono | UTC timestamps (RFC 3339) |
blake3 | Content hashing — pure Rust, no C/OpenSSL |
rayon | Parallel node-file reads in load_state |
fs2 | Cross-platform file locking (flock) |
notify | Filesystem watcher for live reload |
tokio + axum + tower-http | Async runtime and HTTP/WebSocket server for trurlic map |
rust-embed | Map frontend compiled into the binary |
opener | Cross-platform browser launch for trurlic map |
rand | Map authentication token generation |
Invariants
Section titled “Invariants”unsafe_code = "deny"at the crate level;unwrap()/expect()denied outside#[cfg(test)].- Every graph mutation validates the full graph before touching disk — invalid writes are refused, never silently committed.
- Atomic writes: serialize → temp → round-trip verify → rename, with
graph.tomlrenamed last. workflow::advanceis a pure function — no I/O, no side effects.
For the module-by-module description straight from source, see the trurlic repo’s CLAUDE.md.