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 splits work between client and server:

All parsing happens on your machine. The CLI and VS Code extension use the core library to parse source code and build a semantic graph. Only the graph structure (not your source code) goes to the API.

This architecture provides:

  • Privacy: Your code never leaves your machine
  • Speed: Parsing is parallel and local
  • Consistency: Same parsing 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`
│ │ ├── ask.rs # `unfault ask`
│ │ ├── graph.rs # `unfault graph`
│ │ └── login.rs # `unfault login`
│ ├── session/ # Analysis session management
│ │ ├── mod.rs # Session lifecycle
│ │ ├── workspace.rs # Workspace detection
│ │ └── file_collector.rs # File selection
│ └── api/ # API client
│ ├── client.rs # HTTP client
│ └── session.rs # Session API

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
  5. API Call: Send IR to API, receive findings
  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 API communication; the extension focuses on UI.

We considered server-side parsing but chose client-side for:

  1. Privacy: Many organizations can’t send source code to external servers
  2. Latency: Local parsing is faster than uploading and waiting
  3. Offline potential: Core analysis could work without network access
  4. Consistency: Same parser in CLI and extension, same results

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

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

The API receives an Intermediate Representation (IR) that contains:

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

The API does not receive:

  • 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

For end-to-end testing:

Terminal window
# Point CLI to local API
UNFAULT_API_URL=http://localhost:8000 cargo run -- review

How to Contribute

Contribution workflow. Read more