mirror of
https://github.com/samiyev/puaros.git
synced 2025-12-28 07:16:53 +05:00
Compare commits
5 Commits
ipuaro-v0.
...
ipuaro-v0.
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
92ba3fd9ba | ||
|
|
e9aaa708fe | ||
|
|
d6d15dd271 | ||
|
|
d63d85d850 | ||
|
|
41cfc21f20 |
@@ -5,6 +5,154 @@ All notable changes to this project will be documented in this file.
|
|||||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
|
## [0.29.0] - 2025-12-05 - Impact Score
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- **High Impact Files in Initial Context (v0.29.0)**
|
||||||
|
- New `## High Impact Files` section in initial context
|
||||||
|
- Shows files with highest impact scores (percentage of codebase depending on them)
|
||||||
|
- Table format with File, Impact %, and Dependents count
|
||||||
|
- Files sorted by impact score descending
|
||||||
|
- Default: shows top 10 files with impact score >= 5%
|
||||||
|
|
||||||
|
- **Impact Score Computation**
|
||||||
|
- New `impactScore: number` field in `FileMeta` (0-100)
|
||||||
|
- Formula: `(dependents.length / (totalFiles - 1)) * 100`
|
||||||
|
- Computed in `MetaAnalyzer.analyzeAll()` after all files analyzed
|
||||||
|
- New `calculateImpactScore()` helper function in FileMeta.ts
|
||||||
|
|
||||||
|
- **Configuration Option**
|
||||||
|
- `includeHighImpactFiles: boolean` in ContextConfigSchema (default: `true`)
|
||||||
|
- `includeHighImpactFiles` option in `BuildContextOptions`
|
||||||
|
- Users can disable to save tokens: `context.includeHighImpactFiles: false`
|
||||||
|
|
||||||
|
- **New Helper Function in prompts.ts**
|
||||||
|
- `formatHighImpactFiles()` - formats high impact files table for display
|
||||||
|
|
||||||
|
### New Context Format
|
||||||
|
|
||||||
|
```
|
||||||
|
## High Impact Files
|
||||||
|
|
||||||
|
| File | Impact | Dependents |
|
||||||
|
|------|--------|------------|
|
||||||
|
| utils/validation | 67% | 12 files |
|
||||||
|
| types/user | 45% | 8 files |
|
||||||
|
| services/user | 34% | 6 files |
|
||||||
|
```
|
||||||
|
|
||||||
|
### Technical Details
|
||||||
|
|
||||||
|
- Total tests: 1826 passed (was 1798, +28 new tests)
|
||||||
|
- 9 new tests for calculateImpactScore()
|
||||||
|
- 14 new tests for formatHighImpactFiles() and buildInitialContext
|
||||||
|
- 5 new tests for includeHighImpactFiles config option
|
||||||
|
- Coverage: 97.52% lines, 91.3% branches, 98.63% functions
|
||||||
|
- 0 ESLint errors, 3 warnings (pre-existing complexity)
|
||||||
|
- Build successful
|
||||||
|
|
||||||
|
### Notes
|
||||||
|
|
||||||
|
This completes v0.29.0 of the Graph Metrics milestone:
|
||||||
|
- ✅ 0.27.0 - Inline Dependency Graph
|
||||||
|
- ✅ 0.28.0 - Circular Dependencies in Context
|
||||||
|
- ✅ 0.29.0 - Impact Score
|
||||||
|
|
||||||
|
Next milestone: v0.30.0 - Transitive Dependencies Count
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## [0.28.0] - 2025-12-05 - Circular Dependencies in Context
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- **Circular Dependencies in Initial Context (v0.28.0)**
|
||||||
|
- New `## ⚠️ Circular Dependencies` section in initial context
|
||||||
|
- Shows cycle chains immediately without requiring tool calls
|
||||||
|
- Format: `- services/user → services/auth → services/user`
|
||||||
|
- Uses same path shortening as dependency graph (removes `src/`, extensions, `/index`)
|
||||||
|
|
||||||
|
- **Configuration Option**
|
||||||
|
- `includeCircularDeps: boolean` in ContextConfigSchema (default: `true`)
|
||||||
|
- `includeCircularDeps` option in `BuildContextOptions`
|
||||||
|
- `circularDeps: string[][]` parameter to pass pre-computed cycles
|
||||||
|
- Users can disable to save tokens: `context.includeCircularDeps: false`
|
||||||
|
|
||||||
|
- **New Helper Function in prompts.ts**
|
||||||
|
- `formatCircularDeps()` - formats circular dependency cycles for display
|
||||||
|
|
||||||
|
### New Context Format
|
||||||
|
|
||||||
|
```
|
||||||
|
## ⚠️ Circular Dependencies
|
||||||
|
|
||||||
|
- services/user → services/auth → services/user
|
||||||
|
- utils/a → utils/b → utils/c → utils/a
|
||||||
|
```
|
||||||
|
|
||||||
|
### Technical Details
|
||||||
|
|
||||||
|
- Total tests: 1798 passed (was 1775, +23 new tests)
|
||||||
|
- 12 new tests for formatCircularDeps()
|
||||||
|
- 6 new tests for buildInitialContext with includeCircularDeps
|
||||||
|
- 5 new tests for includeCircularDeps config option
|
||||||
|
- Coverage: 97.48% lines, 91.13% branches, 98.63% functions
|
||||||
|
- 0 ESLint errors, 3 warnings (pre-existing complexity in ASTParser and prompts)
|
||||||
|
- Build successful
|
||||||
|
|
||||||
|
## [0.27.0] - 2025-12-05 - Inline Dependency Graph
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- **Dependency Graph in Initial Context (v0.27.0)**
|
||||||
|
- New `## Dependency Graph` section in initial context
|
||||||
|
- Shows file relationships without requiring tool calls
|
||||||
|
- Format: `services/user: → types/user, utils/validation ← controllers/user`
|
||||||
|
- `→` indicates files this file imports (dependencies)
|
||||||
|
- `←` indicates files that import this file (dependents)
|
||||||
|
- Hub files (>5 dependents) shown first
|
||||||
|
- Files sorted by total connections (descending)
|
||||||
|
|
||||||
|
- **Configuration Option**
|
||||||
|
- `includeDepsGraph: boolean` in ContextConfigSchema (default: `true`)
|
||||||
|
- `includeDepsGraph` option in `BuildContextOptions`
|
||||||
|
- Users can disable to save tokens: `context.includeDepsGraph: false`
|
||||||
|
|
||||||
|
- **New Helper Functions in prompts.ts**
|
||||||
|
- `formatDependencyGraph()` - formats entire dependency graph from metas
|
||||||
|
- `formatDepsEntry()` - formats single file's dependencies/dependents
|
||||||
|
- `shortenPath()` - shortens paths (removes `src/`, extensions, `/index`)
|
||||||
|
|
||||||
|
### New Context Format
|
||||||
|
|
||||||
|
```
|
||||||
|
## Dependency Graph
|
||||||
|
|
||||||
|
utils/validation: ← services/user, services/auth, controllers/api
|
||||||
|
services/user: → types/user, utils/validation ← controllers/user, api/routes
|
||||||
|
services/auth: → services/user, utils/jwt ← controllers/auth
|
||||||
|
types/user: ← services/user, services/auth
|
||||||
|
```
|
||||||
|
|
||||||
|
### Technical Details
|
||||||
|
|
||||||
|
- Total tests: 1775 passed (was 1754, +21 new tests)
|
||||||
|
- 16 new tests for formatDependencyGraph()
|
||||||
|
- 5 new tests for includeDepsGraph config option
|
||||||
|
- Coverage: 97.48% lines, 91.07% branches, 98.62% functions
|
||||||
|
- 0 ESLint errors, 2 warnings (pre-existing complexity in ASTParser and prompts)
|
||||||
|
- Build successful
|
||||||
|
|
||||||
|
### Notes
|
||||||
|
|
||||||
|
This completes v0.27.0 of the Graph Metrics milestone:
|
||||||
|
- ✅ 0.27.0 - Inline Dependency Graph
|
||||||
|
|
||||||
|
Next milestone: v0.28.0 - Circular Dependencies in Context
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## [0.26.0] - 2025-12-05 - Rich Initial Context: Decorator Extraction
|
## [0.26.0] - 2025-12-05 - Rich Initial Context: Decorator Extraction
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|||||||
@@ -1884,14 +1884,12 @@ Enhance initial context for LLM: add function signatures, interface field types,
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Version 0.25.0 - Graph Metrics in Context 📊
|
## Version 0.27.0 - Inline Dependency Graph 📊 ✅
|
||||||
|
|
||||||
**Priority:** MEDIUM
|
**Priority:** MEDIUM
|
||||||
**Status:** Planned
|
**Status:** Complete (v0.27.0 released)
|
||||||
|
|
||||||
Add graph metrics to initial context: dependency graph, circular dependencies, impact score.
|
### Description
|
||||||
|
|
||||||
### 0.25.1 - Inline Dependency Graph
|
|
||||||
|
|
||||||
**Problem:** LLM doesn't see file relationships without tool calls
|
**Problem:** LLM doesn't see file relationships without tool calls
|
||||||
**Solution:** Show dependency graph in context
|
**Solution:** Show dependency graph in context
|
||||||
@@ -1906,14 +1904,25 @@ Add graph metrics to initial context: dependency graph, circular dependencies, i
|
|||||||
```
|
```
|
||||||
|
|
||||||
**Changes:**
|
**Changes:**
|
||||||
- [ ] Add `formatDependencyGraph()` to prompts.ts
|
- [x] Add `formatDependencyGraph()` to prompts.ts
|
||||||
- [ ] Use data from `FileMeta.dependencies` and `FileMeta.dependents`
|
- [x] Use data from `FileMeta.dependencies` and `FileMeta.dependents`
|
||||||
- [ ] Group by hub files (many connections)
|
- [x] Group by hub files (many connections)
|
||||||
- [ ] Add `includeDepsGraph: boolean` option to config
|
- [x] Add `includeDepsGraph: boolean` option to config
|
||||||
|
|
||||||
|
**Tests:**
|
||||||
|
- [x] Unit tests for formatDependencyGraph() (16 tests)
|
||||||
|
- [x] Unit tests for includeDepsGraph config option (5 tests)
|
||||||
|
|
||||||
**Why:** LLM sees architecture without tool call.
|
**Why:** LLM sees architecture without tool call.
|
||||||
|
|
||||||
### 0.25.2 - Circular Dependencies in Context
|
---
|
||||||
|
|
||||||
|
## Version 0.28.0 - Circular Dependencies in Context 🔄 ✅
|
||||||
|
|
||||||
|
**Priority:** MEDIUM
|
||||||
|
**Status:** Complete (v0.28.0 released)
|
||||||
|
|
||||||
|
### Description
|
||||||
|
|
||||||
**Problem:** Circular deps are computed but not shown in context
|
**Problem:** Circular deps are computed but not shown in context
|
||||||
**Solution:** Show cycles immediately
|
**Solution:** Show cycles immediately
|
||||||
@@ -1927,13 +1936,26 @@ Add graph metrics to initial context: dependency graph, circular dependencies, i
|
|||||||
```
|
```
|
||||||
|
|
||||||
**Changes:**
|
**Changes:**
|
||||||
- [ ] Add `formatCircularDeps()` to prompts.ts
|
- [x] Add `formatCircularDeps()` to prompts.ts
|
||||||
- [ ] Get circular deps from IndexBuilder
|
- [x] Add `includeCircularDeps: boolean` config option (default: true)
|
||||||
- [ ] Store in Redis as separate key or in meta
|
- [x] Add `circularDeps: string[][]` parameter to `BuildContextOptions`
|
||||||
|
- [x] Integrate into `buildInitialContext()`
|
||||||
|
|
||||||
|
**Tests:**
|
||||||
|
- [x] Unit tests for formatCircularDeps() (12 tests)
|
||||||
|
- [x] Unit tests for buildInitialContext with includeCircularDeps (6 tests)
|
||||||
|
- [x] Unit tests for includeCircularDeps config option (5 tests)
|
||||||
|
|
||||||
**Why:** LLM immediately sees architecture problems.
|
**Why:** LLM immediately sees architecture problems.
|
||||||
|
|
||||||
### 0.25.3 - Impact Score
|
---
|
||||||
|
|
||||||
|
## Version 0.29.0 - Impact Score 📈 ✅
|
||||||
|
|
||||||
|
**Priority:** MEDIUM
|
||||||
|
**Status:** Complete (v0.29.0 released)
|
||||||
|
|
||||||
|
### Description
|
||||||
|
|
||||||
**Problem:** LLM doesn't know which files are critical
|
**Problem:** LLM doesn't know which files are critical
|
||||||
**Solution:** Show impact score (% of codebase that depends on file)
|
**Solution:** Show impact score (% of codebase that depends on file)
|
||||||
@@ -1950,14 +1972,27 @@ Add graph metrics to initial context: dependency graph, circular dependencies, i
|
|||||||
```
|
```
|
||||||
|
|
||||||
**Changes:**
|
**Changes:**
|
||||||
- [ ] Add `impactScore: number` to FileMeta (0-100)
|
- [x] Add `impactScore: number` to FileMeta (0-100)
|
||||||
- [ ] Compute in MetaAnalyzer: (transitiveDepByCount / totalFiles) * 100
|
- [x] Compute in MetaAnalyzer: (dependents.length / (totalFiles - 1)) * 100
|
||||||
- [ ] Add `formatHighImpactFiles()` to prompts.ts
|
- [x] Add `formatHighImpactFiles()` to prompts.ts
|
||||||
- [ ] Show top-10 high impact files
|
- [x] Show top-10 high impact files
|
||||||
|
- [x] Add `includeHighImpactFiles` config option (default: true)
|
||||||
|
|
||||||
|
**Tests:**
|
||||||
|
- [x] Unit tests for calculateImpactScore (9 tests)
|
||||||
|
- [x] Unit tests for formatHighImpactFiles (14 tests)
|
||||||
|
- [x] Unit tests for includeHighImpactFiles config (5 tests)
|
||||||
|
|
||||||
**Why:** LLM understands which files are critical for changes.
|
**Why:** LLM understands which files are critical for changes.
|
||||||
|
|
||||||
### 0.25.4 - Transitive Dependencies Count
|
---
|
||||||
|
|
||||||
|
## Version 0.30.0 - Transitive Dependencies Count 🔢
|
||||||
|
|
||||||
|
**Priority:** MEDIUM
|
||||||
|
**Status:** Planned
|
||||||
|
|
||||||
|
### Description
|
||||||
|
|
||||||
**Problem:** Currently only counting direct dependencies
|
**Problem:** Currently only counting direct dependencies
|
||||||
**Solution:** Add transitive dependencies to meta
|
**Solution:** Add transitive dependencies to meta
|
||||||
@@ -1977,8 +2012,7 @@ interface FileMeta {
|
|||||||
- [ ] Store in FileMeta
|
- [ ] Store in FileMeta
|
||||||
|
|
||||||
**Tests:**
|
**Tests:**
|
||||||
- [ ] Unit tests for graph metrics computation
|
- [ ] Unit tests for transitive dependencies computation
|
||||||
- [ ] Unit tests for new context sections
|
|
||||||
- [ ] Performance tests for large codebases
|
- [ ] Performance tests for large codebases
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -1994,12 +2028,12 @@ interface FileMeta {
|
|||||||
- [x] Error handling complete ✅ (v0.16.0)
|
- [x] Error handling complete ✅ (v0.16.0)
|
||||||
- [ ] Performance optimized
|
- [ ] Performance optimized
|
||||||
- [x] Documentation complete ✅ (v0.17.0)
|
- [x] Documentation complete ✅ (v0.17.0)
|
||||||
- [x] Test coverage ≥91% branches, ≥95% lines/functions/statements ✅ (91.21% branches, 97.5% lines, 98.58% functions, 97.5% statements - 1687 tests)
|
- [x] Test coverage ≥91% branches, ≥95% lines/functions/statements ✅ (91.3% branches, 97.52% lines, 98.63% functions, 97.52% statements - 1826 tests)
|
||||||
- [x] 0 ESLint errors ✅
|
- [x] 0 ESLint errors ✅
|
||||||
- [x] Examples working ✅ (v0.18.0)
|
- [x] Examples working ✅ (v0.18.0)
|
||||||
- [x] CHANGELOG.md up to date ✅
|
- [x] CHANGELOG.md up to date ✅
|
||||||
- [x] Rich initial context (v0.24.0) — function signatures, interface fields, enum values, decorators ✅
|
- [x] Rich initial context (v0.24.0-v0.26.0) — function signatures, interface fields, enum values, decorators ✅
|
||||||
- [ ] Graph metrics in context (v0.25.0) — dependency graph, circular deps, impact score
|
- [ ] Graph metrics in context (v0.27.0-v0.30.0) — dependency graph ✅, circular deps ✅, impact score ✅, transitive deps
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -2078,7 +2112,7 @@ sessions:list # List<session_id>
|
|||||||
|
|
||||||
**Last Updated:** 2025-12-05
|
**Last Updated:** 2025-12-05
|
||||||
**Target Version:** 1.0.0
|
**Target Version:** 1.0.0
|
||||||
**Current Version:** 0.26.0
|
**Current Version:** 0.29.0
|
||||||
**Next Milestones:** v0.25.0 (Graph Metrics - 0/4 complete)
|
**Next Milestones:** v0.30.0 (Transitive Deps), v1.0.0 (Production Ready)
|
||||||
|
|
||||||
> **Note:** v0.24.0 complete ✅. v0.25.0 (Graph Metrics) is required for 1.0.0 release. It enables LLM to see architecture without tool calls.
|
> **Note:** Rich Initial Context complete ✅ (v0.24.0-v0.26.0). Graph Metrics in progress (v0.27.0 ✅, v0.28.0 ✅, v0.29.0 ✅, v0.30.0 pending) for 1.0.0 release.
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@samiyev/ipuaro",
|
"name": "@samiyev/ipuaro",
|
||||||
"version": "0.26.0",
|
"version": "0.29.0",
|
||||||
"description": "Local AI agent for codebase operations with infinite context feeling",
|
"description": "Local AI agent for codebase operations with infinite context feeling",
|
||||||
"author": "Fozilbek Samiyev <fozilbek.samiyev@gmail.com>",
|
"author": "Fozilbek Samiyev <fozilbek.samiyev@gmail.com>",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|||||||
@@ -26,6 +26,8 @@ export interface FileMeta {
|
|||||||
isEntryPoint: boolean
|
isEntryPoint: boolean
|
||||||
/** File type classification */
|
/** File type classification */
|
||||||
fileType: "source" | "test" | "config" | "types" | "unknown"
|
fileType: "source" | "test" | "config" | "types" | "unknown"
|
||||||
|
/** Impact score (0-100): percentage of codebase that depends on this file */
|
||||||
|
impactScore: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createFileMeta(partial: Partial<FileMeta> = {}): FileMeta {
|
export function createFileMeta(partial: Partial<FileMeta> = {}): FileMeta {
|
||||||
@@ -41,6 +43,7 @@ export function createFileMeta(partial: Partial<FileMeta> = {}): FileMeta {
|
|||||||
isHub: false,
|
isHub: false,
|
||||||
isEntryPoint: false,
|
isEntryPoint: false,
|
||||||
fileType: "unknown",
|
fileType: "unknown",
|
||||||
|
impactScore: 0,
|
||||||
...partial,
|
...partial,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -48,3 +51,20 @@ export function createFileMeta(partial: Partial<FileMeta> = {}): FileMeta {
|
|||||||
export function isHubFile(dependentCount: number): boolean {
|
export function isHubFile(dependentCount: number): boolean {
|
||||||
return dependentCount > 5
|
return dependentCount > 5
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate impact score based on number of dependents and total files.
|
||||||
|
* Impact score represents what percentage of the codebase depends on this file.
|
||||||
|
* @param dependentCount - Number of files that depend on this file
|
||||||
|
* @param totalFiles - Total number of files in the project
|
||||||
|
* @returns Impact score from 0 to 100
|
||||||
|
*/
|
||||||
|
export function calculateImpactScore(dependentCount: number, totalFiles: number): number {
|
||||||
|
if (totalFiles <= 1) {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
// Exclude the file itself from the total
|
||||||
|
const maxPossibleDependents = totalFiles - 1
|
||||||
|
const score = (dependentCount / maxPossibleDependents) * 100
|
||||||
|
return Math.round(Math.min(100, score))
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import * as path from "node:path"
|
import * as path from "node:path"
|
||||||
import {
|
import {
|
||||||
|
calculateImpactScore,
|
||||||
type ComplexityMetrics,
|
type ComplexityMetrics,
|
||||||
createFileMeta,
|
createFileMeta,
|
||||||
type FileMeta,
|
type FileMeta,
|
||||||
@@ -430,6 +431,7 @@ export class MetaAnalyzer {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Batch analyze multiple files.
|
* Batch analyze multiple files.
|
||||||
|
* Computes impact scores after all files are analyzed.
|
||||||
*/
|
*/
|
||||||
analyzeAll(files: Map<string, { ast: FileAST; content: string }>): Map<string, FileMeta> {
|
analyzeAll(files: Map<string, { ast: FileAST; content: string }>): Map<string, FileMeta> {
|
||||||
const allASTs = new Map<string, FileAST>()
|
const allASTs = new Map<string, FileAST>()
|
||||||
@@ -443,6 +445,12 @@ export class MetaAnalyzer {
|
|||||||
results.set(filePath, meta)
|
results.set(filePath, meta)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Compute impact scores now that we know total file count
|
||||||
|
const totalFiles = results.size
|
||||||
|
for (const [, meta] of results) {
|
||||||
|
meta.impactScore = calculateImpactScore(meta.dependents.length, totalFiles)
|
||||||
|
}
|
||||||
|
|
||||||
return results
|
return results
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,6 +16,10 @@ export interface ProjectStructure {
|
|||||||
*/
|
*/
|
||||||
export interface BuildContextOptions {
|
export interface BuildContextOptions {
|
||||||
includeSignatures?: boolean
|
includeSignatures?: boolean
|
||||||
|
includeDepsGraph?: boolean
|
||||||
|
includeCircularDeps?: boolean
|
||||||
|
includeHighImpactFiles?: boolean
|
||||||
|
circularDeps?: string[][]
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -127,11 +131,35 @@ export function buildInitialContext(
|
|||||||
): string {
|
): string {
|
||||||
const sections: string[] = []
|
const sections: string[] = []
|
||||||
const includeSignatures = options?.includeSignatures ?? true
|
const includeSignatures = options?.includeSignatures ?? true
|
||||||
|
const includeDepsGraph = options?.includeDepsGraph ?? true
|
||||||
|
const includeCircularDeps = options?.includeCircularDeps ?? true
|
||||||
|
const includeHighImpactFiles = options?.includeHighImpactFiles ?? true
|
||||||
|
|
||||||
sections.push(formatProjectHeader(structure))
|
sections.push(formatProjectHeader(structure))
|
||||||
sections.push(formatDirectoryTree(structure))
|
sections.push(formatDirectoryTree(structure))
|
||||||
sections.push(formatFileOverview(asts, metas, includeSignatures))
|
sections.push(formatFileOverview(asts, metas, includeSignatures))
|
||||||
|
|
||||||
|
if (includeDepsGraph && metas && metas.size > 0) {
|
||||||
|
const depsGraph = formatDependencyGraph(metas)
|
||||||
|
if (depsGraph) {
|
||||||
|
sections.push(depsGraph)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (includeHighImpactFiles && metas && metas.size > 0) {
|
||||||
|
const highImpactSection = formatHighImpactFiles(metas)
|
||||||
|
if (highImpactSection) {
|
||||||
|
sections.push(highImpactSection)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (includeCircularDeps && options?.circularDeps && options.circularDeps.length > 0) {
|
||||||
|
const circularDepsSection = formatCircularDeps(options.circularDeps)
|
||||||
|
if (circularDepsSection) {
|
||||||
|
sections.push(circularDepsSection)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return sections.join("\n\n")
|
return sections.join("\n\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -414,6 +442,209 @@ function formatFileFlags(meta?: FileMeta): string {
|
|||||||
return flags.length > 0 ? ` (${flags.join(", ")})` : ""
|
return flags.length > 0 ? ` (${flags.join(", ")})` : ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shorten a file path for display in dependency graph.
|
||||||
|
* Removes common prefixes like "src/" and file extensions.
|
||||||
|
*/
|
||||||
|
function shortenPath(path: string): string {
|
||||||
|
let short = path
|
||||||
|
if (short.startsWith("src/")) {
|
||||||
|
short = short.slice(4)
|
||||||
|
}
|
||||||
|
// Remove common extensions
|
||||||
|
short = short.replace(/\.(ts|tsx|js|jsx)$/, "")
|
||||||
|
// Remove /index suffix
|
||||||
|
short = short.replace(/\/index$/, "")
|
||||||
|
return short
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format a single dependency graph entry.
|
||||||
|
* Format: "path: → dep1, dep2 ← dependent1, dependent2"
|
||||||
|
*/
|
||||||
|
function formatDepsEntry(path: string, dependencies: string[], dependents: string[]): string {
|
||||||
|
const parts: string[] = []
|
||||||
|
const shortPath = shortenPath(path)
|
||||||
|
|
||||||
|
if (dependencies.length > 0) {
|
||||||
|
const deps = dependencies.map(shortenPath).join(", ")
|
||||||
|
parts.push(`→ ${deps}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dependents.length > 0) {
|
||||||
|
const deps = dependents.map(shortenPath).join(", ")
|
||||||
|
parts.push(`← ${deps}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parts.length === 0) {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${shortPath}: ${parts.join(" ")}`
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format dependency graph for all files.
|
||||||
|
* Shows hub files first, then files with dependencies/dependents.
|
||||||
|
*
|
||||||
|
* Format:
|
||||||
|
* ## Dependency Graph
|
||||||
|
* services/user: → types/user, utils/validation ← controllers/user
|
||||||
|
* services/auth: → services/user, utils/jwt ← controllers/auth
|
||||||
|
*/
|
||||||
|
export function formatDependencyGraph(metas: Map<string, FileMeta>): string | null {
|
||||||
|
if (metas.size === 0) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const entries: { path: string; deps: string[]; dependents: string[]; isHub: boolean }[] = []
|
||||||
|
|
||||||
|
for (const [path, meta] of metas) {
|
||||||
|
// Only include files that have connections
|
||||||
|
if (meta.dependencies.length > 0 || meta.dependents.length > 0) {
|
||||||
|
entries.push({
|
||||||
|
path,
|
||||||
|
deps: meta.dependencies,
|
||||||
|
dependents: meta.dependents,
|
||||||
|
isHub: meta.isHub,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (entries.length === 0) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort: hubs first, then by total connections (desc), then by path
|
||||||
|
entries.sort((a, b) => {
|
||||||
|
if (a.isHub !== b.isHub) {
|
||||||
|
return a.isHub ? -1 : 1
|
||||||
|
}
|
||||||
|
const aTotal = a.deps.length + a.dependents.length
|
||||||
|
const bTotal = b.deps.length + b.dependents.length
|
||||||
|
if (aTotal !== bTotal) {
|
||||||
|
return bTotal - aTotal
|
||||||
|
}
|
||||||
|
return a.path.localeCompare(b.path)
|
||||||
|
})
|
||||||
|
|
||||||
|
const lines: string[] = ["## Dependency Graph", ""]
|
||||||
|
|
||||||
|
for (const entry of entries) {
|
||||||
|
const line = formatDepsEntry(entry.path, entry.deps, entry.dependents)
|
||||||
|
if (line) {
|
||||||
|
lines.push(line)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return null if only header (no actual entries)
|
||||||
|
if (lines.length <= 2) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return lines.join("\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format circular dependencies for display in context.
|
||||||
|
* Shows warning section with cycle chains.
|
||||||
|
*
|
||||||
|
* Format:
|
||||||
|
* ## ⚠️ Circular Dependencies
|
||||||
|
* - services/user → services/auth → services/user
|
||||||
|
* - utils/a → utils/b → utils/c → utils/a
|
||||||
|
*/
|
||||||
|
export function formatCircularDeps(cycles: string[][]): string | null {
|
||||||
|
if (!cycles || cycles.length === 0) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const lines: string[] = ["## ⚠️ Circular Dependencies", ""]
|
||||||
|
|
||||||
|
for (const cycle of cycles) {
|
||||||
|
if (cycle.length === 0) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
const formattedCycle = cycle.map(shortenPath).join(" → ")
|
||||||
|
lines.push(`- ${formattedCycle}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return null if only header (no actual cycles)
|
||||||
|
if (lines.length <= 2) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return lines.join("\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format high impact files table for display in context.
|
||||||
|
* Shows files with highest impact scores (most dependents).
|
||||||
|
*
|
||||||
|
* Format:
|
||||||
|
* ## High Impact Files
|
||||||
|
* | File | Impact | Dependents |
|
||||||
|
* |------|--------|------------|
|
||||||
|
* | src/utils/validation.ts | 67% | 12 files |
|
||||||
|
*
|
||||||
|
* @param metas - Map of file paths to their metadata
|
||||||
|
* @param limit - Maximum number of files to show (default: 10)
|
||||||
|
* @param minImpact - Minimum impact score to include (default: 5)
|
||||||
|
*/
|
||||||
|
export function formatHighImpactFiles(
|
||||||
|
metas: Map<string, FileMeta>,
|
||||||
|
limit = 10,
|
||||||
|
minImpact = 5,
|
||||||
|
): string | null {
|
||||||
|
if (metas.size === 0) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
// Collect files with impact score >= minImpact
|
||||||
|
const impactFiles: { path: string; impact: number; dependents: number }[] = []
|
||||||
|
|
||||||
|
for (const [path, meta] of metas) {
|
||||||
|
if (meta.impactScore >= minImpact) {
|
||||||
|
impactFiles.push({
|
||||||
|
path,
|
||||||
|
impact: meta.impactScore,
|
||||||
|
dependents: meta.dependents.length,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (impactFiles.length === 0) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort by impact score descending, then by path
|
||||||
|
impactFiles.sort((a, b) => {
|
||||||
|
if (a.impact !== b.impact) {
|
||||||
|
return b.impact - a.impact
|
||||||
|
}
|
||||||
|
return a.path.localeCompare(b.path)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Take top N files
|
||||||
|
const topFiles = impactFiles.slice(0, limit)
|
||||||
|
|
||||||
|
const lines: string[] = [
|
||||||
|
"## High Impact Files",
|
||||||
|
"",
|
||||||
|
"| File | Impact | Dependents |",
|
||||||
|
"|------|--------|------------|",
|
||||||
|
]
|
||||||
|
|
||||||
|
for (const file of topFiles) {
|
||||||
|
const shortPath = shortenPath(file.path)
|
||||||
|
const impact = `${String(file.impact)}%`
|
||||||
|
const dependents = file.dependents === 1 ? "1 file" : `${String(file.dependents)} files`
|
||||||
|
lines.push(`| ${shortPath} | ${impact} | ${dependents} |`)
|
||||||
|
}
|
||||||
|
|
||||||
|
return lines.join("\n")
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Format line range for display.
|
* Format line range for display.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -115,6 +115,9 @@ export const ContextConfigSchema = z.object({
|
|||||||
autoCompressAt: z.number().min(0).max(1).default(0.8),
|
autoCompressAt: z.number().min(0).max(1).default(0.8),
|
||||||
compressionMethod: z.enum(["llm-summary", "truncate"]).default("llm-summary"),
|
compressionMethod: z.enum(["llm-summary", "truncate"]).default("llm-summary"),
|
||||||
includeSignatures: z.boolean().default(true),
|
includeSignatures: z.boolean().default(true),
|
||||||
|
includeDepsGraph: z.boolean().default(true),
|
||||||
|
includeCircularDeps: z.boolean().default(true),
|
||||||
|
includeHighImpactFiles: z.boolean().default(true),
|
||||||
})
|
})
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
import { describe, it, expect } from "vitest"
|
import { describe, it, expect } from "vitest"
|
||||||
import { createFileMeta, isHubFile } from "../../../../src/domain/value-objects/FileMeta.js"
|
import {
|
||||||
|
calculateImpactScore,
|
||||||
|
createFileMeta,
|
||||||
|
isHubFile,
|
||||||
|
} from "../../../../src/domain/value-objects/FileMeta.js"
|
||||||
|
|
||||||
describe("FileMeta", () => {
|
describe("FileMeta", () => {
|
||||||
describe("createFileMeta", () => {
|
describe("createFileMeta", () => {
|
||||||
@@ -15,6 +19,7 @@ describe("FileMeta", () => {
|
|||||||
expect(meta.isHub).toBe(false)
|
expect(meta.isHub).toBe(false)
|
||||||
expect(meta.isEntryPoint).toBe(false)
|
expect(meta.isEntryPoint).toBe(false)
|
||||||
expect(meta.fileType).toBe("unknown")
|
expect(meta.fileType).toBe("unknown")
|
||||||
|
expect(meta.impactScore).toBe(0)
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should merge partial values", () => {
|
it("should merge partial values", () => {
|
||||||
@@ -42,4 +47,51 @@ describe("FileMeta", () => {
|
|||||||
expect(isHubFile(0)).toBe(false)
|
expect(isHubFile(0)).toBe(false)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe("calculateImpactScore", () => {
|
||||||
|
it("should return 0 for file with 0 dependents", () => {
|
||||||
|
expect(calculateImpactScore(0, 10)).toBe(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should return 0 when totalFiles is 0", () => {
|
||||||
|
expect(calculateImpactScore(5, 0)).toBe(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should return 0 when totalFiles is 1", () => {
|
||||||
|
expect(calculateImpactScore(0, 1)).toBe(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should calculate correct percentage", () => {
|
||||||
|
// 5 dependents out of 10 files (excluding itself = 9 possible)
|
||||||
|
// 5/9 * 100 = 55.56 → rounded to 56
|
||||||
|
expect(calculateImpactScore(5, 10)).toBe(56)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should return 100 when all other files depend on it", () => {
|
||||||
|
// 9 dependents out of 10 files (9 possible dependents)
|
||||||
|
expect(calculateImpactScore(9, 10)).toBe(100)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should cap at 100", () => {
|
||||||
|
// Edge case: more dependents than possible (shouldn't happen normally)
|
||||||
|
expect(calculateImpactScore(20, 10)).toBe(100)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should round the percentage", () => {
|
||||||
|
// 1 dependent out of 3 files (2 possible)
|
||||||
|
// 1/2 * 100 = 50
|
||||||
|
expect(calculateImpactScore(1, 3)).toBe(50)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should calculate impact for small projects", () => {
|
||||||
|
// 1 dependent out of 2 files (1 possible)
|
||||||
|
expect(calculateImpactScore(1, 2)).toBe(100)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should calculate impact for larger projects", () => {
|
||||||
|
// 50 dependents out of 100 files (99 possible)
|
||||||
|
// 50/99 * 100 = 50.51 → rounded to 51
|
||||||
|
expect(calculateImpactScore(50, 100)).toBe(51)
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -4,6 +4,9 @@ import {
|
|||||||
buildInitialContext,
|
buildInitialContext,
|
||||||
buildFileContext,
|
buildFileContext,
|
||||||
truncateContext,
|
truncateContext,
|
||||||
|
formatDependencyGraph,
|
||||||
|
formatCircularDeps,
|
||||||
|
formatHighImpactFiles,
|
||||||
type ProjectStructure,
|
type ProjectStructure,
|
||||||
} from "../../../../src/infrastructure/llm/prompts.js"
|
} from "../../../../src/infrastructure/llm/prompts.js"
|
||||||
import type { FileAST } from "../../../../src/domain/value-objects/FileAST.js"
|
import type { FileAST } from "../../../../src/domain/value-objects/FileAST.js"
|
||||||
@@ -2013,4 +2016,948 @@ describe("prompts", () => {
|
|||||||
expect(context).not.toContain("@")
|
expect(context).not.toContain("@")
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe("dependency graph (0.27.0)", () => {
|
||||||
|
describe("formatDependencyGraph", () => {
|
||||||
|
it("should return null for empty metas", () => {
|
||||||
|
const metas = new Map<string, FileMeta>()
|
||||||
|
|
||||||
|
const result = formatDependencyGraph(metas)
|
||||||
|
|
||||||
|
expect(result).toBeNull()
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should return null when no files have dependencies or dependents", () => {
|
||||||
|
const metas = new Map<string, FileMeta>([
|
||||||
|
[
|
||||||
|
"src/isolated.ts",
|
||||||
|
{
|
||||||
|
complexity: { loc: 10, nesting: 1, cyclomaticComplexity: 1, score: 10 },
|
||||||
|
dependencies: [],
|
||||||
|
dependents: [],
|
||||||
|
isHub: false,
|
||||||
|
isEntryPoint: true,
|
||||||
|
fileType: "source",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
])
|
||||||
|
|
||||||
|
const result = formatDependencyGraph(metas)
|
||||||
|
|
||||||
|
expect(result).toBeNull()
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should format file with only dependencies", () => {
|
||||||
|
const metas = new Map<string, FileMeta>([
|
||||||
|
[
|
||||||
|
"src/services/user.ts",
|
||||||
|
{
|
||||||
|
complexity: { loc: 50, nesting: 2, cyclomaticComplexity: 5, score: 30 },
|
||||||
|
dependencies: ["src/types/user.ts", "src/utils/validation.ts"],
|
||||||
|
dependents: [],
|
||||||
|
isHub: false,
|
||||||
|
isEntryPoint: false,
|
||||||
|
fileType: "source",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
])
|
||||||
|
|
||||||
|
const result = formatDependencyGraph(metas)
|
||||||
|
|
||||||
|
expect(result).toContain("## Dependency Graph")
|
||||||
|
expect(result).toContain("services/user: → types/user, utils/validation")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should format file with only dependents", () => {
|
||||||
|
const metas = new Map<string, FileMeta>([
|
||||||
|
[
|
||||||
|
"src/types/user.ts",
|
||||||
|
{
|
||||||
|
complexity: { loc: 20, nesting: 1, cyclomaticComplexity: 1, score: 10 },
|
||||||
|
dependencies: [],
|
||||||
|
dependents: ["src/services/user.ts", "src/controllers/user.ts"],
|
||||||
|
isHub: false,
|
||||||
|
isEntryPoint: false,
|
||||||
|
fileType: "types",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
])
|
||||||
|
|
||||||
|
const result = formatDependencyGraph(metas)
|
||||||
|
|
||||||
|
expect(result).toContain("## Dependency Graph")
|
||||||
|
expect(result).toContain("types/user: ← services/user, controllers/user")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should format file with both dependencies and dependents", () => {
|
||||||
|
const metas = new Map<string, FileMeta>([
|
||||||
|
[
|
||||||
|
"src/services/user.ts",
|
||||||
|
{
|
||||||
|
complexity: {
|
||||||
|
loc: 80,
|
||||||
|
nesting: 3,
|
||||||
|
cyclomaticComplexity: 10,
|
||||||
|
score: 50,
|
||||||
|
},
|
||||||
|
dependencies: ["src/types/user.ts", "src/utils/validation.ts"],
|
||||||
|
dependents: ["src/controllers/user.ts", "src/api/routes.ts"],
|
||||||
|
isHub: false,
|
||||||
|
isEntryPoint: false,
|
||||||
|
fileType: "source",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
])
|
||||||
|
|
||||||
|
const result = formatDependencyGraph(metas)
|
||||||
|
|
||||||
|
expect(result).toContain("## Dependency Graph")
|
||||||
|
expect(result).toContain(
|
||||||
|
"services/user: → types/user, utils/validation ← controllers/user, api/routes",
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should sort hub files first", () => {
|
||||||
|
const metas = new Map<string, FileMeta>([
|
||||||
|
[
|
||||||
|
"src/utils/helpers.ts",
|
||||||
|
{
|
||||||
|
complexity: { loc: 30, nesting: 1, cyclomaticComplexity: 3, score: 20 },
|
||||||
|
dependencies: [],
|
||||||
|
dependents: ["a.ts", "b.ts", "c.ts", "d.ts", "e.ts", "f.ts", "g.ts"],
|
||||||
|
isHub: true,
|
||||||
|
isEntryPoint: false,
|
||||||
|
fileType: "source",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"src/services/user.ts",
|
||||||
|
{
|
||||||
|
complexity: { loc: 50, nesting: 2, cyclomaticComplexity: 5, score: 30 },
|
||||||
|
dependencies: ["src/types/user.ts"],
|
||||||
|
dependents: ["src/controllers/user.ts"],
|
||||||
|
isHub: false,
|
||||||
|
isEntryPoint: false,
|
||||||
|
fileType: "source",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
])
|
||||||
|
|
||||||
|
const result = formatDependencyGraph(metas)
|
||||||
|
|
||||||
|
expect(result).not.toBeNull()
|
||||||
|
const lines = result!.split("\n")
|
||||||
|
const hubIndex = lines.findIndex((l) => l.includes("utils/helpers"))
|
||||||
|
const serviceIndex = lines.findIndex((l) => l.includes("services/user"))
|
||||||
|
expect(hubIndex).toBeLessThan(serviceIndex)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should sort by total connections (descending) for non-hubs", () => {
|
||||||
|
const metas = new Map<string, FileMeta>([
|
||||||
|
[
|
||||||
|
"src/a.ts",
|
||||||
|
{
|
||||||
|
complexity: { loc: 10, nesting: 1, cyclomaticComplexity: 1, score: 10 },
|
||||||
|
dependencies: ["x.ts"],
|
||||||
|
dependents: [],
|
||||||
|
isHub: false,
|
||||||
|
isEntryPoint: false,
|
||||||
|
fileType: "source",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"src/b.ts",
|
||||||
|
{
|
||||||
|
complexity: { loc: 10, nesting: 1, cyclomaticComplexity: 1, score: 10 },
|
||||||
|
dependencies: ["x.ts", "y.ts"],
|
||||||
|
dependents: ["z.ts"],
|
||||||
|
isHub: false,
|
||||||
|
isEntryPoint: false,
|
||||||
|
fileType: "source",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
])
|
||||||
|
|
||||||
|
const result = formatDependencyGraph(metas)
|
||||||
|
|
||||||
|
expect(result).not.toBeNull()
|
||||||
|
const lines = result!.split("\n")
|
||||||
|
const aIndex = lines.findIndex((l) => l.startsWith("a:"))
|
||||||
|
const bIndex = lines.findIndex((l) => l.startsWith("b:"))
|
||||||
|
expect(bIndex).toBeLessThan(aIndex)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should shorten src/ prefix", () => {
|
||||||
|
const metas = new Map<string, FileMeta>([
|
||||||
|
[
|
||||||
|
"src/index.ts",
|
||||||
|
{
|
||||||
|
complexity: { loc: 10, nesting: 1, cyclomaticComplexity: 1, score: 10 },
|
||||||
|
dependencies: ["src/utils/helpers.ts"],
|
||||||
|
dependents: [],
|
||||||
|
isHub: false,
|
||||||
|
isEntryPoint: true,
|
||||||
|
fileType: "source",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
])
|
||||||
|
|
||||||
|
const result = formatDependencyGraph(metas)
|
||||||
|
|
||||||
|
expect(result).toContain("index: → utils/helpers")
|
||||||
|
expect(result).not.toContain("src/")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should remove file extensions", () => {
|
||||||
|
const metas = new Map<string, FileMeta>([
|
||||||
|
[
|
||||||
|
"lib/utils.ts",
|
||||||
|
{
|
||||||
|
complexity: { loc: 10, nesting: 1, cyclomaticComplexity: 1, score: 10 },
|
||||||
|
dependencies: ["lib/helpers.tsx", "lib/types.js"],
|
||||||
|
dependents: [],
|
||||||
|
isHub: false,
|
||||||
|
isEntryPoint: false,
|
||||||
|
fileType: "source",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
])
|
||||||
|
|
||||||
|
const result = formatDependencyGraph(metas)
|
||||||
|
|
||||||
|
expect(result).toContain("lib/utils: → lib/helpers, lib/types")
|
||||||
|
expect(result).not.toContain(".ts")
|
||||||
|
expect(result).not.toContain(".tsx")
|
||||||
|
expect(result).not.toContain(".js")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should remove /index suffix", () => {
|
||||||
|
const metas = new Map<string, FileMeta>([
|
||||||
|
[
|
||||||
|
"src/components/index.ts",
|
||||||
|
{
|
||||||
|
complexity: { loc: 10, nesting: 1, cyclomaticComplexity: 1, score: 10 },
|
||||||
|
dependencies: ["src/utils/index.ts"],
|
||||||
|
dependents: [],
|
||||||
|
isHub: false,
|
||||||
|
isEntryPoint: true,
|
||||||
|
fileType: "source",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
])
|
||||||
|
|
||||||
|
const result = formatDependencyGraph(metas)
|
||||||
|
|
||||||
|
expect(result).toContain("components: → utils")
|
||||||
|
expect(result).not.toContain("/index")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should handle multiple files in graph", () => {
|
||||||
|
const metas = new Map<string, FileMeta>([
|
||||||
|
[
|
||||||
|
"src/services/user.ts",
|
||||||
|
{
|
||||||
|
complexity: { loc: 50, nesting: 2, cyclomaticComplexity: 5, score: 30 },
|
||||||
|
dependencies: ["src/types/user.ts"],
|
||||||
|
dependents: ["src/controllers/user.ts"],
|
||||||
|
isHub: false,
|
||||||
|
isEntryPoint: false,
|
||||||
|
fileType: "source",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"src/services/auth.ts",
|
||||||
|
{
|
||||||
|
complexity: { loc: 40, nesting: 2, cyclomaticComplexity: 4, score: 25 },
|
||||||
|
dependencies: ["src/services/user.ts", "src/utils/jwt.ts"],
|
||||||
|
dependents: ["src/controllers/auth.ts"],
|
||||||
|
isHub: false,
|
||||||
|
isEntryPoint: false,
|
||||||
|
fileType: "source",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
])
|
||||||
|
|
||||||
|
const result = formatDependencyGraph(metas)
|
||||||
|
|
||||||
|
expect(result).toContain("## Dependency Graph")
|
||||||
|
expect(result).toContain("services/user: → types/user ← controllers/user")
|
||||||
|
expect(result).toContain(
|
||||||
|
"services/auth: → services/user, utils/jwt ← controllers/auth",
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("buildInitialContext with includeDepsGraph", () => {
|
||||||
|
const structure: ProjectStructure = {
|
||||||
|
name: "test-project",
|
||||||
|
rootPath: "/test",
|
||||||
|
files: ["src/index.ts"],
|
||||||
|
directories: ["src"],
|
||||||
|
}
|
||||||
|
|
||||||
|
const asts = new Map<string, FileAST>([
|
||||||
|
[
|
||||||
|
"src/index.ts",
|
||||||
|
{
|
||||||
|
imports: [],
|
||||||
|
exports: [],
|
||||||
|
functions: [],
|
||||||
|
classes: [],
|
||||||
|
interfaces: [],
|
||||||
|
typeAliases: [],
|
||||||
|
parseError: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
])
|
||||||
|
|
||||||
|
it("should include dependency graph by default", () => {
|
||||||
|
const metas = new Map<string, FileMeta>([
|
||||||
|
[
|
||||||
|
"src/index.ts",
|
||||||
|
{
|
||||||
|
complexity: { loc: 10, nesting: 1, cyclomaticComplexity: 1, score: 10 },
|
||||||
|
dependencies: ["src/utils.ts"],
|
||||||
|
dependents: [],
|
||||||
|
isHub: false,
|
||||||
|
isEntryPoint: true,
|
||||||
|
fileType: "source",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
])
|
||||||
|
|
||||||
|
const context = buildInitialContext(structure, asts, metas)
|
||||||
|
|
||||||
|
expect(context).toContain("## Dependency Graph")
|
||||||
|
expect(context).toContain("index: → utils")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should exclude dependency graph when includeDepsGraph is false", () => {
|
||||||
|
const metas = new Map<string, FileMeta>([
|
||||||
|
[
|
||||||
|
"src/index.ts",
|
||||||
|
{
|
||||||
|
complexity: { loc: 10, nesting: 1, cyclomaticComplexity: 1, score: 10 },
|
||||||
|
dependencies: ["src/utils.ts"],
|
||||||
|
dependents: [],
|
||||||
|
isHub: false,
|
||||||
|
isEntryPoint: true,
|
||||||
|
fileType: "source",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
])
|
||||||
|
|
||||||
|
const context = buildInitialContext(structure, asts, metas, {
|
||||||
|
includeDepsGraph: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(context).not.toContain("## Dependency Graph")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should not include dependency graph when metas is undefined", () => {
|
||||||
|
const context = buildInitialContext(structure, asts, undefined, {
|
||||||
|
includeDepsGraph: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(context).not.toContain("## Dependency Graph")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should not include dependency graph when metas is empty", () => {
|
||||||
|
const emptyMetas = new Map<string, FileMeta>()
|
||||||
|
|
||||||
|
const context = buildInitialContext(structure, asts, emptyMetas, {
|
||||||
|
includeDepsGraph: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(context).not.toContain("## Dependency Graph")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should not include dependency graph when no files have connections", () => {
|
||||||
|
const metas = new Map<string, FileMeta>([
|
||||||
|
[
|
||||||
|
"src/index.ts",
|
||||||
|
{
|
||||||
|
complexity: { loc: 10, nesting: 1, cyclomaticComplexity: 1, score: 10 },
|
||||||
|
dependencies: [],
|
||||||
|
dependents: [],
|
||||||
|
isHub: false,
|
||||||
|
isEntryPoint: true,
|
||||||
|
fileType: "source",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
])
|
||||||
|
|
||||||
|
const context = buildInitialContext(structure, asts, metas, {
|
||||||
|
includeDepsGraph: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(context).not.toContain("## Dependency Graph")
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("high impact files (0.29.0)", () => {
|
||||||
|
describe("formatHighImpactFiles", () => {
|
||||||
|
it("should return null for empty metas", () => {
|
||||||
|
const metas = new Map<string, FileMeta>()
|
||||||
|
|
||||||
|
const result = formatHighImpactFiles(metas)
|
||||||
|
|
||||||
|
expect(result).toBeNull()
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should return null when no files have impact score >= minImpact", () => {
|
||||||
|
const metas = new Map<string, FileMeta>([
|
||||||
|
[
|
||||||
|
"src/low.ts",
|
||||||
|
{
|
||||||
|
complexity: { loc: 10, nesting: 1, cyclomaticComplexity: 1, score: 10 },
|
||||||
|
dependencies: [],
|
||||||
|
dependents: [],
|
||||||
|
isHub: false,
|
||||||
|
isEntryPoint: false,
|
||||||
|
fileType: "source",
|
||||||
|
impactScore: 2,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
])
|
||||||
|
|
||||||
|
const result = formatHighImpactFiles(metas)
|
||||||
|
|
||||||
|
expect(result).toBeNull()
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should format file with high impact score", () => {
|
||||||
|
const metas = new Map<string, FileMeta>([
|
||||||
|
[
|
||||||
|
"src/utils/validation.ts",
|
||||||
|
{
|
||||||
|
complexity: { loc: 50, nesting: 2, cyclomaticComplexity: 5, score: 30 },
|
||||||
|
dependencies: [],
|
||||||
|
dependents: [
|
||||||
|
"a.ts",
|
||||||
|
"b.ts",
|
||||||
|
"c.ts",
|
||||||
|
"d.ts",
|
||||||
|
"e.ts",
|
||||||
|
"f.ts",
|
||||||
|
"g.ts",
|
||||||
|
"h.ts",
|
||||||
|
"i.ts",
|
||||||
|
"j.ts",
|
||||||
|
"k.ts",
|
||||||
|
"l.ts",
|
||||||
|
],
|
||||||
|
isHub: true,
|
||||||
|
isEntryPoint: false,
|
||||||
|
fileType: "source",
|
||||||
|
impactScore: 67,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
])
|
||||||
|
|
||||||
|
const result = formatHighImpactFiles(metas)
|
||||||
|
|
||||||
|
expect(result).toContain("## High Impact Files")
|
||||||
|
expect(result).toContain("| File | Impact | Dependents |")
|
||||||
|
expect(result).toContain("| utils/validation | 67% | 12 files |")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should sort by impact score descending", () => {
|
||||||
|
const metas = new Map<string, FileMeta>([
|
||||||
|
[
|
||||||
|
"src/low.ts",
|
||||||
|
{
|
||||||
|
complexity: { loc: 10, nesting: 1, cyclomaticComplexity: 1, score: 10 },
|
||||||
|
dependencies: [],
|
||||||
|
dependents: ["a.ts"],
|
||||||
|
isHub: false,
|
||||||
|
isEntryPoint: false,
|
||||||
|
fileType: "source",
|
||||||
|
impactScore: 10,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"src/high.ts",
|
||||||
|
{
|
||||||
|
complexity: { loc: 20, nesting: 1, cyclomaticComplexity: 1, score: 10 },
|
||||||
|
dependencies: [],
|
||||||
|
dependents: ["a.ts", "b.ts", "c.ts", "d.ts", "e.ts"],
|
||||||
|
isHub: false,
|
||||||
|
isEntryPoint: false,
|
||||||
|
fileType: "source",
|
||||||
|
impactScore: 50,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
])
|
||||||
|
|
||||||
|
const result = formatHighImpactFiles(metas)
|
||||||
|
|
||||||
|
expect(result).not.toBeNull()
|
||||||
|
const lines = result!.split("\n")
|
||||||
|
const highIndex = lines.findIndex((l) => l.includes("high"))
|
||||||
|
const lowIndex = lines.findIndex((l) => l.includes("low"))
|
||||||
|
expect(highIndex).toBeLessThan(lowIndex)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should limit to top N files", () => {
|
||||||
|
const metas = new Map<string, FileMeta>()
|
||||||
|
for (let i = 0; i < 20; i++) {
|
||||||
|
metas.set(`src/file${String(i)}.ts`, {
|
||||||
|
complexity: { loc: 10, nesting: 1, cyclomaticComplexity: 1, score: 10 },
|
||||||
|
dependencies: [],
|
||||||
|
dependents: ["a.ts"],
|
||||||
|
isHub: false,
|
||||||
|
isEntryPoint: false,
|
||||||
|
fileType: "source",
|
||||||
|
impactScore: 10 + i,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = formatHighImpactFiles(metas, 5)
|
||||||
|
|
||||||
|
expect(result).not.toBeNull()
|
||||||
|
const dataLines = result!
|
||||||
|
.split("\n")
|
||||||
|
.filter((l) => l.startsWith("| ") && l.includes("%"))
|
||||||
|
expect(dataLines).toHaveLength(5)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should filter by minImpact", () => {
|
||||||
|
const metas = new Map<string, FileMeta>([
|
||||||
|
[
|
||||||
|
"src/high.ts",
|
||||||
|
{
|
||||||
|
complexity: { loc: 10, nesting: 1, cyclomaticComplexity: 1, score: 10 },
|
||||||
|
dependencies: [],
|
||||||
|
dependents: ["a.ts", "b.ts", "c.ts"],
|
||||||
|
isHub: false,
|
||||||
|
isEntryPoint: false,
|
||||||
|
fileType: "source",
|
||||||
|
impactScore: 30,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"src/low.ts",
|
||||||
|
{
|
||||||
|
complexity: { loc: 10, nesting: 1, cyclomaticComplexity: 1, score: 10 },
|
||||||
|
dependencies: [],
|
||||||
|
dependents: ["a.ts"],
|
||||||
|
isHub: false,
|
||||||
|
isEntryPoint: false,
|
||||||
|
fileType: "source",
|
||||||
|
impactScore: 5,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
])
|
||||||
|
|
||||||
|
const result = formatHighImpactFiles(metas, 10, 20)
|
||||||
|
|
||||||
|
expect(result).not.toBeNull()
|
||||||
|
expect(result).toContain("high")
|
||||||
|
expect(result).not.toContain("low")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should show singular 'file' for 1 dependent", () => {
|
||||||
|
const metas = new Map<string, FileMeta>([
|
||||||
|
[
|
||||||
|
"src/single.ts",
|
||||||
|
{
|
||||||
|
complexity: { loc: 10, nesting: 1, cyclomaticComplexity: 1, score: 10 },
|
||||||
|
dependencies: [],
|
||||||
|
dependents: ["a.ts"],
|
||||||
|
isHub: false,
|
||||||
|
isEntryPoint: false,
|
||||||
|
fileType: "source",
|
||||||
|
impactScore: 10,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
])
|
||||||
|
|
||||||
|
const result = formatHighImpactFiles(metas)
|
||||||
|
|
||||||
|
expect(result).toContain("1 file")
|
||||||
|
expect(result).not.toContain("1 files")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should shorten src/ prefix", () => {
|
||||||
|
const metas = new Map<string, FileMeta>([
|
||||||
|
[
|
||||||
|
"src/services/user.ts",
|
||||||
|
{
|
||||||
|
complexity: { loc: 10, nesting: 1, cyclomaticComplexity: 1, score: 10 },
|
||||||
|
dependencies: [],
|
||||||
|
dependents: ["a.ts", "b.ts"],
|
||||||
|
isHub: false,
|
||||||
|
isEntryPoint: false,
|
||||||
|
fileType: "source",
|
||||||
|
impactScore: 20,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
])
|
||||||
|
|
||||||
|
const result = formatHighImpactFiles(metas)
|
||||||
|
|
||||||
|
expect(result).toContain("services/user")
|
||||||
|
expect(result).not.toContain("src/")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should remove file extensions", () => {
|
||||||
|
const metas = new Map<string, FileMeta>([
|
||||||
|
[
|
||||||
|
"lib/utils.ts",
|
||||||
|
{
|
||||||
|
complexity: { loc: 10, nesting: 1, cyclomaticComplexity: 1, score: 10 },
|
||||||
|
dependencies: [],
|
||||||
|
dependents: ["a.ts", "b.ts"],
|
||||||
|
isHub: false,
|
||||||
|
isEntryPoint: false,
|
||||||
|
fileType: "source",
|
||||||
|
impactScore: 20,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
])
|
||||||
|
|
||||||
|
const result = formatHighImpactFiles(metas)
|
||||||
|
|
||||||
|
expect(result).toContain("lib/utils")
|
||||||
|
expect(result).not.toContain(".ts")
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("buildInitialContext with includeHighImpactFiles", () => {
|
||||||
|
const structure: ProjectStructure = {
|
||||||
|
name: "test-project",
|
||||||
|
rootPath: "/test",
|
||||||
|
files: ["src/index.ts"],
|
||||||
|
directories: ["src"],
|
||||||
|
}
|
||||||
|
|
||||||
|
const asts = new Map<string, FileAST>([
|
||||||
|
[
|
||||||
|
"src/index.ts",
|
||||||
|
{
|
||||||
|
imports: [],
|
||||||
|
exports: [],
|
||||||
|
functions: [],
|
||||||
|
classes: [],
|
||||||
|
interfaces: [],
|
||||||
|
typeAliases: [],
|
||||||
|
parseError: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
])
|
||||||
|
|
||||||
|
it("should include high impact files by default", () => {
|
||||||
|
const metas = new Map<string, FileMeta>([
|
||||||
|
[
|
||||||
|
"src/index.ts",
|
||||||
|
{
|
||||||
|
complexity: { loc: 10, nesting: 1, cyclomaticComplexity: 1, score: 10 },
|
||||||
|
dependencies: [],
|
||||||
|
dependents: ["a.ts", "b.ts"],
|
||||||
|
isHub: false,
|
||||||
|
isEntryPoint: true,
|
||||||
|
fileType: "source",
|
||||||
|
impactScore: 20,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
])
|
||||||
|
|
||||||
|
const context = buildInitialContext(structure, asts, metas)
|
||||||
|
|
||||||
|
expect(context).toContain("## High Impact Files")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should exclude high impact files when includeHighImpactFiles is false", () => {
|
||||||
|
const metas = new Map<string, FileMeta>([
|
||||||
|
[
|
||||||
|
"src/index.ts",
|
||||||
|
{
|
||||||
|
complexity: { loc: 10, nesting: 1, cyclomaticComplexity: 1, score: 10 },
|
||||||
|
dependencies: [],
|
||||||
|
dependents: ["a.ts", "b.ts"],
|
||||||
|
isHub: false,
|
||||||
|
isEntryPoint: true,
|
||||||
|
fileType: "source",
|
||||||
|
impactScore: 20,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
])
|
||||||
|
|
||||||
|
const context = buildInitialContext(structure, asts, metas, {
|
||||||
|
includeHighImpactFiles: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(context).not.toContain("## High Impact Files")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should not include high impact files when metas is undefined", () => {
|
||||||
|
const context = buildInitialContext(structure, asts, undefined, {
|
||||||
|
includeHighImpactFiles: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(context).not.toContain("## High Impact Files")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should not include high impact files when metas is empty", () => {
|
||||||
|
const emptyMetas = new Map<string, FileMeta>()
|
||||||
|
|
||||||
|
const context = buildInitialContext(structure, asts, emptyMetas, {
|
||||||
|
includeHighImpactFiles: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(context).not.toContain("## High Impact Files")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should not include high impact files when no files have high impact", () => {
|
||||||
|
const metas = new Map<string, FileMeta>([
|
||||||
|
[
|
||||||
|
"src/index.ts",
|
||||||
|
{
|
||||||
|
complexity: { loc: 10, nesting: 1, cyclomaticComplexity: 1, score: 10 },
|
||||||
|
dependencies: [],
|
||||||
|
dependents: [],
|
||||||
|
isHub: false,
|
||||||
|
isEntryPoint: true,
|
||||||
|
fileType: "source",
|
||||||
|
impactScore: 0,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
])
|
||||||
|
|
||||||
|
const context = buildInitialContext(structure, asts, metas, {
|
||||||
|
includeHighImpactFiles: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(context).not.toContain("## High Impact Files")
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("circular dependencies (0.28.0)", () => {
|
||||||
|
describe("formatCircularDeps", () => {
|
||||||
|
it("should return null for empty array", () => {
|
||||||
|
const result = formatCircularDeps([])
|
||||||
|
|
||||||
|
expect(result).toBeNull()
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should return null for undefined", () => {
|
||||||
|
const result = formatCircularDeps(undefined as unknown as string[][])
|
||||||
|
|
||||||
|
expect(result).toBeNull()
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should format a simple two-node cycle", () => {
|
||||||
|
const cycles = [["src/a.ts", "src/b.ts", "src/a.ts"]]
|
||||||
|
|
||||||
|
const result = formatCircularDeps(cycles)
|
||||||
|
|
||||||
|
expect(result).toContain("## ⚠️ Circular Dependencies")
|
||||||
|
expect(result).toContain("- a → b → a")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should format a three-node cycle", () => {
|
||||||
|
const cycles = [
|
||||||
|
["src/services/user.ts", "src/services/auth.ts", "src/services/user.ts"],
|
||||||
|
]
|
||||||
|
|
||||||
|
const result = formatCircularDeps(cycles)
|
||||||
|
|
||||||
|
expect(result).toContain("## ⚠️ Circular Dependencies")
|
||||||
|
expect(result).toContain("- services/user → services/auth → services/user")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should format multiple cycles", () => {
|
||||||
|
const cycles = [
|
||||||
|
["src/a.ts", "src/b.ts", "src/a.ts"],
|
||||||
|
["src/utils/x.ts", "src/utils/y.ts", "src/utils/z.ts", "src/utils/x.ts"],
|
||||||
|
]
|
||||||
|
|
||||||
|
const result = formatCircularDeps(cycles)
|
||||||
|
|
||||||
|
expect(result).toContain("## ⚠️ Circular Dependencies")
|
||||||
|
expect(result).toContain("- a → b → a")
|
||||||
|
expect(result).toContain("- utils/x → utils/y → utils/z → utils/x")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should shorten paths (remove src/ prefix)", () => {
|
||||||
|
const cycles = [
|
||||||
|
["src/services/user.ts", "src/types/user.ts", "src/services/user.ts"],
|
||||||
|
]
|
||||||
|
|
||||||
|
const result = formatCircularDeps(cycles)
|
||||||
|
|
||||||
|
expect(result).not.toContain("src/")
|
||||||
|
expect(result).toContain("services/user → types/user → services/user")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should remove file extensions", () => {
|
||||||
|
const cycles = [["lib/a.ts", "lib/b.tsx", "lib/c.js", "lib/a.ts"]]
|
||||||
|
|
||||||
|
const result = formatCircularDeps(cycles)
|
||||||
|
|
||||||
|
expect(result).not.toContain(".ts")
|
||||||
|
expect(result).not.toContain(".tsx")
|
||||||
|
expect(result).not.toContain(".js")
|
||||||
|
expect(result).toContain("lib/a → lib/b → lib/c → lib/a")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should remove /index suffix", () => {
|
||||||
|
const cycles = [
|
||||||
|
["src/components/index.ts", "src/utils/index.ts", "src/components/index.ts"],
|
||||||
|
]
|
||||||
|
|
||||||
|
const result = formatCircularDeps(cycles)
|
||||||
|
|
||||||
|
expect(result).not.toContain("/index")
|
||||||
|
expect(result).toContain("components → utils → components")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should skip empty cycles", () => {
|
||||||
|
const cycles = [[], ["src/a.ts", "src/b.ts", "src/a.ts"], []]
|
||||||
|
|
||||||
|
const result = formatCircularDeps(cycles)
|
||||||
|
|
||||||
|
expect(result).toContain("- a → b → a")
|
||||||
|
const lines = result!.split("\n").filter((l) => l.startsWith("- "))
|
||||||
|
expect(lines).toHaveLength(1)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should return null if all cycles are empty", () => {
|
||||||
|
const cycles = [[], [], []]
|
||||||
|
|
||||||
|
const result = formatCircularDeps(cycles)
|
||||||
|
|
||||||
|
expect(result).toBeNull()
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should format self-referencing cycle", () => {
|
||||||
|
const cycles = [["src/self.ts", "src/self.ts"]]
|
||||||
|
|
||||||
|
const result = formatCircularDeps(cycles)
|
||||||
|
|
||||||
|
expect(result).toContain("- self → self")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should handle long cycles", () => {
|
||||||
|
const cycles = [
|
||||||
|
[
|
||||||
|
"src/a.ts",
|
||||||
|
"src/b.ts",
|
||||||
|
"src/c.ts",
|
||||||
|
"src/d.ts",
|
||||||
|
"src/e.ts",
|
||||||
|
"src/f.ts",
|
||||||
|
"src/a.ts",
|
||||||
|
],
|
||||||
|
]
|
||||||
|
|
||||||
|
const result = formatCircularDeps(cycles)
|
||||||
|
|
||||||
|
expect(result).toContain("- a → b → c → d → e → f → a")
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("buildInitialContext with includeCircularDeps", () => {
|
||||||
|
const structure: ProjectStructure = {
|
||||||
|
name: "test-project",
|
||||||
|
rootPath: "/test",
|
||||||
|
files: ["src/index.ts"],
|
||||||
|
directories: ["src"],
|
||||||
|
}
|
||||||
|
|
||||||
|
const asts = new Map<string, FileAST>([
|
||||||
|
[
|
||||||
|
"src/index.ts",
|
||||||
|
{
|
||||||
|
imports: [],
|
||||||
|
exports: [],
|
||||||
|
functions: [],
|
||||||
|
classes: [],
|
||||||
|
interfaces: [],
|
||||||
|
typeAliases: [],
|
||||||
|
parseError: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
])
|
||||||
|
|
||||||
|
it("should include circular deps when circularDeps provided", () => {
|
||||||
|
const circularDeps = [["src/a.ts", "src/b.ts", "src/a.ts"]]
|
||||||
|
|
||||||
|
const context = buildInitialContext(structure, asts, undefined, {
|
||||||
|
circularDeps,
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(context).toContain("## ⚠️ Circular Dependencies")
|
||||||
|
expect(context).toContain("- a → b → a")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should not include circular deps when includeCircularDeps is false", () => {
|
||||||
|
const circularDeps = [["src/a.ts", "src/b.ts", "src/a.ts"]]
|
||||||
|
|
||||||
|
const context = buildInitialContext(structure, asts, undefined, {
|
||||||
|
circularDeps,
|
||||||
|
includeCircularDeps: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(context).not.toContain("## ⚠️ Circular Dependencies")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should not include circular deps when circularDeps is empty", () => {
|
||||||
|
const context = buildInitialContext(structure, asts, undefined, {
|
||||||
|
circularDeps: [],
|
||||||
|
includeCircularDeps: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(context).not.toContain("## ⚠️ Circular Dependencies")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should not include circular deps when circularDeps is undefined", () => {
|
||||||
|
const context = buildInitialContext(structure, asts, undefined, {
|
||||||
|
includeCircularDeps: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(context).not.toContain("## ⚠️ Circular Dependencies")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should include circular deps by default when circularDeps provided", () => {
|
||||||
|
const circularDeps = [["src/x.ts", "src/y.ts", "src/x.ts"]]
|
||||||
|
|
||||||
|
const context = buildInitialContext(structure, asts, undefined, {
|
||||||
|
circularDeps,
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(context).toContain("## ⚠️ Circular Dependencies")
|
||||||
|
expect(context).toContain("- x → y → x")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should include both dependency graph and circular deps", () => {
|
||||||
|
const metas = new Map<string, FileMeta>([
|
||||||
|
[
|
||||||
|
"src/index.ts",
|
||||||
|
{
|
||||||
|
complexity: { loc: 10, nesting: 1, cyclomaticComplexity: 1, score: 10 },
|
||||||
|
dependencies: ["src/utils.ts"],
|
||||||
|
dependents: [],
|
||||||
|
isHub: false,
|
||||||
|
isEntryPoint: true,
|
||||||
|
fileType: "source",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
])
|
||||||
|
const circularDeps = [["src/a.ts", "src/b.ts", "src/a.ts"]]
|
||||||
|
|
||||||
|
const context = buildInitialContext(structure, asts, metas, {
|
||||||
|
circularDeps,
|
||||||
|
includeDepsGraph: true,
|
||||||
|
includeCircularDeps: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(context).toContain("## Dependency Graph")
|
||||||
|
expect(context).toContain("## ⚠️ Circular Dependencies")
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -16,6 +16,9 @@ describe("ContextConfigSchema", () => {
|
|||||||
autoCompressAt: 0.8,
|
autoCompressAt: 0.8,
|
||||||
compressionMethod: "llm-summary",
|
compressionMethod: "llm-summary",
|
||||||
includeSignatures: true,
|
includeSignatures: true,
|
||||||
|
includeDepsGraph: true,
|
||||||
|
includeCircularDeps: true,
|
||||||
|
includeHighImpactFiles: true,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -28,6 +31,9 @@ describe("ContextConfigSchema", () => {
|
|||||||
autoCompressAt: 0.8,
|
autoCompressAt: 0.8,
|
||||||
compressionMethod: "llm-summary",
|
compressionMethod: "llm-summary",
|
||||||
includeSignatures: true,
|
includeSignatures: true,
|
||||||
|
includeDepsGraph: true,
|
||||||
|
includeCircularDeps: true,
|
||||||
|
includeHighImpactFiles: true,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@@ -165,6 +171,9 @@ describe("ContextConfigSchema", () => {
|
|||||||
autoCompressAt: 0.8,
|
autoCompressAt: 0.8,
|
||||||
compressionMethod: "llm-summary",
|
compressionMethod: "llm-summary",
|
||||||
includeSignatures: true,
|
includeSignatures: true,
|
||||||
|
includeDepsGraph: true,
|
||||||
|
includeCircularDeps: true,
|
||||||
|
includeHighImpactFiles: true,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -179,6 +188,9 @@ describe("ContextConfigSchema", () => {
|
|||||||
autoCompressAt: 0.9,
|
autoCompressAt: 0.9,
|
||||||
compressionMethod: "llm-summary",
|
compressionMethod: "llm-summary",
|
||||||
includeSignatures: true,
|
includeSignatures: true,
|
||||||
|
includeDepsGraph: true,
|
||||||
|
includeCircularDeps: true,
|
||||||
|
includeHighImpactFiles: true,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -194,6 +206,9 @@ describe("ContextConfigSchema", () => {
|
|||||||
autoCompressAt: 0.8,
|
autoCompressAt: 0.8,
|
||||||
compressionMethod: "truncate",
|
compressionMethod: "truncate",
|
||||||
includeSignatures: true,
|
includeSignatures: true,
|
||||||
|
includeDepsGraph: true,
|
||||||
|
includeCircularDeps: true,
|
||||||
|
includeHighImpactFiles: true,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@@ -206,6 +221,9 @@ describe("ContextConfigSchema", () => {
|
|||||||
autoCompressAt: 0.85,
|
autoCompressAt: 0.85,
|
||||||
compressionMethod: "truncate" as const,
|
compressionMethod: "truncate" as const,
|
||||||
includeSignatures: false,
|
includeSignatures: false,
|
||||||
|
includeDepsGraph: false,
|
||||||
|
includeCircularDeps: false,
|
||||||
|
includeHighImpactFiles: false,
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = ContextConfigSchema.parse(config)
|
const result = ContextConfigSchema.parse(config)
|
||||||
@@ -219,6 +237,9 @@ describe("ContextConfigSchema", () => {
|
|||||||
autoCompressAt: 0.8,
|
autoCompressAt: 0.8,
|
||||||
compressionMethod: "llm-summary" as const,
|
compressionMethod: "llm-summary" as const,
|
||||||
includeSignatures: true,
|
includeSignatures: true,
|
||||||
|
includeDepsGraph: true,
|
||||||
|
includeCircularDeps: true,
|
||||||
|
includeHighImpactFiles: true,
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = ContextConfigSchema.parse(config)
|
const result = ContextConfigSchema.parse(config)
|
||||||
@@ -250,4 +271,79 @@ describe("ContextConfigSchema", () => {
|
|||||||
expect(() => ContextConfigSchema.parse({ includeSignatures: 1 })).toThrow()
|
expect(() => ContextConfigSchema.parse({ includeSignatures: 1 })).toThrow()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe("includeDepsGraph", () => {
|
||||||
|
it("should accept true", () => {
|
||||||
|
const result = ContextConfigSchema.parse({ includeDepsGraph: true })
|
||||||
|
expect(result.includeDepsGraph).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should accept false", () => {
|
||||||
|
const result = ContextConfigSchema.parse({ includeDepsGraph: false })
|
||||||
|
expect(result.includeDepsGraph).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should default to true", () => {
|
||||||
|
const result = ContextConfigSchema.parse({})
|
||||||
|
expect(result.includeDepsGraph).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should reject non-boolean", () => {
|
||||||
|
expect(() => ContextConfigSchema.parse({ includeDepsGraph: "true" })).toThrow()
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should reject number", () => {
|
||||||
|
expect(() => ContextConfigSchema.parse({ includeDepsGraph: 1 })).toThrow()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("includeCircularDeps", () => {
|
||||||
|
it("should accept true", () => {
|
||||||
|
const result = ContextConfigSchema.parse({ includeCircularDeps: true })
|
||||||
|
expect(result.includeCircularDeps).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should accept false", () => {
|
||||||
|
const result = ContextConfigSchema.parse({ includeCircularDeps: false })
|
||||||
|
expect(result.includeCircularDeps).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should default to true", () => {
|
||||||
|
const result = ContextConfigSchema.parse({})
|
||||||
|
expect(result.includeCircularDeps).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should reject non-boolean", () => {
|
||||||
|
expect(() => ContextConfigSchema.parse({ includeCircularDeps: "true" })).toThrow()
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should reject number", () => {
|
||||||
|
expect(() => ContextConfigSchema.parse({ includeCircularDeps: 1 })).toThrow()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("includeHighImpactFiles", () => {
|
||||||
|
it("should accept true", () => {
|
||||||
|
const result = ContextConfigSchema.parse({ includeHighImpactFiles: true })
|
||||||
|
expect(result.includeHighImpactFiles).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should accept false", () => {
|
||||||
|
const result = ContextConfigSchema.parse({ includeHighImpactFiles: false })
|
||||||
|
expect(result.includeHighImpactFiles).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should default to true", () => {
|
||||||
|
const result = ContextConfigSchema.parse({})
|
||||||
|
expect(result.includeHighImpactFiles).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should reject non-boolean", () => {
|
||||||
|
expect(() => ContextConfigSchema.parse({ includeHighImpactFiles: "true" })).toThrow()
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should reject number", () => {
|
||||||
|
expect(() => ContextConfigSchema.parse({ includeHighImpactFiles: 1 })).toThrow()
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user