Skip to content

Architecture

This document explains how Unfault’s client-side components work. If you’re contributing to the CLI, core library, or VS Code extension, this context helps you understand where your changes fit.

Unfault runs entirely on your machine:

Everything runs on your machine. The CLI and VS Code extension use the core library to parse source code and build a semantic graph. The analysis crate then runs rules against that graph in-process. No source code, IR, or findings leave your machine.

This architecture provides:

  • Privacy: Your code never leaves your machine
  • Speed: Parsing and analysis are parallel and local
  • Offline support: Works without network access
  • Consistency: Same analysis logic in CLI and extension

The core library is the foundation of client-side analysis. It handles:

  1. Parsing: Tree-sitter grammars for Python, Go, Rust, TypeScript
  2. Semantic Extraction: Functions, classes, imports, calls
  3. Framework Analysis: FastAPI routes, Express middleware, Gin handlers
  4. Graph Construction: Nodes (files, functions) and edges (calls, imports)
  5. IR Generation: Serialization for the API
core/
├── src/
│ ├── lib.rs # Public API
│ ├── parse/ # Tree-sitter parsing
│ │ ├── python.rs
│ │ ├── go.rs
│ │ ├── rust.rs
│ │ └── typescript.rs
│ ├── semantics/ # Semantic extraction
│ │ ├── mod.rs
│ │ ├── python/ # Python-specific semantics
│ │ ├── go/
│ │ └── ...
│ ├── graph/ # Graph construction
│ │ ├── mod.rs # CodeGraph implementation
│ │ ├── nodes.rs # Node types
│ │ └── edges.rs # Edge types
│ └── ir.rs # Intermediate representation

The CodeGraph is the central data structure:

pub struct CodeGraph {
nodes: Vec<GraphNode>,
edges: Vec<GraphEdge>,
// Indexes for efficient lookups
file_index: HashMap<PathBuf, NodeIndex>,
function_index: HashMap<String, NodeIndex>,
}
pub enum GraphNode {
File { path: PathBuf, language: Language },
Function { name: String, qualified_name: String, ... },
Class { name: String, ... },
ExternalModule { name: String, category: ModuleCategory },
// Framework-specific nodes
Route { method: HttpMethod, path: String, ... },
Middleware { name: String, ... },
}
pub enum GraphEdgeKind {
Contains, // File contains Function
Calls, // Function calls Function
Imports, // File imports Module
Inherits, // Class inherits Class
UsesLibrary, // Function uses external library
// Framework-specific edges
RegistersRoute,
AppliesMiddleware,
}

The CLI orchestrates the analysis workflow:

cli/
├── src/
│ ├── main.rs # Entry point, argument parsing
│ ├── commands/ # Subcommands
│ │ ├── review.rs # `unfault review`
│ │ ├── lint.rs # `unfault lint`
│ │ ├── graph.rs # `unfault graph`
│ │ ├── info.rs # `unfault info`
│ │ ├── config.rs # `unfault config`
│ │ ├── lsp.rs # `unfault lsp`
│ │ └── agent_skills.rs # `unfault config agent`
│ ├── session/ # Analysis session management
│ │ ├── mod.rs # Session lifecycle
│ │ └── workspace.rs # Workspace detection
│ └── integration/ # Observability integrations
│ └── (gcp, datadog, dynatrace)

The review flow:

  1. Workspace Detection: Scan for languages, frameworks, config files
  2. File Collection: Select files based on hints and exclusions
  3. Parsing: Use core to parse files and build graph
  4. IR Generation: Serialize graph to JSON (intermediate representation)
  5. Local Analysis: Pass IR to unfault-analysis which runs rules in-process
  6. Output: Format and display findings

The extension provides real-time analysis via LSP:

vscode/
├── src/
│ ├── extension.ts # Extension entry point
│ ├── contextView.ts # Context sidebar webview
│ └── welcomePanel.ts # Onboarding UI

The extension spawns the CLI in LSP mode (unfault lsp) and communicates via the Language Server Protocol. The CLI handles all parsing and analysis; the extension focuses on UI.

Analysis runs entirely on your machine:

  1. Privacy: Source code never leaves your system
  2. Speed: No round-trips, parsing runs in parallel on your hardware
  3. Offline support: Works without network access
  4. Consistency: Same analysis in CLI and editor extension

The core library is a separate crate from the CLI. This is good practice:

  • Clear separation of concerns (parsing vs. orchestration vs. analysis)
  • Easier testing (test parsing logic independently)
  • Reusable foundation for future clients

The analysis crate lives separately from core and cli:

  • Rules are testable in isolation
  • New rules can be added without touching parsing logic
  • Profiles (rule sets) are decoupled from both parsing and output

The Intermediate Representation (IR) passed from core to analysis contains:

  • File paths and languages
  • Function names and signatures
  • Import relationships
  • Call relationships
  • Framework topology (routes, middleware)

The IR does not contain:

  • Source code
  • String literals
  • Comments
  • Variable values

To add support for a new language:

  1. Add Tree-sitter grammar to core/Cargo.toml
  2. Create parser in core/src/parse/{language}.rs
  3. Create semantic extractor in core/src/semantics/{language}/
  4. Add to SourceSemantics enum
  5. Update CLI language detection
  6. Add tests with sample code

To add support for a new framework:

  1. Add detection logic in core/src/semantics/{language}/frameworks/
  2. Add framework-specific nodes/edges if needed
  3. Update FrameworkGuess signals
  4. Add tests with sample framework code

To capture more semantic information:

  1. Extend the model in core/src/semantics/{language}/model.rs
  2. Update extraction logic in core/src/semantics/{language}/mod.rs
  3. Add tests covering the new extraction

The CLI can discover SLOs from observability platforms and link them to route handlers. Currently supported: GCP Cloud Monitoring, Datadog, Dynatrace.

To add a new provider:

  1. Create provider module in cli/src/slo/{provider}.rs
  2. Implement credential detection (environment variables, config files)
  3. Implement fetch_slos to query the provider’s API
  4. Return SloDefinition structs with name, target, path pattern
  5. Register in SloEnricher (cli/src/slo/mod.rs)
  6. Add environment variables to documentation

Example provider structure:

pub struct MyProvider {
api_key: String,
endpoint: String,
}
impl MyProvider {
pub fn is_available() -> bool {
env::var("MY_PROVIDER_API_KEY").is_ok()
}
pub fn from_env() -> Option<Self> {
let api_key = env::var("MY_PROVIDER_API_KEY").ok()?;
Some(Self { api_key, endpoint: "https://api.myprovider.com".into() })
}
pub async fn fetch_slos(&self, client: &Client) -> Result<Vec<SloDefinition>> {
// Query API and map to SloDefinition
}
}
Terminal window
cd core
cargo test # All tests
cargo test python # Python-related tests
cargo test semantics::python # Specific module
Terminal window
cd cli
cargo test
Terminal window
# Run the full workspace tests
cargo test --workspace

How to Contribute

Contribution workflow. Read more