Integrity Model
Design philosophy
Section titled “Design philosophy”Trurlic treats the decision graph as a critical data structure. If the graph is corrupt, every agent consuming it gets wrong constraints. The integrity model ensures the graph is always consistent, writes are never partial, and tampering is detectable.
Key invariants
Section titled “Key invariants”These define the architecture. If any invariant is violated, the system is broken.
I-1 Fail-closed on writes. Every graph mutation validates the full graph before touching disk. Invalid writes are refused with a clear error, never silently committed. There is no code path that writes an invalid graph.
I-2 Atomic writes. The write protocol is: serialize → write to temp file → verify round-trip parse → rename into place. graph.toml is renamed last as the commit point. A crash at any point between steps produces a recoverable state.
I-3 Content integrity. Every node file has a BLAKE3 hash stored in graph.toml. trurlic check verifies all hashes. This is tamper detection, not encryption — if someone modifies a decision file by hand, the next check catches the mismatch and reconciles.
I-4 File locking. fs2::FileExt provides cross-platform flock. StoreLock is a proof-of-lock type — write methods require &StoreLock as a parameter, making it a compile-time error to attempt a write without holding the lock. The lock is never held across LLM calls.
I-5 No unsafe code. unsafe_code = "deny" is enforced in Cargo.toml. There are zero unsafe blocks in the codebase.
I-6 No panics in production. unwrap() and expect() are denied outside #[cfg(test)] via #![cfg_attr(not(test), deny(clippy::unwrap_used, clippy::expect_used))]. Every error path returns a Result.
I-7 Pure workflow. workflow::advance is a pure function. No I/O, no LLM calls, no side effects. Same graph state plus same inputs produces the same output, always. This makes the workflow engine fully testable and deterministic.
I-8 Boundary type discipline. Boundary types (Decision, Pattern, Component, GraphIndex) derive Serialize + Deserialize. Internal types (InMemoryGraph, Engine) do not. This prevents accidental serialization of internal state.
I-9 Secret hygiene. API keys are wrapped in Zeroizing<String> (from the zeroize crate). They are zeroed from memory on drop. Display and Debug implementations show only a redacted form (last 4 characters). Keys never appear in logs or error messages.
Atomic write protocol
Section titled “Atomic write protocol”Every mutation follows this exact sequence:
1. Acquire StoreLock (file lock)2. Validate the proposed mutation against the full graph3. If invalid → return error, release lock, no disk change4. Serialize new node to TOML5. Write serialized content to a temp file (same directory)6. Read temp file back and parse — verify round-trip fidelity7. If round-trip fails → delete temp, return error8. Rename temp file to final node path (atomic on POSIX)9. Rebuild graph.toml with updated edges and hashes10. Serialize graph.toml to temp file11. Verify graph.toml round-trip12. Rename graph.toml temp to graph.toml (commit point)13. Release StoreLockStep 12 is the commit point. If the process crashes before step 12, graph.toml still reflects the previous consistent state. Orphaned temp files are cleaned up on next startup.
Graph validation
Section titled “Graph validation”The full graph validation (trurlic check) enforces:
- Every decision references an existing component (or
project) - Every edge references existing nodes on both ends (no dangling edges)
- No cycles in
DependsOnedges - BLAKE3 hashes match file contents
- Superseded decisions point to existing decisions
- Pattern decisions all exist and total at least two
- Component names are valid kebab-case
- Decision filenames match their content slugs
- No duplicate names within a node type
Validation runs before every write and as a standalone health check. The --rebuild flag reconstructs graph.toml from node files as a nuclear recovery option — non-inferable edges (ConnectsTo, DependsOn, etc.) will be lost.
BLAKE3 hashing
Section titled “BLAKE3 hashing”Node files are content-hashed with BLAKE3 (pure Rust, no C dependency). Hashes are stored in the [hashes] section of graph.toml:
[hashes]"components/auth" = "b3:a1b2c3d4...""decisions/auth-jwt-with-dpop-binding" = "b3:e5f6a7b8..."Hashes serve two purposes: detecting external modifications (hand edits, git merges) and verifying index consistency. They are not a security mechanism — they are an integrity signal.
Fail-closed behavior
Section titled “Fail-closed behavior”| Failure | Result |
|---|---|
| Dangling edge in proposed write | Write refused, error returned |
Cycle detected in DependsOn graph | Write refused, error returned |
| Round-trip parse mismatch | Write aborted, temp file deleted |
| File lock unavailable | Write blocks until lock acquired |
| BLAKE3 hash mismatch on check | Warning with file path and expected vs actual hash |
| graph.toml missing or corrupt | Rebuild from node files via trurlic check --rebuild |
| Concurrent CLI + MCP write | Serialized via file lock — second writer waits |
There is no code path that commits an invalid graph. Every validation failure is surfaced as an explicit error with enough context to diagnose.
Operational safety
Section titled “Operational safety”Live reload. The MCP server and map watch .trurlic/ for external changes via notify. When files change (CLI command, git checkout, manual edit), state is reloaded under the write lock. The swap takes microseconds — read tools experience no interruption.
Crash recovery. On startup, Trurlic scans for orphaned temp files from interrupted writes and removes them. graph.toml is either at its last consistent state (rename didn’t happen) or at the new state (rename completed). There is no intermediate state.
Manual editing. TOML node files are human-readable and hand-editable. After editing, run trurlic check to reconcile the graph index. The --rebuild flag regenerates graph.toml entirely from node files.
For the module boundaries and thread model, see Architecture. For the TOML schema, see Decision Graph.