graphify-dotnet is a .NET 10 port of safishamsi/graphify, implementing an AI-powered knowledge graph builder for codebases. This document describes the system architecture, design decisions, and mappings from the Python source to .NET implementations.
- Composition over inheritance: Use interfaces and dependency injection instead of deep class hierarchies
- Immutable data structures: Records for nodes/edges ensure thread-safety
- .NET idioms:
IOptions<T>,ILogger<T>,async/await,CancellationToken - Type safety: Strong typing with nullable reference types enabled
- Pipeline pattern: Each stage is a discrete
IPipelineStage<TIn, TOut>implementation
FileDetector -> Extractor -> GraphBuilder -> ClusterEngine -> Analyzer -> ReportGenerator -> IGraphExporter[]
| | | | | | |
Detect Extract Build Cluster Analyze Report Export
Files Features Graph (Louvain) Metrics Summary Formats
Each stage is decoupled and testable. Output from one stage feeds into the next via well-defined data models.
graphify-dotnet/
├── src/Graphify/ # Core library
│ ├── Models/ # Data models
│ │ ├── DetectedFile.cs # File detection result
│ │ ├── ExtractedNode.cs # Raw extracted node (string IDs)
│ │ ├── ExtractedEdge.cs # Raw extracted edge (string IDs)
│ │ ├── ExtractionResult.cs # Extraction output container
│ │ ├── GraphNode.cs # Graph node (object references)
│ │ ├── GraphEdge.cs # Graph edge (IEdge<GraphNode>)
│ │ ├── AnalysisResult.cs # Analysis metrics
│ │ ├── GraphReport.cs # Human-readable report
│ │ ├── Confidence.cs # EXTRACTED | INFERRED | AMBIGUOUS
│ │ └── FileType.cs # Code | Document | Paper | Image
│ ├── Pipeline/ # Pipeline stages
│ │ ├── IPipelineStage.cs # Base interface
│ │ ├── FileDetector.cs # Stage 1: Detect files
│ │ ├── Extractor.cs # Stage 2: Extract (hybrid)
│ │ ├── GraphBuilder.cs # Stage 3: Build graph
│ │ ├── ClusterEngine.cs # Stage 4: Community detection
│ │ ├── Analyzer.cs # Stage 5: Metrics & analysis
│ │ ├── ReportGenerator.cs # Stage 6: Generate report
│ │ ├── SemanticExtractor.cs # AI semantic extraction
│ │ ├── ExtractionPrompts.cs # Prompt templates
│ │ └── BenchmarkRunner.cs # Token reduction benchmarks
│ ├── Graph/ # Graph data structures
│ │ └── KnowledgeGraph.cs # QuikGraph wrapper
│ ├── Export/ # Export implementations
│ │ ├── IGraphExporter.cs # Base exporter interface
│ │ ├── JsonExporter.cs # graph.json
│ │ ├── HtmlExporter.cs # graph.html (vis.js)
│ │ ├── SvgExporter.cs # graph.svg
│ │ ├── WikiExporter.cs # wiki/ markdown articles
│ │ ├── ObsidianExporter.cs # obsidian-vault/
│ │ ├── Neo4jExporter.cs # cypher.txt or direct push
│ │ └── LadybugExporter.cs # graph.ladybug.cypher
│ ├── Cache/ # SHA256 caching
│ │ ├── ICacheProvider.cs # Cache interface
│ │ ├── SemanticCache.cs # File hash cache
│ │ └── CacheEntry.cs # Cache entry model
│ ├── Validation/ # Schema validation
│ │ ├── IGraphValidator.cs # Validator interface
│ │ ├── ExtractionValidator.cs # Node/edge validation
│ │ └── ValidationResult.cs # Validation result
│ ├── Security/ # Input sanitization
│ │ ├── ISecurityValidator.cs # Security interface
│ │ └── InputValidator.cs # Path traversal checks
│ └── Ingest/ # URL ingestion
│ ├── IDataIngester.cs # Ingester interface
│ └── UrlIngester.cs # Fetch papers/tweets
├── src/Graphify.Cli/ # Console application
│ ├── Program.cs # CLI entry point
│ └── PipelineRunner.cs # Pipeline orchestration
├── src/Graphify.Sdk/ # GitHub Copilot SDK integration
│ └── CopilotExtractor.cs # Copilot-specific extractors
├── src/Graphify.Mcp/ # MCP stdio server
│ └── McpServer.cs # ModelContextProtocol server
└── src/tests/ # Test projects
├── Graphify.Tests/ # Unit tests
└── Graphify.Integration.Tests/ # Integration tests
ExtractedNode
Id: Unique node identifier (string)Label: Display nameFileType: Code | Document | Paper | ImageSourceFile: Origin file pathSourceLocation: Optional line/columnMetadata: Dictionary<string, string>
ExtractedEdge
Source: Source node ID (string)Target: Target node ID (string)Relation: Relationship type (e.g., "calls", "imports", "semantically_similar_to")Confidence: EXTRACTED | INFERRED | AMBIGUOUSSourceFile: Origin fileSourceLocation: Optional line/columnWeight: Edge weight (default 1.0)
ExtractionResult
Nodes: ListEdges: ListRawText: Original text contentSourceFile: File pathExtractionMethod: Ast | Semantic | HybridTimestamp: Extraction timeConfidenceScores: Dictionary<string, double>
GraphNode
Id: Unique identifier (string)Type: Node typeCommunity: Optional community ID (assigned by clustering)Metadata: IReadOnlyDictionary<string, string>
GraphEdge : IEdge
Source: Source GraphNode referenceTarget: Target GraphNode referenceRelationship: Relation typeConfidence: Confidence enumWeight: Edge weight
KnowledgeGraph
Wraps QuikGraph's BidirectionalGraph<GraphNode, GraphEdge> with domain-specific methods:
AddNode(GraphNode node)AddEdge(GraphEdge edge)GetNodeById(string id)GetNodesByCommunity(int communityId)AssignCommunities(Dictionary<string, int> assignments)MergeGraph(KnowledgeGraph other)
Responsibility: Scan directories and categorize files
Inputs: Root directory path, options (ignore patterns, max file size)
Outputs: List<DetectedFile>
Implementation:
- Recursive directory traversal with configurable ignore patterns
- File type detection based on extension
- Size filtering (skip files > max size)
- Git-aware (respects .gitignore if present)
Python Mapping: detect.py
Responsibility: Extract nodes and edges from files
Inputs: List<DetectedFile>
Outputs: List<ExtractionResult>
Implementation: Two extraction paths:
- Uses
TreeSitter.Bindingsfor multi-language AST parsing - Extracts:
- Classes, functions, methods
- Import/require statements
- Call graphs (function → function edges)
- Docstrings and rationale comments (
// NOTE:,// WHY:, etc.)
- All edges marked
Confidence.Extracted
- Uses
Microsoft.Extensions.AIwithIChatClientabstraction - Supports any compatible provider (OpenAI, Anthropic, Azure OpenAI)
- Extracts:
- Concepts and entities
- Relationships between concepts
- Design rationale
- Confidence scores for inferences
- Images processed via vision models
- PDF text extraction via standard libraries
Python Mapping: extract.py, semantic_extractor.py
Responsibility: Assemble nodes and edges into a graph
Inputs: List<ExtractionResult>
Outputs: KnowledgeGraph
Implementation:
- Validate all extraction results via
ExtractionValidator - Deduplicate nodes by ID (merge metadata)
- Resolve edge string IDs to GraphNode references
- Build
BidirectionalGraph<GraphNode, GraphEdge> - Wrap in
KnowledgeGraphdomain model
Python Mapping: build_graph.py
Responsibility: Community detection via Louvain algorithm
Inputs: KnowledgeGraph
Outputs: KnowledgeGraph (with community assignments)
Implementation:
- Convert graph to adjacency format required by clustering library
- Apply Louvain community detection
- Assign community IDs to nodes via
AssignCommunities() - Calculate modularity score
Library: Microsoft.Research.GraphCluster or Accord.NET
Python Mapping: cluster.py (uses graspologic for Leiden)
Responsibility: Calculate graph metrics and identify key nodes
Inputs: KnowledgeGraph (clustered)
Outputs: AnalysisResult
Implementation: Uses QuikGraph algorithm library:
- Degree centrality: Identify "god nodes" (highest degree)
- Betweenness centrality: Nodes on shortest paths
- Surprising connections: Cross-community edges weighted by rarity
- Community statistics: Size, density, inter-community edges
Python Mapping: analyze.py
Responsibility: Generate human-readable summary
Inputs: AnalysisResult, KnowledgeGraph
Outputs: GraphReport (GRAPH_REPORT.md)
Implementation:
- Format top god nodes with descriptions
- List surprising connections with "why" explanations
- Generate suggested questions based on graph structure
- Token reduction benchmark vs reading raw files
Python Mapping: report.py
Responsibility: Serialize graph to various formats
Interface: IGraphExporter
public interface IGraphExporter
{
Task ExportAsync(KnowledgeGraph graph, string outputPath, CancellationToken cancellationToken = default);
}Implementations:
- Serialize entire graph to
graph.json - Include nodes, edges, communities, metadata, analysis results
- Generate interactive vis.js visualization
- Embed HtmlTemplate resource
- Color nodes by community
- Support click, zoom, search, filter
- Render static SVG using force-directed layout
- Suitable for documentation embedding
- Generate
wiki/directory with markdown articles - One article per community
index.mdentry point with navigation
- Create Obsidian vault with backlinks
- Node files use
[[wikilinks]]syntax - Graph view compatible
- Generate
cypher.txtwith CREATE statements - Optional direct push via Bolt protocol
- Generate
graph.ladybug.cypherwith Ladybug-specific DDL (CREATE NODE TABLE,CREATE REL TABLE) - Metadata stored as native
MAP(STRING, STRING)instead of JSON strings - Embedded database compatibility — no server required
Python Mapping: export.py
graphify-dotnet uses the Microsoft.Extensions.AI abstraction layer for LLM interactions:
public interface IChatClient
{
Task<ChatCompletion> GetChatCompletionAsync(ChatMessage[] messages, CancellationToken cancellationToken = default);
}Benefits:
- Provider-agnostic (OpenAI, Anthropic, Azure OpenAI, local models)
- Testable via mock implementations
- Consistent API across all semantic extraction stages
Configuration:
- Inject
IChatClientvia DI - Configure provider in
appsettings.jsonor environment variables - Supports Azure OpenAI managed identity
Located in Pipeline/ExtractionPrompts.cs:
- Document concept extraction
- Image/diagram analysis
- Design rationale extraction
- Confidence scoring guidelines
Why QuikGraph?
- Mature, stable library for .NET graph algorithms
- Generic design: works with any
IEdge<TVertex> - Bidirectional edges: O(1) access to in/out edges
- Algorithm library: betweenness centrality, shortest paths, topological sort
BidirectionalGraph<GraphNode, GraphEdge>:
- Vertices:
GraphNodeinstances - Edges:
GraphEdgeimplementingIEdge<GraphNode> - Parallel edges allowed (same source/target, different relation)
Why wrap QuikGraph?
- Domain-specific API (
GetNodesByCommunity()vs raw QuikGraph) - Node indexing by string ID (QuikGraph only indexes by object reference)
- Node replacement semantics (remove + add cycle hidden from caller)
- Future-proofing: swap graph library without changing pipeline code
Trade-offs:
- Immutability cost: Updating node properties requires remove+add for all edges
- Acceptable because clustering happens once per pipeline run
- Deduplication is caller's responsibility for parallel edges
SemanticCache tracks file changes:
- Hash each file's content with SHA256
- Store hash → ExtractionResult mapping
- On
--update, only re-extract files with changed hashes - AST extraction is fast enough to skip cache (no LLM cost)
Python Mapping: cache/ directory with SHA256 entries
InputValidator:
- Path traversal prevention (reject
..) - Maximum file size limits
- Allowed file extension whitelist
- Sanitize user-provided identifiers
ExtractionValidator:
- All nodes have non-empty
Id,Label,SourceFile - All edges reference valid node IDs
- All edges have non-empty
RelationandSourceFile - Returns
ValidationResult(non-throwing)
Python Mapping: validate.py
- Pipeline stage isolation
- Mock
IChatClientfor semantic extraction - QuikGraph integration tests
- Validation logic tests
- End-to-end pipeline on sample codebases
- Export format validation
- Cache correctness
- CLI command execution
| Python Module | .NET Implementation | Notes |
|---|---|---|
detect.py |
Pipeline/FileDetector.cs |
Uses .NET FileSystemWatcher patterns |
extract.py |
Pipeline/Extractor.cs |
Hybrid AST + semantic |
semantic_extractor.py |
Pipeline/SemanticExtractor.cs |
Uses IChatClient abstraction |
build_graph.py |
Pipeline/GraphBuilder.cs |
Builds KnowledgeGraph |
cluster.py |
Pipeline/ClusterEngine.cs |
Louvain instead of Leiden |
analyze.py |
Pipeline/Analyzer.cs |
Uses QuikGraph algorithms |
report.py |
Pipeline/ReportGenerator.cs |
Markdown generation |
export.py |
Export/IGraphExporter implementations |
Multiple exporters |
validate.py |
Validation/ExtractionValidator.cs |
Non-throwing validation |
cache/ |
Cache/SemanticCache.cs |
SHA256 hashing |
| NetworkX | QuikGraph | .NET graph library |
| graspologic (Leiden) | Louvain | Community detection |
| tree-sitter (Python bindings) | TreeSitter.Bindings | Multi-language AST |
| Claude API | Microsoft.Extensions.AI | Provider-agnostic |
| Package | Purpose |
|---|---|
| Microsoft.Extensions.AI | LLM abstraction (IChatClient) |
| QuikGraph | Graph data structures and algorithms |
| TreeSitter.Bindings | Multi-language AST parsing |
| System.CommandLine | CLI framework |
| ModelContextProtocol | MCP stdio server |
| Microsoft.Extensions.DependencyInjection | DI container |
| Microsoft.Extensions.Configuration | Config management |
| Microsoft.Extensions.Logging | Logging abstraction |
| xUnit | Testing framework |
| coverlet.collector | Code coverage |
Python graphify has a hyperedges list for N-to-M relationships (e.g., all classes implementing a protocol). QuikGraph doesn't support hyperedges natively. Current approach: store as metadata or separate list.
Should we serialize the entire QuikGraph or just nodes+edges as JSON? Current approach: JSON export serializes nodes+edges only (QuikGraph is runtime structure).
Should community assignments be stored in a separate Dictionary<string, int> instead of mutating nodes? Current approach mutates nodes (requires remove+add cycle) for simplicity.
Python uses Leiden via graspologic. .NET uses Louvain. Leiden typically finds higher-quality communities but is newer. May revisit if Leiden becomes available in .NET.
- File detection: O(n) files, limited by filesystem
- AST extraction: O(n) files × O(m) AST nodes per file
- Semantic extraction: O(n) docs × LLM latency (parallelizable)
- Graph building: O(n) nodes + O(e) edges
- Clustering: O(n log n) for Louvain
- Analysis: O(n + e) for most metrics, O(n²) for betweenness
- Export: O(n + e) serialization
Scaling: Tested with graphs up to 10,000 nodes. Beyond 100k nodes, consider graph database (Neo4j) instead of in-memory QuikGraph.
See root README.md for contribution guidelines. Key extension points:
- Add a language: Implement tree-sitter grammar support in
Extractor.cs - Add an exporter: Implement
IGraphExporterfor new format - Add a pipeline stage: Implement
IPipelineStage<TIn, TOut> - Add validation rules: Extend
ExtractionValidatoror create custom validator
- Python source: safishamsi/graphify
- QuikGraph: GitHub
- Microsoft.Extensions.AI: NuGet
- TreeSitter: tree-sitter.github.io