mirror of
https://github.com/samiyev/puaros.git
synced 2025-12-27 23:06:54 +05:00
Compare commits
4 Commits
ipuaro-v0.
...
ipuaro-v0.
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
17d75dbd54 | ||
|
|
fac5966678 | ||
|
|
92ba3fd9ba | ||
|
|
e9aaa708fe |
@@ -5,6 +5,104 @@ 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/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [0.30.0] - 2025-12-05 - Transitive Dependencies Count
|
||||
|
||||
### Added
|
||||
|
||||
- **Transitive Dependency Counts in FileMeta (v0.30.0)**
|
||||
- New `transitiveDepCount: number` field - count of files that depend on this file transitively
|
||||
- New `transitiveDepByCount: number` field - count of files this file depends on transitively
|
||||
- Includes both direct and indirect dependencies/dependents
|
||||
- Excludes the file itself from counts (handles circular dependencies)
|
||||
|
||||
- **Transitive Dependency Computation in MetaAnalyzer**
|
||||
- New `computeTransitiveCounts()` method - computes transitive counts for all files
|
||||
- New `getTransitiveDependents()` method - DFS with cycle detection for dependents
|
||||
- New `getTransitiveDependencies()` method - DFS with cycle detection for dependencies
|
||||
- Top-level caching for efficiency (avoids re-computing for each file)
|
||||
- Graceful handling of circular dependencies
|
||||
|
||||
### Technical Details
|
||||
|
||||
- Total tests: 1840 passed (was 1826, +14 new tests)
|
||||
- 9 new tests for computeTransitiveCounts()
|
||||
- 2 new tests for getTransitiveDependents()
|
||||
- 2 new tests for getTransitiveDependencies()
|
||||
- 1 new test for analyzeAll with transitive counts
|
||||
- Coverage: 97.58% lines, 91.5% branches, 98.64% functions
|
||||
- 0 ESLint errors, 3 warnings (pre-existing complexity)
|
||||
- Build successful
|
||||
|
||||
### Notes
|
||||
|
||||
This completes v0.30.0 - the final feature milestone before v1.0.0:
|
||||
- ✅ 0.27.0 - Inline Dependency Graph
|
||||
- ✅ 0.28.0 - Circular Dependencies in Context
|
||||
- ✅ 0.29.0 - Impact Score
|
||||
- ✅ 0.30.0 - Transitive Dependencies Count
|
||||
|
||||
Next milestone: v1.0.0 - Production Ready
|
||||
|
||||
---
|
||||
|
||||
## [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
|
||||
|
||||
@@ -1950,10 +1950,10 @@ Enhance initial context for LLM: add function signatures, interface field types,
|
||||
|
||||
---
|
||||
|
||||
## Version 0.29.0 - Impact Score 📈
|
||||
## Version 0.29.0 - Impact Score 📈 ✅
|
||||
|
||||
**Priority:** MEDIUM
|
||||
**Status:** Planned
|
||||
**Status:** Complete (v0.29.0 released)
|
||||
|
||||
### Description
|
||||
|
||||
@@ -1972,19 +1972,25 @@ Enhance initial context for LLM: add function signatures, interface field types,
|
||||
```
|
||||
|
||||
**Changes:**
|
||||
- [ ] Add `impactScore: number` to FileMeta (0-100)
|
||||
- [ ] Compute in MetaAnalyzer: (transitiveDepByCount / totalFiles) * 100
|
||||
- [ ] Add `formatHighImpactFiles()` to prompts.ts
|
||||
- [ ] Show top-10 high impact files
|
||||
- [x] Add `impactScore: number` to FileMeta (0-100)
|
||||
- [x] Compute in MetaAnalyzer: (dependents.length / (totalFiles - 1)) * 100
|
||||
- [x] Add `formatHighImpactFiles()` to prompts.ts
|
||||
- [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.
|
||||
|
||||
---
|
||||
|
||||
## Version 0.30.0 - Transitive Dependencies Count 🔢
|
||||
## Version 0.30.0 - Transitive Dependencies Count 🔢 ✅
|
||||
|
||||
**Priority:** MEDIUM
|
||||
**Status:** Planned
|
||||
**Status:** Complete (v0.30.0 released)
|
||||
|
||||
### Description
|
||||
|
||||
@@ -2001,13 +2007,19 @@ interface FileMeta {
|
||||
```
|
||||
|
||||
**Changes:**
|
||||
- [ ] Add `computeTransitiveDeps()` to MetaAnalyzer
|
||||
- [ ] Use DFS with memoization for efficiency
|
||||
- [ ] Store in FileMeta
|
||||
- [x] Add `transitiveDepCount` and `transitiveDepByCount` to FileMeta
|
||||
- [x] Add `computeTransitiveCounts()` to MetaAnalyzer
|
||||
- [x] Add `getTransitiveDependents()` with DFS and cycle detection
|
||||
- [x] Add `getTransitiveDependencies()` with DFS and cycle detection
|
||||
- [x] Use top-level caching for efficiency
|
||||
- [x] Handle circular dependencies gracefully (exclude self from count)
|
||||
|
||||
**Tests:**
|
||||
- [ ] Unit tests for transitive dependencies computation
|
||||
- [ ] Performance tests for large codebases
|
||||
- [x] Unit tests for transitive dependencies computation (14 tests)
|
||||
- [x] Tests for circular dependencies
|
||||
- [x] Tests for diamond dependency patterns
|
||||
- [x] Tests for deep dependency chains
|
||||
- [x] Cache behavior tests
|
||||
|
||||
---
|
||||
|
||||
@@ -2022,12 +2034,12 @@ interface FileMeta {
|
||||
- [x] Error handling complete ✅ (v0.16.0)
|
||||
- [ ] Performance optimized
|
||||
- [x] Documentation complete ✅ (v0.17.0)
|
||||
- [x] Test coverage ≥91% branches, ≥95% lines/functions/statements ✅ (91.13% branches, 97.48% lines, 98.63% functions, 97.48% statements - 1798 tests)
|
||||
- [x] Test coverage ≥91% branches, ≥95% lines/functions/statements ✅ (91.5% branches, 97.58% lines, 98.64% functions, 97.58% statements - 1840 tests)
|
||||
- [x] 0 ESLint errors ✅
|
||||
- [x] Examples working ✅ (v0.18.0)
|
||||
- [x] CHANGELOG.md up to date ✅
|
||||
- [x] Rich initial context (v0.24.0-v0.26.0) — function signatures, interface fields, enum values, decorators ✅
|
||||
- [ ] Graph metrics in context (v0.27.0-v0.30.0) — dependency graph ✅, circular deps ✅, impact score, transitive deps
|
||||
- [x] Graph metrics in context (v0.27.0-v0.30.0) — dependency graph ✅, circular deps ✅, impact score ✅, transitive deps ✅
|
||||
|
||||
---
|
||||
|
||||
@@ -2106,7 +2118,7 @@ sessions:list # List<session_id>
|
||||
|
||||
**Last Updated:** 2025-12-05
|
||||
**Target Version:** 1.0.0
|
||||
**Current Version:** 0.28.0
|
||||
**Next Milestones:** v0.29.0 (Impact Score), v0.30.0 (Transitive Deps)
|
||||
**Current Version:** 0.30.0
|
||||
**Next Milestones:** v1.0.0 (Production Ready)
|
||||
|
||||
> **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.
|
||||
> **Note:** Rich Initial Context complete ✅ (v0.24.0-v0.26.0). Graph Metrics complete ✅ (v0.27.0-v0.30.0). All feature milestones done, ready for v1.0.0 stabilization.
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@samiyev/ipuaro",
|
||||
"version": "0.28.0",
|
||||
"version": "0.30.0",
|
||||
"description": "Local AI agent for codebase operations with infinite context feeling",
|
||||
"author": "Fozilbek Samiyev <fozilbek.samiyev@gmail.com>",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -26,6 +26,12 @@ export interface FileMeta {
|
||||
isEntryPoint: boolean
|
||||
/** File type classification */
|
||||
fileType: "source" | "test" | "config" | "types" | "unknown"
|
||||
/** Impact score (0-100): percentage of codebase that depends on this file */
|
||||
impactScore: number
|
||||
/** Count of files that depend on this file transitively (including indirect dependents) */
|
||||
transitiveDepCount: number
|
||||
/** Count of files this file depends on transitively (including indirect dependencies) */
|
||||
transitiveDepByCount: number
|
||||
}
|
||||
|
||||
export function createFileMeta(partial: Partial<FileMeta> = {}): FileMeta {
|
||||
@@ -41,6 +47,9 @@ export function createFileMeta(partial: Partial<FileMeta> = {}): FileMeta {
|
||||
isHub: false,
|
||||
isEntryPoint: false,
|
||||
fileType: "unknown",
|
||||
impactScore: 0,
|
||||
transitiveDepCount: 0,
|
||||
transitiveDepByCount: 0,
|
||||
...partial,
|
||||
}
|
||||
}
|
||||
@@ -48,3 +57,20 @@ export function createFileMeta(partial: Partial<FileMeta> = {}): FileMeta {
|
||||
export function isHubFile(dependentCount: number): boolean {
|
||||
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 {
|
||||
calculateImpactScore,
|
||||
type ComplexityMetrics,
|
||||
createFileMeta,
|
||||
type FileMeta,
|
||||
@@ -430,6 +431,7 @@ export class MetaAnalyzer {
|
||||
|
||||
/**
|
||||
* Batch analyze multiple files.
|
||||
* Computes impact scores and transitive dependencies after all files are analyzed.
|
||||
*/
|
||||
analyzeAll(files: Map<string, { ast: FileAST; content: string }>): Map<string, FileMeta> {
|
||||
const allASTs = new Map<string, FileAST>()
|
||||
@@ -443,6 +445,171 @@ export class MetaAnalyzer {
|
||||
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)
|
||||
}
|
||||
|
||||
// Compute transitive dependency counts
|
||||
this.computeTransitiveCounts(results)
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute transitive dependency counts for all files.
|
||||
* Uses DFS with memoization for efficiency.
|
||||
*/
|
||||
computeTransitiveCounts(metas: Map<string, FileMeta>): void {
|
||||
// Memoization caches
|
||||
const transitiveDepCache = new Map<string, Set<string>>()
|
||||
const transitiveDepByCache = new Map<string, Set<string>>()
|
||||
|
||||
// Compute transitive dependents (files that depend on this file, directly or transitively)
|
||||
for (const [filePath, meta] of metas) {
|
||||
const transitiveDeps = this.getTransitiveDependents(filePath, metas, transitiveDepCache)
|
||||
// Exclude the file itself from count (can happen in cycles)
|
||||
meta.transitiveDepCount = transitiveDeps.has(filePath)
|
||||
? transitiveDeps.size - 1
|
||||
: transitiveDeps.size
|
||||
}
|
||||
|
||||
// Compute transitive dependencies (files this file depends on, directly or transitively)
|
||||
for (const [filePath, meta] of metas) {
|
||||
const transitiveDepsBy = this.getTransitiveDependencies(
|
||||
filePath,
|
||||
metas,
|
||||
transitiveDepByCache,
|
||||
)
|
||||
// Exclude the file itself from count (can happen in cycles)
|
||||
meta.transitiveDepByCount = transitiveDepsBy.has(filePath)
|
||||
? transitiveDepsBy.size - 1
|
||||
: transitiveDepsBy.size
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all files that depend on the given file transitively.
|
||||
* Uses DFS with cycle detection. Caching only at the top level.
|
||||
*/
|
||||
getTransitiveDependents(
|
||||
filePath: string,
|
||||
metas: Map<string, FileMeta>,
|
||||
cache: Map<string, Set<string>>,
|
||||
visited?: Set<string>,
|
||||
): Set<string> {
|
||||
// Return cached result if available (only valid for top-level calls)
|
||||
if (!visited) {
|
||||
const cached = cache.get(filePath)
|
||||
if (cached) {
|
||||
return cached
|
||||
}
|
||||
}
|
||||
|
||||
const isTopLevel = !visited
|
||||
if (!visited) {
|
||||
visited = new Set()
|
||||
}
|
||||
|
||||
// Detect cycles
|
||||
if (visited.has(filePath)) {
|
||||
return new Set()
|
||||
}
|
||||
|
||||
visited.add(filePath)
|
||||
const result = new Set<string>()
|
||||
|
||||
const meta = metas.get(filePath)
|
||||
if (!meta) {
|
||||
if (isTopLevel) {
|
||||
cache.set(filePath, result)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// Add direct dependents
|
||||
for (const dependent of meta.dependents) {
|
||||
result.add(dependent)
|
||||
|
||||
// Recursively add transitive dependents
|
||||
const transitive = this.getTransitiveDependents(
|
||||
dependent,
|
||||
metas,
|
||||
cache,
|
||||
new Set(visited),
|
||||
)
|
||||
for (const t of transitive) {
|
||||
result.add(t)
|
||||
}
|
||||
}
|
||||
|
||||
// Only cache top-level results (not intermediate results during recursion)
|
||||
if (isTopLevel) {
|
||||
cache.set(filePath, result)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all files that the given file depends on transitively.
|
||||
* Uses DFS with cycle detection. Caching only at the top level.
|
||||
*/
|
||||
getTransitiveDependencies(
|
||||
filePath: string,
|
||||
metas: Map<string, FileMeta>,
|
||||
cache: Map<string, Set<string>>,
|
||||
visited?: Set<string>,
|
||||
): Set<string> {
|
||||
// Return cached result if available (only valid for top-level calls)
|
||||
if (!visited) {
|
||||
const cached = cache.get(filePath)
|
||||
if (cached) {
|
||||
return cached
|
||||
}
|
||||
}
|
||||
|
||||
const isTopLevel = !visited
|
||||
if (!visited) {
|
||||
visited = new Set()
|
||||
}
|
||||
|
||||
// Detect cycles
|
||||
if (visited.has(filePath)) {
|
||||
return new Set()
|
||||
}
|
||||
|
||||
visited.add(filePath)
|
||||
const result = new Set<string>()
|
||||
|
||||
const meta = metas.get(filePath)
|
||||
if (!meta) {
|
||||
if (isTopLevel) {
|
||||
cache.set(filePath, result)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// Add direct dependencies
|
||||
for (const dependency of meta.dependencies) {
|
||||
result.add(dependency)
|
||||
|
||||
// Recursively add transitive dependencies
|
||||
const transitive = this.getTransitiveDependencies(
|
||||
dependency,
|
||||
metas,
|
||||
cache,
|
||||
new Set(visited),
|
||||
)
|
||||
for (const t of transitive) {
|
||||
result.add(t)
|
||||
}
|
||||
}
|
||||
|
||||
// Only cache top-level results (not intermediate results during recursion)
|
||||
if (isTopLevel) {
|
||||
cache.set(filePath, result)
|
||||
}
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ export interface BuildContextOptions {
|
||||
includeSignatures?: boolean
|
||||
includeDepsGraph?: boolean
|
||||
includeCircularDeps?: boolean
|
||||
includeHighImpactFiles?: boolean
|
||||
circularDeps?: string[][]
|
||||
}
|
||||
|
||||
@@ -132,6 +133,7 @@ export function buildInitialContext(
|
||||
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(formatDirectoryTree(structure))
|
||||
@@ -144,6 +146,13 @@ export function buildInitialContext(
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
@@ -568,6 +577,74 @@ export function formatCircularDeps(cycles: string[][]): string | 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.
|
||||
*/
|
||||
|
||||
@@ -117,6 +117,7 @@ export const ContextConfigSchema = z.object({
|
||||
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 { createFileMeta, isHubFile } from "../../../../src/domain/value-objects/FileMeta.js"
|
||||
import {
|
||||
calculateImpactScore,
|
||||
createFileMeta,
|
||||
isHubFile,
|
||||
} from "../../../../src/domain/value-objects/FileMeta.js"
|
||||
|
||||
describe("FileMeta", () => {
|
||||
describe("createFileMeta", () => {
|
||||
@@ -15,6 +19,7 @@ describe("FileMeta", () => {
|
||||
expect(meta.isHub).toBe(false)
|
||||
expect(meta.isEntryPoint).toBe(false)
|
||||
expect(meta.fileType).toBe("unknown")
|
||||
expect(meta.impactScore).toBe(0)
|
||||
})
|
||||
|
||||
it("should merge partial values", () => {
|
||||
@@ -42,4 +47,51 @@ describe("FileMeta", () => {
|
||||
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)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -3,6 +3,7 @@ import { MetaAnalyzer } from "../../../../src/infrastructure/indexer/MetaAnalyze
|
||||
import { ASTParser } from "../../../../src/infrastructure/indexer/ASTParser.js"
|
||||
import type { FileAST } from "../../../../src/domain/value-objects/FileAST.js"
|
||||
import { createEmptyFileAST } from "../../../../src/domain/value-objects/FileAST.js"
|
||||
import { createFileMeta, type FileMeta } from "../../../../src/domain/value-objects/FileMeta.js"
|
||||
|
||||
describe("MetaAnalyzer", () => {
|
||||
let analyzer: MetaAnalyzer
|
||||
@@ -737,4 +738,368 @@ export function createComponent(): MyComponent {
|
||||
expect(meta.fileType).toBe("source")
|
||||
})
|
||||
})
|
||||
|
||||
describe("computeTransitiveCounts", () => {
|
||||
it("should compute transitive dependents for a simple chain", () => {
|
||||
// A -> B -> C (A depends on B, B depends on C)
|
||||
// So C has transitive dependents: B, A
|
||||
const metas = new Map<string, FileMeta>()
|
||||
metas.set(
|
||||
"/project/a.ts",
|
||||
createFileMeta({
|
||||
dependencies: ["/project/b.ts"],
|
||||
dependents: [],
|
||||
}),
|
||||
)
|
||||
metas.set(
|
||||
"/project/b.ts",
|
||||
createFileMeta({
|
||||
dependencies: ["/project/c.ts"],
|
||||
dependents: ["/project/a.ts"],
|
||||
}),
|
||||
)
|
||||
metas.set(
|
||||
"/project/c.ts",
|
||||
createFileMeta({
|
||||
dependencies: [],
|
||||
dependents: ["/project/b.ts"],
|
||||
}),
|
||||
)
|
||||
|
||||
analyzer.computeTransitiveCounts(metas)
|
||||
|
||||
expect(metas.get("/project/c.ts")!.transitiveDepCount).toBe(2) // B and A
|
||||
expect(metas.get("/project/b.ts")!.transitiveDepCount).toBe(1) // A
|
||||
expect(metas.get("/project/a.ts")!.transitiveDepCount).toBe(0) // none
|
||||
})
|
||||
|
||||
it("should compute transitive dependencies for a simple chain", () => {
|
||||
// A -> B -> C (A depends on B, B depends on C)
|
||||
// So A has transitive dependencies: B, C
|
||||
const metas = new Map<string, FileMeta>()
|
||||
metas.set(
|
||||
"/project/a.ts",
|
||||
createFileMeta({
|
||||
dependencies: ["/project/b.ts"],
|
||||
dependents: [],
|
||||
}),
|
||||
)
|
||||
metas.set(
|
||||
"/project/b.ts",
|
||||
createFileMeta({
|
||||
dependencies: ["/project/c.ts"],
|
||||
dependents: ["/project/a.ts"],
|
||||
}),
|
||||
)
|
||||
metas.set(
|
||||
"/project/c.ts",
|
||||
createFileMeta({
|
||||
dependencies: [],
|
||||
dependents: ["/project/b.ts"],
|
||||
}),
|
||||
)
|
||||
|
||||
analyzer.computeTransitiveCounts(metas)
|
||||
|
||||
expect(metas.get("/project/a.ts")!.transitiveDepByCount).toBe(2) // B and C
|
||||
expect(metas.get("/project/b.ts")!.transitiveDepByCount).toBe(1) // C
|
||||
expect(metas.get("/project/c.ts")!.transitiveDepByCount).toBe(0) // none
|
||||
})
|
||||
|
||||
it("should handle diamond dependency pattern", () => {
|
||||
// A
|
||||
// / \
|
||||
// B C
|
||||
// \ /
|
||||
// D
|
||||
// A depends on B and C, both depend on D
|
||||
const metas = new Map<string, FileMeta>()
|
||||
metas.set(
|
||||
"/project/a.ts",
|
||||
createFileMeta({
|
||||
dependencies: ["/project/b.ts", "/project/c.ts"],
|
||||
dependents: [],
|
||||
}),
|
||||
)
|
||||
metas.set(
|
||||
"/project/b.ts",
|
||||
createFileMeta({
|
||||
dependencies: ["/project/d.ts"],
|
||||
dependents: ["/project/a.ts"],
|
||||
}),
|
||||
)
|
||||
metas.set(
|
||||
"/project/c.ts",
|
||||
createFileMeta({
|
||||
dependencies: ["/project/d.ts"],
|
||||
dependents: ["/project/a.ts"],
|
||||
}),
|
||||
)
|
||||
metas.set(
|
||||
"/project/d.ts",
|
||||
createFileMeta({
|
||||
dependencies: [],
|
||||
dependents: ["/project/b.ts", "/project/c.ts"],
|
||||
}),
|
||||
)
|
||||
|
||||
analyzer.computeTransitiveCounts(metas)
|
||||
|
||||
// D is depended on by B, C, and transitively by A
|
||||
expect(metas.get("/project/d.ts")!.transitiveDepCount).toBe(3)
|
||||
// A depends on B, C, and transitively on D
|
||||
expect(metas.get("/project/a.ts")!.transitiveDepByCount).toBe(3)
|
||||
})
|
||||
|
||||
it("should handle circular dependencies gracefully", () => {
|
||||
// A -> B -> C -> A (circular)
|
||||
const metas = new Map<string, FileMeta>()
|
||||
metas.set(
|
||||
"/project/a.ts",
|
||||
createFileMeta({
|
||||
dependencies: ["/project/b.ts"],
|
||||
dependents: ["/project/c.ts"],
|
||||
}),
|
||||
)
|
||||
metas.set(
|
||||
"/project/b.ts",
|
||||
createFileMeta({
|
||||
dependencies: ["/project/c.ts"],
|
||||
dependents: ["/project/a.ts"],
|
||||
}),
|
||||
)
|
||||
metas.set(
|
||||
"/project/c.ts",
|
||||
createFileMeta({
|
||||
dependencies: ["/project/a.ts"],
|
||||
dependents: ["/project/b.ts"],
|
||||
}),
|
||||
)
|
||||
|
||||
// Should not throw, should handle cycles
|
||||
analyzer.computeTransitiveCounts(metas)
|
||||
|
||||
// Each file has the other 2 as transitive dependents
|
||||
expect(metas.get("/project/a.ts")!.transitiveDepCount).toBe(2)
|
||||
expect(metas.get("/project/b.ts")!.transitiveDepCount).toBe(2)
|
||||
expect(metas.get("/project/c.ts")!.transitiveDepCount).toBe(2)
|
||||
})
|
||||
|
||||
it("should return 0 for files with no dependencies", () => {
|
||||
const metas = new Map<string, FileMeta>()
|
||||
metas.set(
|
||||
"/project/a.ts",
|
||||
createFileMeta({
|
||||
dependencies: [],
|
||||
dependents: [],
|
||||
}),
|
||||
)
|
||||
|
||||
analyzer.computeTransitiveCounts(metas)
|
||||
|
||||
expect(metas.get("/project/a.ts")!.transitiveDepCount).toBe(0)
|
||||
expect(metas.get("/project/a.ts")!.transitiveDepByCount).toBe(0)
|
||||
})
|
||||
|
||||
it("should handle empty metas map", () => {
|
||||
const metas = new Map<string, FileMeta>()
|
||||
// Should not throw
|
||||
expect(() => analyzer.computeTransitiveCounts(metas)).not.toThrow()
|
||||
})
|
||||
|
||||
it("should handle single file", () => {
|
||||
const metas = new Map<string, FileMeta>()
|
||||
metas.set(
|
||||
"/project/a.ts",
|
||||
createFileMeta({
|
||||
dependencies: [],
|
||||
dependents: [],
|
||||
}),
|
||||
)
|
||||
|
||||
analyzer.computeTransitiveCounts(metas)
|
||||
|
||||
expect(metas.get("/project/a.ts")!.transitiveDepCount).toBe(0)
|
||||
expect(metas.get("/project/a.ts")!.transitiveDepByCount).toBe(0)
|
||||
})
|
||||
|
||||
it("should handle multiple roots depending on same leaf", () => {
|
||||
// A -> C, B -> C
|
||||
const metas = new Map<string, FileMeta>()
|
||||
metas.set(
|
||||
"/project/a.ts",
|
||||
createFileMeta({
|
||||
dependencies: ["/project/c.ts"],
|
||||
dependents: [],
|
||||
}),
|
||||
)
|
||||
metas.set(
|
||||
"/project/b.ts",
|
||||
createFileMeta({
|
||||
dependencies: ["/project/c.ts"],
|
||||
dependents: [],
|
||||
}),
|
||||
)
|
||||
metas.set(
|
||||
"/project/c.ts",
|
||||
createFileMeta({
|
||||
dependencies: [],
|
||||
dependents: ["/project/a.ts", "/project/b.ts"],
|
||||
}),
|
||||
)
|
||||
|
||||
analyzer.computeTransitiveCounts(metas)
|
||||
|
||||
expect(metas.get("/project/c.ts")!.transitiveDepCount).toBe(2) // A and B
|
||||
expect(metas.get("/project/a.ts")!.transitiveDepByCount).toBe(1) // C
|
||||
expect(metas.get("/project/b.ts")!.transitiveDepByCount).toBe(1) // C
|
||||
})
|
||||
|
||||
it("should handle deep dependency chains", () => {
|
||||
// A -> B -> C -> D -> E
|
||||
const metas = new Map<string, FileMeta>()
|
||||
metas.set(
|
||||
"/project/a.ts",
|
||||
createFileMeta({
|
||||
dependencies: ["/project/b.ts"],
|
||||
dependents: [],
|
||||
}),
|
||||
)
|
||||
metas.set(
|
||||
"/project/b.ts",
|
||||
createFileMeta({
|
||||
dependencies: ["/project/c.ts"],
|
||||
dependents: ["/project/a.ts"],
|
||||
}),
|
||||
)
|
||||
metas.set(
|
||||
"/project/c.ts",
|
||||
createFileMeta({
|
||||
dependencies: ["/project/d.ts"],
|
||||
dependents: ["/project/b.ts"],
|
||||
}),
|
||||
)
|
||||
metas.set(
|
||||
"/project/d.ts",
|
||||
createFileMeta({
|
||||
dependencies: ["/project/e.ts"],
|
||||
dependents: ["/project/c.ts"],
|
||||
}),
|
||||
)
|
||||
metas.set(
|
||||
"/project/e.ts",
|
||||
createFileMeta({
|
||||
dependencies: [],
|
||||
dependents: ["/project/d.ts"],
|
||||
}),
|
||||
)
|
||||
|
||||
analyzer.computeTransitiveCounts(metas)
|
||||
|
||||
// E has transitive dependents: D, C, B, A
|
||||
expect(metas.get("/project/e.ts")!.transitiveDepCount).toBe(4)
|
||||
// A has transitive dependencies: B, C, D, E
|
||||
expect(metas.get("/project/a.ts")!.transitiveDepByCount).toBe(4)
|
||||
})
|
||||
})
|
||||
|
||||
describe("getTransitiveDependents", () => {
|
||||
it("should return empty set for file not in metas", () => {
|
||||
const metas = new Map<string, FileMeta>()
|
||||
const cache = new Map<string, Set<string>>()
|
||||
|
||||
const result = analyzer.getTransitiveDependents("/project/unknown.ts", metas, cache)
|
||||
|
||||
expect(result.size).toBe(0)
|
||||
})
|
||||
|
||||
it("should use cache for repeated calls", () => {
|
||||
const metas = new Map<string, FileMeta>()
|
||||
metas.set(
|
||||
"/project/a.ts",
|
||||
createFileMeta({
|
||||
dependencies: [],
|
||||
dependents: ["/project/b.ts"],
|
||||
}),
|
||||
)
|
||||
metas.set(
|
||||
"/project/b.ts",
|
||||
createFileMeta({
|
||||
dependencies: ["/project/a.ts"],
|
||||
dependents: [],
|
||||
}),
|
||||
)
|
||||
|
||||
const cache = new Map<string, Set<string>>()
|
||||
const result1 = analyzer.getTransitiveDependents("/project/a.ts", metas, cache)
|
||||
const result2 = analyzer.getTransitiveDependents("/project/a.ts", metas, cache)
|
||||
|
||||
// Should return same instance from cache
|
||||
expect(result1).toBe(result2)
|
||||
expect(result1.size).toBe(1)
|
||||
})
|
||||
})
|
||||
|
||||
describe("getTransitiveDependencies", () => {
|
||||
it("should return empty set for file not in metas", () => {
|
||||
const metas = new Map<string, FileMeta>()
|
||||
const cache = new Map<string, Set<string>>()
|
||||
|
||||
const result = analyzer.getTransitiveDependencies("/project/unknown.ts", metas, cache)
|
||||
|
||||
expect(result.size).toBe(0)
|
||||
})
|
||||
|
||||
it("should use cache for repeated calls", () => {
|
||||
const metas = new Map<string, FileMeta>()
|
||||
metas.set(
|
||||
"/project/a.ts",
|
||||
createFileMeta({
|
||||
dependencies: ["/project/b.ts"],
|
||||
dependents: [],
|
||||
}),
|
||||
)
|
||||
metas.set(
|
||||
"/project/b.ts",
|
||||
createFileMeta({
|
||||
dependencies: [],
|
||||
dependents: ["/project/a.ts"],
|
||||
}),
|
||||
)
|
||||
|
||||
const cache = new Map<string, Set<string>>()
|
||||
const result1 = analyzer.getTransitiveDependencies("/project/a.ts", metas, cache)
|
||||
const result2 = analyzer.getTransitiveDependencies("/project/a.ts", metas, cache)
|
||||
|
||||
// Should return same instance from cache
|
||||
expect(result1).toBe(result2)
|
||||
expect(result1.size).toBe(1)
|
||||
})
|
||||
})
|
||||
|
||||
describe("analyzeAll with transitive counts", () => {
|
||||
it("should compute transitive counts in analyzeAll", () => {
|
||||
const files = new Map<string, { ast: FileAST; content: string }>()
|
||||
|
||||
// A imports B, B imports C
|
||||
const aContent = `import { b } from "./b"`
|
||||
const aAST = parser.parse(aContent, "ts")
|
||||
files.set("/project/src/a.ts", { ast: aAST, content: aContent })
|
||||
|
||||
const bContent = `import { c } from "./c"\nexport const b = () => c()`
|
||||
const bAST = parser.parse(bContent, "ts")
|
||||
files.set("/project/src/b.ts", { ast: bAST, content: bContent })
|
||||
|
||||
const cContent = `export const c = () => 42`
|
||||
const cAST = parser.parse(cContent, "ts")
|
||||
files.set("/project/src/c.ts", { ast: cAST, content: cContent })
|
||||
|
||||
const results = analyzer.analyzeAll(files)
|
||||
|
||||
// C has transitive dependents: B and A
|
||||
expect(results.get("/project/src/c.ts")!.transitiveDepCount).toBe(2)
|
||||
// A has transitive dependencies: B and C
|
||||
expect(results.get("/project/src/a.ts")!.transitiveDepByCount).toBe(2)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
truncateContext,
|
||||
formatDependencyGraph,
|
||||
formatCircularDeps,
|
||||
formatHighImpactFiles,
|
||||
type ProjectStructure,
|
||||
} from "../../../../src/infrastructure/llm/prompts.js"
|
||||
import type { FileAST } from "../../../../src/domain/value-objects/FileAST.js"
|
||||
@@ -2395,6 +2396,345 @@ describe("prompts", () => {
|
||||
})
|
||||
})
|
||||
|
||||
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", () => {
|
||||
|
||||
@@ -18,6 +18,7 @@ describe("ContextConfigSchema", () => {
|
||||
includeSignatures: true,
|
||||
includeDepsGraph: true,
|
||||
includeCircularDeps: true,
|
||||
includeHighImpactFiles: true,
|
||||
})
|
||||
})
|
||||
|
||||
@@ -32,6 +33,7 @@ describe("ContextConfigSchema", () => {
|
||||
includeSignatures: true,
|
||||
includeDepsGraph: true,
|
||||
includeCircularDeps: true,
|
||||
includeHighImpactFiles: true,
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -171,6 +173,7 @@ describe("ContextConfigSchema", () => {
|
||||
includeSignatures: true,
|
||||
includeDepsGraph: true,
|
||||
includeCircularDeps: true,
|
||||
includeHighImpactFiles: true,
|
||||
})
|
||||
})
|
||||
|
||||
@@ -187,6 +190,7 @@ describe("ContextConfigSchema", () => {
|
||||
includeSignatures: true,
|
||||
includeDepsGraph: true,
|
||||
includeCircularDeps: true,
|
||||
includeHighImpactFiles: true,
|
||||
})
|
||||
})
|
||||
|
||||
@@ -204,6 +208,7 @@ describe("ContextConfigSchema", () => {
|
||||
includeSignatures: true,
|
||||
includeDepsGraph: true,
|
||||
includeCircularDeps: true,
|
||||
includeHighImpactFiles: true,
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -218,6 +223,7 @@ describe("ContextConfigSchema", () => {
|
||||
includeSignatures: false,
|
||||
includeDepsGraph: false,
|
||||
includeCircularDeps: false,
|
||||
includeHighImpactFiles: false,
|
||||
}
|
||||
|
||||
const result = ContextConfigSchema.parse(config)
|
||||
@@ -233,6 +239,7 @@ describe("ContextConfigSchema", () => {
|
||||
includeSignatures: true,
|
||||
includeDepsGraph: true,
|
||||
includeCircularDeps: true,
|
||||
includeHighImpactFiles: true,
|
||||
}
|
||||
|
||||
const result = ContextConfigSchema.parse(config)
|
||||
@@ -314,4 +321,29 @@ describe("ContextConfigSchema", () => {
|
||||
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