aluate load-bearing logic rather than generating generic descriptions. A score of 5 indicates that modifying or removing the file will likely break system behavior or violate a core constraint.
interface FileArtifact {
path: string;
purpose: string;
publicSurface: string[];
hiddenInvariants: string[];
riskScore: number; // 1-5
}
async function extractFileDecisions(
filePath: string,
sourceContent: string,
modelClient: any
): Promise<FileArtifact> {
const prompt = `
Analyze the following source file. Return a JSON object with:
- purpose: One-sentence description of the file's role
- publicSurface: Array of exported interfaces, functions, or contracts
- hiddenInvariants: Array of implicit constraints, assumptions, or workarounds
- riskScore: Integer 1-5 indicating load-bearing criticality
Source:
${sourceContent}
`;
const response = await modelClient.generate(prompt, {
temperature: 0.2,
maxTokens: 1024
});
return JSON.parse(response.text) as FileArtifact;
}
Architecture Rationale: Whole-repo context is preserved by feeding the entire repository structure into the model's context window before processing individual files. This prevents cross-reference fragmentation. The risk score acts as a weighting mechanism for downstream clustering, ensuring that high-criticality files dominate the architectural graph rather than being diluted by utility modules.
Phase 2: Module-Level Clustering & ADR Generation
Phase 2 aggregates all FileArtifact outputs and identifies clusters of files that share hidden invariants. Each cluster is transformed into an Architecture Decision Record (ADR) following the standard status/context/decision/consequences format. The clustering algorithm groups files by semantic overlap in their invariants rather than directory structure or import graphs.
interface ADR {
id: string;
status: 'proposed' | 'accepted' | 'deprecated' | 'superseded';
context: string;
decision: string;
consequences: string[];
sourceFiles: string[];
sharedInvariant: string;
}
async function clusterModulesAndGenerateADRs(
artifacts: FileArtifact[],
modelClient: any
): Promise<ADR[]> {
const invariantMap = new Map<string, FileArtifact[]>();
artifacts.forEach(artifact => {
artifact.hiddenInvariants.forEach(inv => {
if (!invariantMap.has(inv)) invariantMap.set(inv, []);
invariantMap.get(inv)!.push(artifact);
});
});
const adrs: ADR[] = [];
for (const [invariant, files] of invariantMap.entries()) {
const prompt = `
Generate an Architecture Decision Record for the following cluster.
Cluster files: ${files.map(f => f.path).join(', ')}
Shared invariant: ${invariant}
Return JSON with: id, status, context, decision, consequences, sourceFiles, sharedInvariant
`;
const response = await modelClient.generate(prompt, { temperature: 0.3 });
adrs.push(JSON.parse(response.text) as ADR);
}
return adrs;
}
Architecture Rationale: Grouping by shared invariants rather than file dependencies surfaces architectural coupling that static analysis misses. The ADR format standardizes output, making it queryable and versionable. Status tracking ensures the graph reflects current system reality rather than historical snapshots.
Phase 3: Architecture-Level Graph Construction
The final phase applies Leiden clustering to the generated ADRs. Leiden is preferred over traditional modularity optimization because it produces stabler cluster boundaries on small-to-medium graphs and guarantees well-connected communities. Each resulting node receives a maintainer-facing explanation that contextualizes the cluster's impact on system evolution.
interface GraphNode {
clusterId: string;
adrs: ADR[];
maintainerNote: string;
crossCuttingConcept: string;
}
async function buildArchitectureGraph(
adrs: ADR[],
modelClient: any
): Promise<GraphNode[]> {
// Simulated Leiden clustering step
// In production, this would use a graph library (e.g., igraph, leidenalg via WASM)
const clusters = performLeidenClustering(adrs);
const graphNodes: GraphNode[] = [];
for (const cluster of clusters) {
const prompt = `
Synthesize a maintainer note for this architectural cluster.
ADRs: ${cluster.map(a => a.decision).join(' | ')}
Focus on cross-cutting impact, migration risks, and maintenance priorities.
Return JSON with: clusterId, adrs, maintainerNote, crossCuttingConcept
`;
const response = await modelClient.generate(prompt, { temperature: 0.2 });
graphNodes.push(JSON.parse(response.text) as GraphNode);
}
return graphNodes;
}
Architecture Rationale: Leiden clustering excels at identifying overlapping concerns in sparse decision graphs. The maintainer note transforms abstract clusters into actionable guidance, bridging the gap between architectural theory and daily engineering work. The output is structured for direct ingestion into knowledge bases like Obsidian, with automatic backlinking between related clusters and source files.
Pitfall Guide
1. Premature File Summarization
Explanation: Compressing files into isolated summaries before clustering severs cross-referential context. The model loses the ability to trace how constraints propagate across modules.
Fix: Ingest whole repositories into the context window. Use structured extraction prompts instead of generative summarization. Preserve raw source references until the clustering phase.
2. Ignoring Risk Score Weighting
Explanation: Treating all files equally dilutes the architectural graph with utility code and masks load-bearing logic. High-risk modules become indistinguishable from helper functions.
Fix: Apply risk scores as edge weights during clustering. Prioritize ADR generation for files scoring 4 or 5. Use risk thresholds to filter noise in the final graph.
3. Using Modularity Clustering on Small Graphs
Explanation: Traditional modularity optimization produces unstable boundaries on graphs with fewer than 50 nodes. Clusters fragment arbitrarily, making the output unreliable for maintenance planning.
Fix: Use Leiden or Louvain variants optimized for small-to-medium graphs. Validate cluster stability by running multiple iterations and measuring Jaccard similarity between runs.
4. Static ADR Generation Without Versioning
Explanation: Generating ADRs as one-off artifacts creates knowledge drift. As the codebase evolves, the graph becomes misaligned with reality, reducing trust in the pipeline.
Fix: Implement incremental updates. Track ADR status transitions and timestamp extractions. Schedule periodic re-runs with delta detection to update only affected clusters.
5. Prompt Drift in Phase 2
Explanation: Inconsistent clustering criteria cause overlapping or contradictory ADRs. The model may group files by superficial similarities rather than shared architectural constraints.
Fix: Anchor clustering prompts with explicit invariant matching rules. Use deterministic pre-filtering to group files by exact invariant strings before LLM synthesis. Validate cluster coherence with semantic similarity thresholds.
6. Over-Engineering the Graph Output
Explanation: Generating excessive nodes or overly granular clusters creates cognitive overload. Maintainers abandon the knowledge base when it requires more effort to navigate than the code itself.
Fix: Apply a minimum cluster size threshold. Merge nodes with >80% semantic overlap. Prioritize breadth over depth in maintainer notes. Use progressive disclosure patterns in the final vault.
7. Skipping Cross-Reference Validation
Explanation: Phantom dependencies appear when the model infers relationships that don't exist in the source. These false links corrupt the graph and mislead refactoring efforts.
Fix: Validate inferred cross-references against static analysis outputs. Require at least two independent signals (e.g., shared invariant + import graph + runtime trace) before establishing a graph edge.
Production Bundle
Action Checklist
Decision Matrix
| Scenario | Recommended Approach | Why | Cost Impact |
|---|
| Monolith >500 files | Whole-repo ingestion + Leiden clustering | Preserves cross-module constraints; Leiden handles medium graphs stably | Moderate compute cost; high maintenance ROI |
| Microservices <50 files | Per-service extraction + static dependency mapping | Overhead of LLM clustering outweighs benefits; static graphs suffice | Low compute cost; minimal setup |
| Active development branch | Incremental delta extraction + ADR versioning | Prevents knowledge drift; aligns graph with live codebase | Higher pipeline complexity; reduces regression risk |
| Legacy archive / read-only | One-pass full extraction + Obsidian vault export | Maximizes context utilization; creates navigable knowledge artifact | One-time compute cost; long-term onboarding savings |
Configuration Template
pipeline:
version: "2.1"
model:
provider: "anthropic"
name: "claude-sonnet-4.6"
context_window: 1000000
temperature: 0.2
extraction:
phase1:
output_format: "json"
risk_threshold: 4
include_invariants: true
phase2:
clustering_strategy: "semantic_invariant"
adr_schema: "standard"
phase3:
algorithm: "leiden"
min_cluster_size: 3
stability_iterations: 5
output:
format: "obsidian_vault"
backlink_strategy: "bidirectional"
include_raw_prompts: false
version_tracking: true
Quick Start Guide
- Initialize the pipeline configuration: Copy the YAML template above and adjust the model provider, context window, and clustering parameters to match your environment.
- Run Phase 1 extraction: Point the pipeline at your repository root. The system will ingest files, extract purpose/surface/invariants/risk, and store structured artifacts.
- Execute clustering and ADR generation: Feed the Phase 1 output into the module-level clusterer. Validate generated ADRs against known architectural constraints before proceeding.
- Build the architecture graph: Apply Leiden clustering to the ADR set. Generate maintainer notes and export the final graph as an Obsidian vault with automatic backlinks.
- Validate and iterate: Cross-check graph edges against static analysis. Schedule incremental updates to keep the knowledge base aligned with codebase evolution.