#!/usr/bin/env npx tsx
/**
* Generic Dependency Graph Generator
*
* Scans a TypeScript codebase and generates:
* - docs/architecture/DEPENDENCY_GRAPH.md (human-readable)
* - docs/architecture/dependency-graph.json (machine-readable)
* - docs/architecture/dependency-graph.yaml (compact, ~40% smaller than JSON)
* - docs/architecture/dependency-summary.compact.json (LLM-optimized, ~10KB)
*
* Usage: npx tsx tools/create-dependency-graph/create-dependency-graph.ts
*
* This tool is generic and does not depend on any codebase-specific functions.
* It dynamically discovers the project structure from the filesystem.
*/
import { readFileSync, writeFileSync, readdirSync, statSync, existsSync, mkdirSync } from 'fs';
import { join, dirname, relative, basename } from 'path';
import { fileURLToPath } from 'url';
import yaml from 'js-yaml';
// Types
interface Dependency {
file: string;
imports: string[];
reExport?: boolean;
typeOnly?: boolean; // Track type-only imports
}
interface ExternalDependency {
package: string;
imports: string[];
}
interface NodeDependency {
module: string;
imports: string[];
}
interface FileExports {
named: string[];
default: string | null;
types: string[];
interfaces: string[];
enums: string[];
classes: string[];
functions: string[];
constants: string[];
reExported: string[]; // Track re-exported symbols
}
interface ParsedFile {
path: string;
name: string;
externalDependencies: ExternalDependency[];
nodeDependencies: NodeDependency[];
internalDependencies: Dependency[];
exports: FileExports;
description: string | null;
}
interface DependencyMatrix {
[path: string]: {
importsFrom: string[];
exportsTo: string[];
};
}
interface Statistics {
totalTypeScriptFiles: number;
totalModules: number;
totalLinesOfCode: number;
totalExports: number;
totalClasses: number;
totalInterfaces: number;
totalFunctions: number;
totalTypeGuards: number;
totalEnums: number;
totalConstants: number;
totalReExports: number;
totalTypeOnlyImports: number;
runtimeCircularDeps: number; // Excludes type-only cycles
typeOnlyCircularDeps: number; // Type-only cycles (not runtime issues)
}
interface ModuleMap {
[moduleName: string]: {
[filePath: string]: ParsedFile;
};
}
interface PackageJson {
name: string;
version: string;
}
// Constants
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const ROOT_DIR = join(__dirname, '..', '..');
const SRC_DIR = join(ROOT_DIR, 'src');
const OUTPUT_DIR = join(ROOT_DIR, 'docs', 'architecture');
// Read package.json for version and name (prefer workspace package.json)
let packageJson: PackageJson = { name: 'unknown', version: '0.0.0' };
try {
// Try workspace package.json first (src/memory/package.json)
const workspacePackageJson = join(SRC_DIR, 'memory', 'package.json');
if (existsSync(workspacePackageJson)) {
packageJson = JSON.parse(readFileSync(workspacePackageJson, 'utf-8')) as PackageJson;
} else {
packageJson = JSON.parse(readFileSync(join(ROOT_DIR, 'package.json'), 'utf-8')) as PackageJson;
}
} catch {
console.warn('Warning: Could not read package.json, using defaults');
}
/**
* Recursively get all TypeScript files in a directory
*/
function getAllTsFiles(dir: string, files: string[] = []): string[] {
if (!existsSync(dir)) {
return files;
}
const entries = readdirSync(dir);
for (const entry of entries) {
// Skip node_modules and dist directories
if (entry === 'node_modules' || entry === 'dist') {
continue;
}
const fullPath = join(dir, entry);
const stat = statSync(fullPath);
if (stat.isDirectory()) {
getAllTsFiles(fullPath, files);
} else if (entry.endsWith('.ts') && !entry.endsWith('.test.ts') && !entry.endsWith('.spec.ts')) {
files.push(fullPath);
}
}
return files;
}
/**
* Parse a TypeScript file for imports and exports
*/
function parseFile(filePath: string): ParsedFile {
const content = readFileSync(filePath, 'utf-8');
const relativePath = relative(ROOT_DIR, filePath).replace(/\\/g, '/');
const result: ParsedFile = {
path: relativePath,
name: basename(filePath, '.ts'),
externalDependencies: [],
nodeDependencies: [],
internalDependencies: [],
exports: {
named: [],
default: null,
types: [],
interfaces: [],
enums: [],
classes: [],
functions: [],
constants: [],
reExported: []
},
description: extractDescription(content)
};
// Parse imports - enhanced to detect type-only imports
// Matches: import type { ... }, import { type X, Y }, import X from, import * as X from
const importRegex = /import\s+(type\s+)?(?:(?:{([^}]+)}|(\w+)|\*\s+as\s+(\w+))(?:\s*,\s*(?:{([^}]+)}|(\w+)))?)\s+from\s+['"]([^'"]+)['"]/g;
let match: RegExpExecArray | null;
const nodeBuiltins = ['fs', 'path', 'url', 'crypto', 'util', 'stream', 'events', 'buffer', 'os', 'child_process', 'http', 'https', 'net', 'dns', 'tls', 'zlib', 'readline', 'assert', 'cluster', 'dgram', 'domain', 'inspector', 'module', 'perf_hooks', 'process', 'punycode', 'querystring', 'repl', 'string_decoder', 'timers', 'tty', 'v8', 'vm', 'worker_threads'];
while ((match = importRegex.exec(content)) !== null) {
const isTypeOnlyImport = !!match[1]; // "import type" prefix
const namedImports = match[2] || match[5] || '';
const defaultImport = match[3] || match[6] || '';
const namespaceImport = match[4] || '';
const source = match[7];
const imports: string[] = [];
let hasRuntimeImport = !isTypeOnlyImport;
if (namedImports) {
const importItems = namedImports.split(',').map(s => s.trim());
for (const item of importItems) {
// Check for inline type imports: import { type Foo, Bar }
const isInlineType = item.startsWith('type ');
const name = item.replace(/^type\s+/, '').split(' as ')[0].trim();
if (name) {
imports.push(name);
// If any import is NOT a type, it's a runtime import
if (!isInlineType && !isTypeOnlyImport) {
hasRuntimeImport = true;
}
}
}
}
if (defaultImport) imports.push(defaultImport);
if (namespaceImport) imports.push(`* as ${namespaceImport}`);
const typeOnly = isTypeOnlyImport || !hasRuntimeImport;
if (source.startsWith('.')) {
result.internalDependencies.push({
file: source,
imports: imports,
typeOnly: typeOnly
});
} else if (source.startsWith('node:') || nodeBuiltins.includes(source.split('/')[0])) {
result.nodeDependencies.push({
module: source.replace('node:', ''),
imports: imports
});
} else {
result.externalDependencies.push({
package: source,
imports: imports
});
}
}
// Parse exports
// Named exports: export { foo, bar }
const namedExportRegex = /export\s*{\s*([^}]+)\s*}/g;
while ((match = namedExportRegex.exec(content)) !== null) {
const exports = match[1].split(',').map(s => s.trim().split(' as ')[0]).filter(Boolean);
result.exports.named.push(...exports);
}
// Export declarations
// export const/let/var
const constExportRegex = /export\s+(?:const|let|var)\s+(\w+)/g;
while ((match = constExportRegex.exec(content)) !== null) {
result.exports.constants.push(match[1]);
result.exports.named.push(match[1]);
}
// export function
const funcExportRegex = /export\s+(?:async\s+)?function\s+(\w+)/g;
while ((match = funcExportRegex.exec(content)) !== null) {
result.exports.functions.push(match[1]);
result.exports.named.push(match[1]);
}
// export class
const classExportRegex = /export\s+class\s+(\w+)/g;
while ((match = classExportRegex.exec(content)) !== null) {
result.exports.classes.push(match[1]);
result.exports.named.push(match[1]);
}
// export interface
const interfaceExportRegex = /export\s+interface\s+(\w+)/g;
while ((match = interfaceExportRegex.exec(content)) !== null) {
result.exports.interfaces.push(match[1]);
result.exports.types.push(match[1]);
}
// export type
const typeExportRegex = /export\s+type\s+(\w+)/g;
while ((match = typeExportRegex.exec(content)) !== null) {
result.exports.types.push(match[1]);
}
// export enum
const enumExportRegex = /export\s+enum\s+(\w+)/g;
while ((match = enumExportRegex.exec(content)) !== null) {
result.exports.enums.push(match[1]);
result.exports.named.push(match[1]);
}
// export default
const defaultExportRegex = /export\s+default\s+(?:class|function|const|let|var)?\s*(\w+)?/;
const defaultMatch = content.match(defaultExportRegex);
if (defaultMatch) {
result.exports.default = defaultMatch[1] || 'default';
}
// Re-exports: export * from
const reExportAllRegex = /export\s+\*\s+from\s+['"]([^'"]+)['"]/g;
while ((match = reExportAllRegex.exec(content)) !== null) {
result.internalDependencies.push({
file: match[1],
imports: ['*'],
reExport: true
});
result.exports.reExported.push(`* from ${match[1]}`);
}
// Re-exports: export { foo } from
const reExportNamedRegex = /export\s*{\s*([^}]+)\s*}\s*from\s+['"]([^'"]+)['"]/g;
while ((match = reExportNamedRegex.exec(content)) !== null) {
const exports = match[1].split(',').map(s => s.trim().split(' as ')[0]).filter(Boolean);
result.internalDependencies.push({
file: match[2],
imports: exports,
reExport: true
});
result.exports.named.push(...exports);
result.exports.reExported.push(...exports);
}
// Re-exports: export type * from (type-only re-exports)
const reExportTypeAllRegex = /export\s+type\s+\*\s+from\s+['"]([^'"]+)['"]/g;
while ((match = reExportTypeAllRegex.exec(content)) !== null) {
result.internalDependencies.push({
file: match[1],
imports: ['*'],
reExport: true,
typeOnly: true
});
result.exports.reExported.push(`type * from ${match[1]}`);
}
// Dedupe exports
result.exports.named = [...new Set(result.exports.named)];
result.exports.types = [...new Set(result.exports.types)];
result.exports.reExported = [...new Set(result.exports.reExported)];
return result;
}
/**
* Extract file description from comments
*/
function extractDescription(content: string): string | null {
// Try to find JSDoc comment at the top
const jsdocMatch = content.match(/\/\*\*\s*\n([^*]*(?:\*(?!\/)[^*]*)*)\*\//);
if (jsdocMatch) {
const lines = jsdocMatch[1].split('\n')
.map(line => line.replace(/^\s*\*\s?/, '').trim())
.filter(line => !line.startsWith('@') && line.length > 0);
if (lines.length > 0) {
return lines[0];
}
}
// Try single-line comment
const singleLineMatch = content.match(/^\/\/\s*(.+)$/m);
if (singleLineMatch) {
return singleLineMatch[1];
}
return null;
}
/**
* Dynamically discover and categorize files into modules based on directory structure
* Supports nested structures like src/memory/core/, src/memory/features/, etc.
*/
function categorizeFiles(files: ParsedFile[]): ModuleMap {
const modules: ModuleMap = {};
for (const file of files) {
const relativePath = file.path;
// Handle entry point (src/index.ts)
if (relativePath === 'src/index.ts') {
if (!modules.entry) modules.entry = {};
modules.entry[relativePath] = file;
continue;
}
// Extract the module name from path
const parts = relativePath.split('/');
if (parts.length >= 2 && parts[0] === 'src') {
let moduleName: string;
// If structure is src/package/submodule/ (e.g., src/memory/core/)
// use submodule as the module name for better granularity
if (parts.length >= 4 && !parts[2].endsWith('.ts')) {
moduleName = parts[2]; // Use submodule name (core, features, search, etc.)
} else if (parts.length === 3 && parts[2].endsWith('.ts')) {
// File directly in src/package/ (e.g., src/memory/index.ts)
moduleName = parts[1]; // Use package name
} else {
moduleName = parts[1].replace('.ts', '');
}
// If it's a file directly in src/, categorize by filename
if (parts.length === 2) {
if (!modules.root) modules.root = {};
modules.root[relativePath] = file;
} else {
// It's in a subdirectory
if (!modules[moduleName]) modules[moduleName] = {};
modules[moduleName][relativePath] = file;
}
}
}
// Remove empty modules
for (const key of Object.keys(modules)) {
if (Object.keys(modules[key]).length === 0) {
delete modules[key];
}
}
return modules;
}
/**
* Build dependency matrix
*/
function buildDependencyMatrix(files: ParsedFile[]): DependencyMatrix {
const matrix: DependencyMatrix = {};
for (const file of files) {
const importedFrom = new Set<string>();
const exportsTo = new Set<string>();
// Find what this file imports from
for (const dep of file.internalDependencies) {
importedFrom.add(dep.file);
}
// Find what files export to this file
for (const other of files) {
if (other.path === file.path) continue;
for (const dep of other.internalDependencies) {
const resolvedPath = resolvePath(other.path, dep.file);
if (resolvedPath === file.path || resolvedPath === file.path.replace('.ts', '')) {
exportsTo.add(other.path);
}
}
}
matrix[file.path] = {
importsFrom: [...importedFrom],
exportsTo: [...exportsTo]
};
}
return matrix;
}
/**
* Resolve relative path
*/
function resolvePath(fromPath: string, relativePath: string): string {
const dir = dirname(fromPath);
let resolved = join(dir, relativePath);
// Remove .js extension if present
resolved = resolved.replace(/\.js$/, '');
// Add .ts extension if not present
if (!resolved.endsWith('.ts')) {
resolved = resolved + '.ts';
}
// Normalize path separators
resolved = resolved.replace(/\\/g, '/');
return resolved;
}
interface CircularDependencyResult {
all: string[][];
runtime: string[][]; // Non-type-only cycles (real runtime issues)
typeOnly: string[][]; // Type-only cycles (safe, no runtime impact)
}
/**
* Detect circular dependencies, distinguishing runtime from type-only cycles
*/
function detectCircularDependencies(files: ParsedFile[]): CircularDependencyResult {
const filePaths = new Set(files.map(f => f.path));
// Build both runtime-only and all-dependencies graphs
const runtimeGraph = new Map<string, string[]>();
const allGraph = new Map<string, string[]>();
for (const file of files) {
const runtimeDeps: string[] = [];
const allDeps: string[] = [];
for (const d of file.internalDependencies) {
const resolved = resolvePath(file.path, d.file);
if (filePaths.has(resolved)) {
allDeps.push(resolved);
// Only add to runtime graph if NOT type-only
if (!d.typeOnly) {
runtimeDeps.push(resolved);
}
}
}
runtimeGraph.set(file.path, runtimeDeps);
allGraph.set(file.path, allDeps);
}
function findCycles(graph: Map<string, string[]>): string[][] {
const cycles: string[][] = [];
const visited = new Set<string>();
const inStack = new Set<string>();
function dfs(node: string, path: string[]): void {
if (inStack.has(node)) {
const cycleStart = path.indexOf(node);
if (cycleStart !== -1) {
const cycle = path.slice(cycleStart);
cycle.push(node);
const cycleKey = [...cycle].sort().join('->');
if (!cycles.some(c => [...c].sort().join('->') === cycleKey)) {
cycles.push(cycle);
}
}
return;
}
if (visited.has(node)) return;
visited.add(node);
inStack.add(node);
path.push(node);
const neighbors = graph.get(node) || [];
for (const neighbor of neighbors) {
dfs(neighbor, path);
}
path.pop();
inStack.delete(node);
}
for (const node of graph.keys()) {
if (!visited.has(node)) {
dfs(node, []);
}
}
return cycles;
}
const allCycles = findCycles(allGraph);
const runtimeCycles = findCycles(runtimeGraph);
// Type-only cycles = cycles in all but not in runtime
const runtimeCycleKeys = new Set(runtimeCycles.map(c => [...c].sort().join('->')));
const typeOnlyCycles = allCycles.filter(c => !runtimeCycleKeys.has([...c].sort().join('->')));
return {
all: allCycles,
runtime: runtimeCycles,
typeOnly: typeOnlyCycles
};
}
/**
* Generate statistics from parsed files
*/
function generateStatistics(files: ParsedFile[], modules: ModuleMap, circularDeps: CircularDependencyResult): Statistics {
let totalExports = 0;
let totalClasses = 0;
let totalInterfaces = 0;
let totalFunctions = 0;
let totalTypeGuards = 0;
let totalEnums = 0;
let totalConstants = 0;
let totalLines = 0;
let totalReExports = 0;
let totalTypeOnlyImports = 0;
for (const file of files) {
totalExports += file.exports.named.length;
totalClasses += file.exports.classes.length;
totalInterfaces += file.exports.interfaces.length;
totalFunctions += file.exports.functions.length;
totalEnums += file.exports.enums.length;
totalConstants += file.exports.constants.length;
totalReExports += file.exports.reExported.length;
// Count type-only imports
totalTypeOnlyImports += file.internalDependencies.filter(d => d.typeOnly).length;
// Count type guards (functions starting with 'is')
totalTypeGuards += file.exports.functions.filter(f => f.startsWith('is')).length;
// Count lines
try {
const content = readFileSync(join(ROOT_DIR, file.path), 'utf-8');
totalLines += content.split('\n').length;
} catch {
// Ignore
}
}
return {
totalTypeScriptFiles: files.length,
totalModules: Object.keys(modules).length,
totalLinesOfCode: totalLines,
totalExports,
totalClasses,
totalInterfaces,
totalFunctions,
totalTypeGuards,
totalEnums,
totalConstants,
totalReExports,
totalTypeOnlyImports,
runtimeCircularDeps: circularDeps.runtime.length,
typeOnlyCircularDeps: circularDeps.typeOnly.length
};
}
/**
* Generate JSON output
*/
function generateJSON(files: ParsedFile[], modules: ModuleMap, stats: Statistics, circularDeps: CircularDependencyResult): object {
const today = new Date().toISOString().split('T')[0];
// Convert modules to JSON-friendly format
const modulesJson: Record<string, Record<string, object>> = {};
for (const [category, categoryFiles] of Object.entries(modules)) {
modulesJson[category] = {};
for (const [path, file] of Object.entries(categoryFiles)) {
const fileData: Record<string, unknown> = {
description: file.description || `${file.name} module`,
externalDependencies: file.externalDependencies,
nodeDependencies: file.nodeDependencies,
internalDependencies: file.internalDependencies.map(d => ({
file: d.file,
imports: d.imports,
...(d.reExport ? { reExport: true } : {}),
...(d.typeOnly ? { typeOnly: true } : {})
})),
exports: file.exports.named,
reExported: file.exports.reExported.length > 0 ? file.exports.reExported : undefined,
classes: file.exports.classes.length > 0 ? file.exports.classes : undefined,
interfaces: file.exports.interfaces.length > 0 ? file.exports.interfaces : undefined,
functions: file.exports.functions.length > 0 ? file.exports.functions : undefined,
enums: file.exports.enums.length > 0 ? file.exports.enums : undefined,
constants: file.exports.constants.length > 0 ? file.exports.constants : undefined
};
// Clean up undefined values
Object.keys(fileData).forEach(key => {
if (fileData[key] === undefined) {
delete fileData[key];
}
});
modulesJson[category][path] = fileData;
}
}
// Build layers from modules
const layers = Object.keys(modules).map(name => ({
name: name.charAt(0).toUpperCase() + name.slice(1),
files: Object.keys(modules[name])
})).filter(l => l.files.length > 0);
return {
metadata: {
name: packageJson.name,
version: packageJson.version,
lastUpdated: today,
totalFiles: stats.totalTypeScriptFiles,
totalModules: stats.totalModules,
totalExports: stats.totalExports
},
entryPoints: files
.filter(f => f.path === 'src/index.ts')
.map(f => ({
file: f.path,
type: 'main',
description: f.description || 'Entry Point'
})),
modules: modulesJson,
dependencyGraph: {
circularDependencies: {
runtime: circularDeps.runtime,
typeOnly: circularDeps.typeOnly,
total: circularDeps.all.length,
runtimeCount: circularDeps.runtime.length,
typeOnlyCount: circularDeps.typeOnly.length
},
layers
},
statistics: stats
};
}
/**
* Generate a dynamic Mermaid diagram from actual dependencies
*/
function generateMermaidDiagram(modules: ModuleMap, files: ParsedFile[]): string {
const lines: string[] = [];
lines.push('```mermaid');
lines.push('graph TD');
// Create subgraphs for each module
const moduleNames = Object.keys(modules);
const nodeIds = new Map<string, string>();
let nodeCounter = 0;
for (const moduleName of moduleNames) {
const title = moduleName.charAt(0).toUpperCase() + moduleName.slice(1);
lines.push(` subgraph ${title}`);
const moduleFiles = Object.keys(modules[moduleName]);
for (const filePath of moduleFiles.slice(0, 5)) { // Limit to 5 files per module for readability
const name = basename(filePath, '.ts');
const nodeId = `N${nodeCounter++}`;
nodeIds.set(filePath, nodeId);
lines.push(` ${nodeId}[${name}]`);
}
if (moduleFiles.length > 5) {
const nodeId = `N${nodeCounter++}`;
lines.push(` ${nodeId}[...${moduleFiles.length - 5} more]`);
}
lines.push(' end');
lines.push('');
}
// Add edges for dependencies (limited for readability)
const addedEdges = new Set<string>();
let edgeCount = 0;
const maxEdges = 30;
for (const file of files) {
const sourceId = nodeIds.get(file.path);
if (!sourceId) continue;
for (const dep of file.internalDependencies) {
if (edgeCount >= maxEdges) break;
const resolved = resolvePath(file.path, dep.file);
const targetId = nodeIds.get(resolved);
if (targetId && sourceId !== targetId) {
const edgeKey = `${sourceId}-${targetId}`;
if (!addedEdges.has(edgeKey)) {
lines.push(` ${sourceId} --> ${targetId}`);
addedEdges.add(edgeKey);
edgeCount++;
}
}
}
}
lines.push('```');
return lines.join('\n');
}
/**
* Generate Markdown output
*/
function generateMarkdown(files: ParsedFile[], modules: ModuleMap, stats: Statistics, circularDeps: CircularDependencyResult, matrix: DependencyMatrix): string {
const today = new Date().toISOString().split('T')[0];
const lines: string[] = [];
const projectName = packageJson.name || 'Project';
lines.push(`# ${projectName} - Dependency Graph`);
lines.push('');
lines.push(`**Version**: ${packageJson.version} | **Last Updated**: ${today}`);
lines.push('');
lines.push('This document provides a comprehensive dependency graph of all files, components, imports, functions, and variables in the codebase.');
lines.push('');
lines.push('---');
lines.push('');
// Table of Contents
lines.push('## Table of Contents');
lines.push('');
lines.push('1. [Overview](#overview)');
let tocIndex = 2;
for (const category of Object.keys(modules)) {
const title = category.charAt(0).toUpperCase() + category.slice(1).replace(/-/g, ' ');
lines.push(`${tocIndex}. [${title} Dependencies](#${category.toLowerCase().replace(/\s+/g, '-')}-dependencies)`);
tocIndex++;
}
lines.push(`${tocIndex}. [Dependency Matrix](#dependency-matrix)`);
lines.push(`${tocIndex + 1}. [Circular Dependency Analysis](#circular-dependency-analysis)`);
lines.push(`${tocIndex + 2}. [Visual Dependency Graph](#visual-dependency-graph)`);
lines.push(`${tocIndex + 3}. [Summary Statistics](#summary-statistics)`);
lines.push('');
lines.push('---');
lines.push('');
// Overview
lines.push('## Overview');
lines.push('');
lines.push('The codebase is organized into the following modules:');
lines.push('');
for (const [moduleName, moduleFiles] of Object.entries(modules)) {
const fileCount = Object.keys(moduleFiles).length;
lines.push(`- **${moduleName}**: ${fileCount} file${fileCount !== 1 ? 's' : ''}`);
}
lines.push('');
lines.push('---');
lines.push('');
// Generate sections for each module category
for (const [category, categoryFiles] of Object.entries(modules)) {
const title = category.charAt(0).toUpperCase() + category.slice(1).replace(/-/g, ' ');
lines.push(`## ${title} Dependencies`);
lines.push('');
for (const [path, file] of Object.entries(categoryFiles)) {
const fileName = basename(path, '.ts');
lines.push(`### \`${path}\` - ${file.description || `${fileName} module`}`);
lines.push('');
// External dependencies
if (file.externalDependencies.length > 0) {
lines.push('**External Dependencies:**');
lines.push('| Package | Import |');
lines.push('|---------|--------|');
for (const dep of file.externalDependencies) {
lines.push(`| \`${dep.package}\` | \`${dep.imports.join(', ')}\` |`);
}
lines.push('');
}
// Node dependencies
if (file.nodeDependencies.length > 0) {
lines.push('**Node.js Built-in Dependencies:**');
lines.push('| Module | Import |');
lines.push('|--------|--------|');
for (const dep of file.nodeDependencies) {
lines.push(`| \`${dep.module}\` | \`${dep.imports.join(', ')}\` |`);
}
lines.push('');
}
// Internal dependencies
if (file.internalDependencies.length > 0) {
lines.push('**Internal Dependencies:**');
lines.push('| File | Imports | Type |');
lines.push('|------|---------|------|');
for (const dep of file.internalDependencies) {
let usage = dep.reExport ? 'Re-export' : 'Import';
if (dep.typeOnly) usage += ' (type-only)';
lines.push(`| \`${dep.file}\` | \`${dep.imports.join(', ')}\` | ${usage} |`);
}
lines.push('');
}
// Exports
if (file.exports.named.length > 0 || file.exports.default || file.exports.reExported.length > 0) {
lines.push('**Exports:**');
if (file.exports.classes.length > 0) {
lines.push(`- Classes: \`${file.exports.classes.join('`, `')}\``);
}
if (file.exports.interfaces.length > 0) {
lines.push(`- Interfaces: \`${file.exports.interfaces.join('`, `')}\``);
}
if (file.exports.enums.length > 0) {
lines.push(`- Enums: \`${file.exports.enums.join('`, `')}\``);
}
if (file.exports.functions.length > 0) {
lines.push(`- Functions: \`${file.exports.functions.join('`, `')}\``);
}
if (file.exports.constants.length > 0) {
lines.push(`- Constants: \`${file.exports.constants.join('`, `')}\``);
}
if (file.exports.reExported.length > 0) {
lines.push(`- Re-exports: \`${file.exports.reExported.join('`, `')}\``);
}
if (file.exports.default) {
lines.push(`- Default: \`${file.exports.default}\``);
}
lines.push('');
}
lines.push('---');
lines.push('');
}
}
// Dependency Matrix
lines.push('## Dependency Matrix');
lines.push('');
lines.push('### File Import/Export Matrix');
lines.push('');
lines.push('| File | Imports From | Exports To |');
lines.push('|------|--------------|------------|');
const matrixEntries = Object.entries(matrix).slice(0, 30); // Limit for readability
for (const [path, deps] of matrixEntries) {
const shortPath = basename(path, '.ts');
const importsCount = deps.importsFrom.length;
const exportsCount = deps.exportsTo.length;
lines.push(`| \`${shortPath}\` | ${importsCount} files | ${exportsCount} files |`);
}
lines.push('');
lines.push('---');
lines.push('');
// Circular Dependencies
lines.push('## Circular Dependency Analysis');
lines.push('');
if (circularDeps.all.length === 0) {
lines.push('**No circular dependencies detected.**');
} else {
lines.push(`**${circularDeps.all.length} circular dependencies detected:**`);
lines.push('');
lines.push(`- **Runtime cycles**: ${circularDeps.runtime.length} (require attention)`);
lines.push(`- **Type-only cycles**: ${circularDeps.typeOnly.length} (safe, no runtime impact)`);
lines.push('');
if (circularDeps.runtime.length > 0) {
lines.push('### Runtime Circular Dependencies');
lines.push('');
lines.push('These cycles involve runtime imports and may cause issues:');
lines.push('');
for (const cycle of circularDeps.runtime.slice(0, 10)) {
lines.push(`- ${cycle.join(' -> ')}`);
}
if (circularDeps.runtime.length > 10) {
lines.push(`- ... and ${circularDeps.runtime.length - 10} more`);
}
lines.push('');
}
if (circularDeps.typeOnly.length > 0) {
lines.push('### Type-Only Circular Dependencies');
lines.push('');
lines.push('These cycles only involve type imports and are safe (erased at runtime):');
lines.push('');
for (const cycle of circularDeps.typeOnly.slice(0, 10)) {
lines.push(`- ${cycle.join(' -> ')}`);
}
if (circularDeps.typeOnly.length > 10) {
lines.push(`- ... and ${circularDeps.typeOnly.length - 10} more`);
}
lines.push('');
}
}
lines.push('---');
lines.push('');
// Visual Dependency Graph
lines.push('## Visual Dependency Graph');
lines.push('');
lines.push(generateMermaidDiagram(modules, files));
lines.push('');
lines.push('---');
lines.push('');
// Summary Statistics
lines.push('## Summary Statistics');
lines.push('');
lines.push('| Category | Count |');
lines.push('|----------|-------|');
lines.push(`| Total TypeScript Files | ${stats.totalTypeScriptFiles} |`);
lines.push(`| Total Modules | ${stats.totalModules} |`);
lines.push(`| Total Lines of Code | ${stats.totalLinesOfCode} |`);
lines.push(`| Total Exports | ${stats.totalExports} |`);
lines.push(`| Total Re-exports | ${stats.totalReExports} |`);
lines.push(`| Total Classes | ${stats.totalClasses} |`);
lines.push(`| Total Interfaces | ${stats.totalInterfaces} |`);
lines.push(`| Total Functions | ${stats.totalFunctions} |`);
lines.push(`| Total Type Guards | ${stats.totalTypeGuards} |`);
lines.push(`| Total Enums | ${stats.totalEnums} |`);
lines.push(`| Type-only Imports | ${stats.totalTypeOnlyImports} |`);
lines.push(`| Runtime Circular Deps | ${stats.runtimeCircularDeps} |`);
lines.push(`| Type-only Circular Deps | ${stats.typeOnlyCircularDeps} |`);
lines.push('');
lines.push('---');
lines.push('');
lines.push(`*Last Updated*: ${today}`);
lines.push(`*Version*: ${packageJson.version}`);
lines.push('');
return lines.join('\n');
}
/**
* Generate compact summary for LLM consumption (~10KB target)
* Uses CTON-style compression: no whitespace, abbreviated keys
*/
function generateCompactSummary(files: ParsedFile[], modules: ModuleMap, stats: Statistics, circularDeps: CircularDependencyResult): string {
// Abbreviate module names and create compact structure
const summary = {
m: { // metadata
n: packageJson.name,
v: packageJson.version,
d: new Date().toISOString().split('T')[0],
f: stats.totalTypeScriptFiles,
e: stats.totalExports,
re: stats.totalReExports
},
s: { // statistics
loc: stats.totalLinesOfCode,
cls: stats.totalClasses,
int: stats.totalInterfaces,
fn: stats.totalFunctions,
tg: stats.totalTypeGuards,
en: stats.totalEnums,
co: stats.totalConstants,
toi: stats.totalTypeOnlyImports
},
c: { // circular deps
rt: circularDeps.runtime.length,
to: circularDeps.typeOnly.length,
// Only include first 5 runtime cycles (if any) for context
rtp: circularDeps.runtime.slice(0, 5).map(c => c.map(p => p.split('/').pop()?.replace('.ts', '')).join('→'))
},
mod: {} as Record<string, { f: number; exp?: string[]; cls?: string[]; err?: string[]; int?: string[]; fn?: string[] }>,
// Hot paths: files with most dependencies
hp: [] as { p: string; i: number; o: number }[]
};
// Compact module summary - more granular breakdown
for (const [modName, modFiles] of Object.entries(modules)) {
const fileList = Object.values(modFiles);
const allClasses = fileList.flatMap(f => f.exports.classes);
const allInterfaces = fileList.flatMap(f => f.exports.interfaces);
const allFunctions = fileList.flatMap(f => f.exports.functions);
const allConstants = fileList.flatMap(f => f.exports.constants);
// Separate error classes from regular classes
const errorClasses = allClasses.filter(c => c.endsWith('Error'));
const regularClasses = allClasses.filter(c => !c.endsWith('Error'));
const modEntry: { f: number; exp?: string[]; cls?: string[]; err?: string[]; int?: string[]; fn?: string[] } = {
f: Object.keys(modFiles).length
};
// Include top exports (constants are usually important API surface)
if (allConstants.length > 0) {
modEntry.exp = [...new Set(allConstants)].slice(0, 15);
}
if (regularClasses.length > 0) {
modEntry.cls = [...new Set(regularClasses)];
}
if (errorClasses.length > 0) {
modEntry.err = [...new Set(errorClasses)];
}
if (allInterfaces.length > 0) {
modEntry.int = [...new Set(allInterfaces)];
}
if (allFunctions.length > 0) {
modEntry.fn = [...new Set(allFunctions)].slice(0, 15);
}
summary.mod[modName] = modEntry;
}
// Find hot paths (files with highest connectivity)
const connectivity = files.map(f => ({
p: f.path.split('/').slice(-2).join('/'),
i: f.internalDependencies.length,
o: files.filter(other =>
other.internalDependencies.some(d => {
const resolved = resolvePath(other.path, d.file);
return resolved === f.path;
})
).length
})).sort((a, b) => (b.i + b.o) - (a.i + a.o));
summary.hp = connectivity.slice(0, 15);
// Output as minified JSON (CTON-style: no whitespace)
return JSON.stringify(summary);
}
/**
* Main function
*/
async function main(): Promise<void> {
console.log('Scanning codebase for dependencies...');
// Ensure output directory exists
if (!existsSync(OUTPUT_DIR)) {
mkdirSync(OUTPUT_DIR, { recursive: true });
console.log(`Created output directory: ${OUTPUT_DIR}`);
}
// Get all TypeScript files
const tsFiles = getAllTsFiles(SRC_DIR);
console.log(`Found ${tsFiles.length} TypeScript files`);
if (tsFiles.length === 0) {
console.error('No TypeScript files found in src/');
process.exit(1);
}
// Parse all files
const parsedFiles = tsFiles.map(parseFile);
console.log('Parsed all files');
// Categorize into modules
const modules = categorizeFiles(parsedFiles);
console.log(`Categorized into ${Object.keys(modules).length} modules`);
// Detect circular dependencies
const circularDeps = detectCircularDependencies(parsedFiles);
console.log(`Found ${circularDeps.all.length} circular dependencies (${circularDeps.runtime.length} runtime, ${circularDeps.typeOnly.length} type-only)`);
// Generate statistics (now needs circularDeps)
const stats = generateStatistics(parsedFiles, modules, circularDeps);
console.log('Generated statistics');
// Build dependency matrix
const matrix = buildDependencyMatrix(parsedFiles);
console.log('Built dependency matrix');
// Generate outputs
const json = generateJSON(parsedFiles, modules, stats, circularDeps);
const markdown = generateMarkdown(parsedFiles, modules, stats, circularDeps, matrix);
// Write outputs
const jsonOutput = JSON.stringify(json, null, 2);
writeFileSync(join(OUTPUT_DIR, 'dependency-graph.json'), jsonOutput);
const jsonSize = Buffer.byteLength(jsonOutput, 'utf8');
console.log(`Written: docs/architecture/dependency-graph.json (${(jsonSize / 1024).toFixed(1)}KB)`);
// Write YAML output (more compact, ~40% smaller than JSON)
const yamlOutput = yaml.dump(json, {
indent: 2,
lineWidth: 120,
noRefs: true,
sortKeys: false,
quotingType: '"',
forceQuotes: false,
});
writeFileSync(join(OUTPUT_DIR, 'dependency-graph.yaml'), yamlOutput);
const yamlSize = Buffer.byteLength(yamlOutput, 'utf8');
const yamlSavings = ((1 - yamlSize / jsonSize) * 100).toFixed(0);
console.log(`Written: docs/architecture/dependency-graph.yaml (${(yamlSize / 1024).toFixed(1)}KB, ${yamlSavings}% smaller)`);
writeFileSync(join(OUTPUT_DIR, 'DEPENDENCY_GRAPH.md'), markdown);
console.log('Written: docs/architecture/DEPENDENCY_GRAPH.md');
// Write compact summary for LLM consumption (CTON-style, ~10KB)
const compactSummary = generateCompactSummary(parsedFiles, modules, stats, circularDeps);
writeFileSync(join(OUTPUT_DIR, 'dependency-summary.compact.json'), compactSummary);
const compactSize = Buffer.byteLength(compactSummary, 'utf8');
console.log(`Written: docs/architecture/dependency-summary.compact.json (${(compactSize / 1024).toFixed(1)}KB)`);
console.log('\nDependency graph generation complete!');
console.log(` - ${stats.totalTypeScriptFiles} files analyzed`);
console.log(` - ${stats.totalExports} exports found (${stats.totalReExports} re-exports)`);
console.log(` - ${stats.totalTypeOnlyImports} type-only imports detected`);
console.log(` - ${circularDeps.all.length} circular dependencies:`);
console.log(` ${circularDeps.runtime.length} runtime (require attention)`);
console.log(` ${circularDeps.typeOnly.length} type-only (safe)`);
console.log(`\nOutput sizes: JSON ${(jsonSize / 1024).toFixed(1)}KB → YAML ${(yamlSize / 1024).toFixed(1)}KB (${yamlSavings}% reduction) → Compact ${(compactSize / 1024).toFixed(1)}KB`);
}
main().catch(console.error);