mirror of
https://github.com/samiyev/puaros.git
synced 2025-12-28 15:26:53 +05:00
Compare commits
14 Commits
ipuaro-v0.
...
ipuaro-v0.
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c82006bbda | ||
|
|
2e84472e49 | ||
|
|
17d75dbd54 | ||
|
|
fac5966678 | ||
|
|
92ba3fd9ba | ||
|
|
e9aaa708fe | ||
|
|
d6d15dd271 | ||
|
|
d63d85d850 | ||
|
|
41cfc21f20 | ||
|
|
eeaa223436 | ||
|
|
36768c06d1 | ||
|
|
5a22cd5c9b | ||
|
|
806c9281b0 | ||
|
|
12197a9624 |
@@ -5,6 +5,337 @@ 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.30.1] - 2025-12-05 - Display Transitive Counts in Context
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- **High Impact Files table now includes transitive counts**
|
||||||
|
- Table header changed from `| File | Impact | Dependents |` to `| File | Impact | Direct | Transitive |`
|
||||||
|
- Shows both direct dependent count and transitive dependent count
|
||||||
|
- Sorting changed: now sorts by transitive count first, then by impact score
|
||||||
|
- Example: `| utils/validation | 67% | 12 | 24 |`
|
||||||
|
|
||||||
|
### Technical Details
|
||||||
|
|
||||||
|
- Total tests: 1839 passed
|
||||||
|
- 0 ESLint errors, 3 warnings (pre-existing complexity)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## [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
|
||||||
|
|
||||||
|
- **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
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- **Decorator Extraction (0.24.4)**
|
||||||
|
- Functions now show their decorators in initial context
|
||||||
|
- Classes now show their decorators in initial context
|
||||||
|
- Methods show decorators per-method
|
||||||
|
- New format: `@Controller('users') class UserController`
|
||||||
|
- Function format: `@Get(':id') async getUser(id: string): Promise<User>`
|
||||||
|
- Supports NestJS decorators: `@Controller`, `@Get`, `@Post`, `@Injectable`, `@UseGuards`, etc.
|
||||||
|
- Supports Angular decorators: `@Component`, `@Injectable`, `@Input`, `@Output`, etc.
|
||||||
|
|
||||||
|
- **FileAST.ts Enhancements**
|
||||||
|
- `decorators?: string[]` field on `FunctionInfo`
|
||||||
|
- `decorators?: string[]` field on `MethodInfo`
|
||||||
|
- `decorators?: string[]` field on `ClassInfo`
|
||||||
|
|
||||||
|
- **ASTParser.ts Enhancements**
|
||||||
|
- `formatDecorator()` - formats decorator node to string (e.g., `@Get(':id')`)
|
||||||
|
- `extractNodeDecorators()` - extracts decorators that are direct children of a node
|
||||||
|
- `extractDecoratorsFromSiblings()` - extracts decorators before the declaration in export statements
|
||||||
|
- Decorators are extracted for classes, methods, and exported functions
|
||||||
|
|
||||||
|
- **prompts.ts Enhancements**
|
||||||
|
- `formatDecoratorsPrefix()` - formats decorators as a prefix string for display
|
||||||
|
- Used in `formatFunctionSignature()` for function decorators
|
||||||
|
- Used in `formatFileSummary()` for class decorators
|
||||||
|
|
||||||
|
### New Context Format
|
||||||
|
|
||||||
|
```
|
||||||
|
### src/controllers/user.controller.ts
|
||||||
|
- @Controller('users') class UserController extends BaseController
|
||||||
|
- @Get(':id') @Auth() async getUser(id: string): Promise<User>
|
||||||
|
- @Post() @ValidateBody() async createUser(data: UserDTO): Promise<User>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Technical Details
|
||||||
|
|
||||||
|
- Total tests: 1754 passed (was 1720, +34 new tests)
|
||||||
|
- 14 new tests for ASTParser decorator extraction
|
||||||
|
- 6 new tests for prompts decorator formatting
|
||||||
|
- +14 other tests from internal improvements
|
||||||
|
- Coverage: 97.49% lines, 91.14% branches, 98.61% functions
|
||||||
|
- 0 ESLint errors, 2 warnings (pre-existing complexity in ASTParser and prompts)
|
||||||
|
- Build successful
|
||||||
|
|
||||||
|
### Notes
|
||||||
|
|
||||||
|
This completes the v0.24.0 Rich Initial Context milestone:
|
||||||
|
- ✅ 0.24.1 - Function Signatures with Types
|
||||||
|
- ✅ 0.24.2 - Interface/Type Field Definitions
|
||||||
|
- ✅ 0.24.3 - Enum Value Definitions
|
||||||
|
- ✅ 0.24.4 - Decorator Extraction
|
||||||
|
|
||||||
|
Next milestone: v0.25.0 - Graph Metrics in Context
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## [0.25.0] - 2025-12-04 - Rich Initial Context: Interface Fields & Type Definitions
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- **Interface Field Definitions (0.24.2)**
|
||||||
|
- Interfaces now show their fields in initial context
|
||||||
|
- New format: `interface User { id: string, name: string, email: string }`
|
||||||
|
- Readonly fields marked: `interface Config { readonly version: string }`
|
||||||
|
- Extends still supported: `interface AdminUser extends User { role: string }`
|
||||||
|
|
||||||
|
- **Type Alias Definitions (0.24.2)**
|
||||||
|
- Type aliases now show their definitions in initial context
|
||||||
|
- Simple types: `type UserId = string`
|
||||||
|
- Union types: `type Status = "pending" | "active" | "done"`
|
||||||
|
- Intersection types: `type AdminUser = User & Admin`
|
||||||
|
- Function types: `type Handler = (event: Event) => void`
|
||||||
|
- Long definitions truncated at 80 characters with `...`
|
||||||
|
|
||||||
|
- **New Helper Functions in prompts.ts**
|
||||||
|
- `formatInterfaceSignature()` - formats interface with fields
|
||||||
|
- `formatTypeAliasSignature()` - formats type alias with definition
|
||||||
|
- `truncateDefinition()` - truncates long type definitions
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- **FileAST.ts**
|
||||||
|
- Added `definition?: string` field to `TypeAliasInfo` interface
|
||||||
|
|
||||||
|
- **ASTParser.ts**
|
||||||
|
- `extractTypeAlias()` now extracts the type definition via `childForFieldName(VALUE)`
|
||||||
|
- Supports all type kinds: simple, union, intersection, object, function, generic, array, tuple
|
||||||
|
|
||||||
|
- **prompts.ts**
|
||||||
|
- `formatFileSummary()` now uses `formatInterfaceSignature()` for interfaces
|
||||||
|
- `formatFileSummary()` now uses `formatTypeAliasSignature()` for type aliases
|
||||||
|
|
||||||
|
### New Context Format
|
||||||
|
|
||||||
|
```
|
||||||
|
### src/types/user.ts
|
||||||
|
- interface User { id: string, name: string, email: string }
|
||||||
|
- interface UserDTO { name: string, email?: string }
|
||||||
|
- type UserId = string
|
||||||
|
- type Status = "pending" | "active" | "done"
|
||||||
|
- type AdminUser = User & Admin
|
||||||
|
```
|
||||||
|
|
||||||
|
### Technical Details
|
||||||
|
|
||||||
|
- Total tests: 1720 passed (was 1702, +18 new tests)
|
||||||
|
- 10 new tests for interface field formatting
|
||||||
|
- 8 new tests for type alias definition extraction
|
||||||
|
- Coverage: 97.5% lines, 91.04% branches, 98.6% functions
|
||||||
|
- 0 ESLint errors, 1 warning (pre-existing complexity in ASTParser)
|
||||||
|
- Build successful
|
||||||
|
|
||||||
|
### Notes
|
||||||
|
|
||||||
|
This completes the second part of Rich Initial Context milestone:
|
||||||
|
- ✅ 0.24.1 - Function Signatures with Types
|
||||||
|
- ✅ 0.24.2 - Interface/Type Field Definitions
|
||||||
|
- ⏳ 0.24.3 - Enum Value Definitions
|
||||||
|
- ⏳ 0.24.4 - Decorator Extraction
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## [0.24.0] - 2025-12-04 - Rich Initial Context: Function Signatures
|
## [0.24.0] - 2025-12-04 - Rich Initial Context: Function Signatures
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|||||||
@@ -1333,40 +1333,40 @@ class ErrorHandler {
|
|||||||
**Priority:** HIGH
|
**Priority:** HIGH
|
||||||
**Status:** Complete (v0.19.0 released)
|
**Status:** Complete (v0.19.0 released)
|
||||||
|
|
||||||
Рефакторинг: переход на чистый XML формат для tool calls (как в CONCEPT.md).
|
Refactoring: transition to pure XML format for tool calls (as in CONCEPT.md).
|
||||||
|
|
||||||
### Текущая проблема
|
### Current Problem
|
||||||
|
|
||||||
OllamaClient использует Ollama native tool calling (JSON Schema), а ResponseParser реализует XML парсинг. Это создаёт путаницу и не соответствует CONCEPT.md.
|
OllamaClient uses Ollama native tool calling (JSON Schema), while ResponseParser implements XML parsing. This creates confusion and doesn't match CONCEPT.md.
|
||||||
|
|
||||||
### 0.19.1 - OllamaClient Refactor
|
### 0.19.1 - OllamaClient Refactor
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
// src/infrastructure/llm/OllamaClient.ts
|
// src/infrastructure/llm/OllamaClient.ts
|
||||||
|
|
||||||
// БЫЛО:
|
// BEFORE:
|
||||||
// - Передаём tools в Ollama SDK format
|
// - Pass tools in Ollama SDK format
|
||||||
// - Извлекаем tool_calls из response.message.tool_calls
|
// - Extract tool_calls from response.message.tool_calls
|
||||||
|
|
||||||
// СТАНЕТ:
|
// AFTER:
|
||||||
// - НЕ передаём tools в SDK
|
// - DON'T pass tools to SDK
|
||||||
// - Tools описаны в system prompt как XML
|
// - Tools described in system prompt as XML
|
||||||
// - LLM возвращает XML в content
|
// - LLM returns XML in content
|
||||||
// - Парсим через ResponseParser
|
// - Parse via ResponseParser
|
||||||
```
|
```
|
||||||
|
|
||||||
**Изменения:**
|
**Changes:**
|
||||||
- [x] Удалить `convertTools()` метод
|
- [x] Remove `convertTools()` method
|
||||||
- [x] Удалить `extractToolCalls()` метод
|
- [x] Remove `extractToolCalls()` method
|
||||||
- [x] Убрать передачу `tools` в `client.chat()`
|
- [x] Remove `tools` from `client.chat()` call
|
||||||
- [x] Возвращать только `content` без `toolCalls`
|
- [x] Return only `content` without `toolCalls`
|
||||||
|
|
||||||
### 0.19.2 - System Prompt Update
|
### 0.19.2 - System Prompt Update
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
// src/infrastructure/llm/prompts.ts
|
// src/infrastructure/llm/prompts.ts
|
||||||
|
|
||||||
// Добавить в SYSTEM_PROMPT полное описание XML формата:
|
// Add full XML format description to SYSTEM_PROMPT:
|
||||||
|
|
||||||
const TOOL_FORMAT_INSTRUCTIONS = `
|
const TOOL_FORMAT_INSTRUCTIONS = `
|
||||||
## Tool Calling Format
|
## Tool Calling Format
|
||||||
@@ -1397,73 +1397,73 @@ Always wait for tool results before making conclusions.
|
|||||||
`
|
`
|
||||||
```
|
```
|
||||||
|
|
||||||
**Изменения:**
|
**Changes:**
|
||||||
- [x] Добавить `TOOL_FORMAT_INSTRUCTIONS` в prompts.ts
|
- [x] Add `TOOL_FORMAT_INSTRUCTIONS` to prompts.ts
|
||||||
- [x] Включить в `SYSTEM_PROMPT`
|
- [x] Include in `SYSTEM_PROMPT`
|
||||||
- [x] Добавить примеры для всех 18 tools
|
- [x] Add examples for all 18 tools
|
||||||
|
|
||||||
### 0.19.3 - HandleMessage Simplification
|
### 0.19.3 - HandleMessage Simplification
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
// src/application/use-cases/HandleMessage.ts
|
// src/application/use-cases/HandleMessage.ts
|
||||||
|
|
||||||
// БЫЛО:
|
// BEFORE:
|
||||||
// const response = await this.llm.chat(messages)
|
// const response = await this.llm.chat(messages)
|
||||||
// const parsed = parseToolCalls(response.content)
|
// const parsed = parseToolCalls(response.content)
|
||||||
|
|
||||||
// СТАНЕТ:
|
// AFTER:
|
||||||
// const response = await this.llm.chat(messages) // без tools
|
// const response = await this.llm.chat(messages) // without tools
|
||||||
// const parsed = parseToolCalls(response.content) // единственный источник
|
// const parsed = parseToolCalls(response.content) // single source
|
||||||
```
|
```
|
||||||
|
|
||||||
**Изменения:**
|
**Changes:**
|
||||||
- [x] Убрать передачу tool definitions в `llm.chat()`
|
- [x] Remove tool definitions from `llm.chat()`
|
||||||
- [x] ResponseParser — единственный источник tool calls
|
- [x] ResponseParser — single source of tool calls
|
||||||
- [x] Упростить логику обработки
|
- [x] Simplify processing logic
|
||||||
|
|
||||||
### 0.19.4 - ILLMClient Interface Update
|
### 0.19.4 - ILLMClient Interface Update
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
// src/domain/services/ILLMClient.ts
|
// src/domain/services/ILLMClient.ts
|
||||||
|
|
||||||
// БЫЛО:
|
// BEFORE:
|
||||||
interface ILLMClient {
|
interface ILLMClient {
|
||||||
chat(messages: ChatMessage[], tools?: ToolDef[]): Promise<LLMResponse>
|
chat(messages: ChatMessage[], tools?: ToolDef[]): Promise<LLMResponse>
|
||||||
}
|
}
|
||||||
|
|
||||||
// СТАНЕТ:
|
// AFTER:
|
||||||
interface ILLMClient {
|
interface ILLMClient {
|
||||||
chat(messages: ChatMessage[]): Promise<LLMResponse>
|
chat(messages: ChatMessage[]): Promise<LLMResponse>
|
||||||
// tools больше не передаются - они в system prompt
|
// tools no longer passed - they're in system prompt
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
**Изменения:**
|
**Changes:**
|
||||||
- [x] Убрать `tools` параметр из `chat()`
|
- [x] Remove `tools` parameter from `chat()`
|
||||||
- [x] Убрать `toolCalls` из `LLMResponse` (парсятся из content)
|
- [x] Remove `toolCalls` from `LLMResponse` (parsed from content)
|
||||||
- [x] Обновить все реализации
|
- [x] Update all implementations
|
||||||
|
|
||||||
### 0.19.5 - ResponseParser Enhancements
|
### 0.19.5 - ResponseParser Enhancements
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
// src/infrastructure/llm/ResponseParser.ts
|
// src/infrastructure/llm/ResponseParser.ts
|
||||||
|
|
||||||
// Улучшения:
|
// Improvements:
|
||||||
// - Лучшая обработка ошибок парсинга
|
// - Better error handling for parsing
|
||||||
// - Поддержка CDATA для многострочного content
|
// - CDATA support for multiline content
|
||||||
// - Валидация имён tools
|
// - Tool name validation
|
||||||
```
|
```
|
||||||
|
|
||||||
**Изменения:**
|
**Changes:**
|
||||||
- [x] Добавить поддержку `<![CDATA[...]]>` для content
|
- [x] Add `<![CDATA[...]]>` support for content
|
||||||
- [x] Валидация: tool name должен быть из известного списка
|
- [x] Validation: tool name must be from known list
|
||||||
- [x] Улучшить сообщения об ошибках парсинга
|
- [x] Improve parsing error messages
|
||||||
|
|
||||||
**Tests:**
|
**Tests:**
|
||||||
- [x] Обновить тесты OllamaClient
|
- [x] Update OllamaClient tests
|
||||||
- [x] Обновить тесты HandleMessage
|
- [x] Update HandleMessage tests
|
||||||
- [x] Добавить тесты ResponseParser для edge cases
|
- [x] Add ResponseParser tests for edge cases
|
||||||
- [ ] E2E тест полного flow с XML (опционально, может быть в 0.20.0)
|
- [ ] E2E test for full XML flow (optional, may be in 0.20.0)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -1779,126 +1779,123 @@ export interface ScanResult {
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Version 0.24.0 - Rich Initial Context 📋
|
## Version 0.24.0 - Rich Initial Context 📋 ✅
|
||||||
|
|
||||||
**Priority:** HIGH
|
**Priority:** HIGH
|
||||||
**Status:** In Progress (1/4 complete)
|
**Status:** Complete (v0.24.0 released)
|
||||||
|
|
||||||
Улучшение initial context для LLM: добавление сигнатур функций, типов интерфейсов и значений enum. Это позволит LLM отвечать на вопросы о типах и параметрах без tool calls.
|
Enhance initial context for LLM: add function signatures, interface field types, and enum values. This allows LLM to answer questions about types and parameters without tool calls.
|
||||||
|
|
||||||
### 0.24.1 - Function Signatures with Types ⭐ ✅
|
### 0.24.1 - Function Signatures with Types ⭐ ✅
|
||||||
|
|
||||||
**Проблема:** Сейчас LLM видит только имена функций: `fn: getUser, createUser`
|
**Problem:** Currently LLM only sees function names: `fn: getUser, createUser`
|
||||||
**Решение:** Показать полные сигнатуры: `async getUser(id: string): Promise<User>`
|
**Solution:** Show full signatures: `async getUser(id: string): Promise<User>`
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
// src/infrastructure/llm/prompts.ts changes
|
// src/infrastructure/llm/prompts.ts changes
|
||||||
|
|
||||||
// БЫЛО:
|
// BEFORE:
|
||||||
// - src/services/user.ts [fn: getUser, createUser]
|
// - src/services/user.ts [fn: getUser, createUser]
|
||||||
|
|
||||||
// СТАНЕТ:
|
// AFTER:
|
||||||
// ### src/services/user.ts
|
// ### src/services/user.ts
|
||||||
// - async getUser(id: string): Promise<User>
|
// - async getUser(id: string): Promise<User>
|
||||||
// - async createUser(data: UserDTO): Promise<User>
|
// - async createUser(data: UserDTO): Promise<User>
|
||||||
// - validateEmail(email: string): boolean
|
// - validateEmail(email: string): boolean
|
||||||
```
|
```
|
||||||
|
|
||||||
**Изменения:**
|
**Changes:**
|
||||||
- [x] Расширить `FunctionInfo` в FileAST для хранения типов параметров и return type (already existed)
|
- [x] Extend `FunctionInfo` in FileAST for parameter types and return type (already existed)
|
||||||
- [x] Обновить `ASTParser.ts` для извлечения типов параметров и return types (arrow functions fixed)
|
- [x] Update `ASTParser.ts` to extract parameter types and return types (arrow functions fixed)
|
||||||
- [x] Обновить `formatFileSummary()` в prompts.ts для вывода сигнатур
|
- [x] Update `formatFileSummary()` in prompts.ts to output signatures
|
||||||
- [x] Добавить опцию `includeSignatures: boolean` в config
|
- [x] Add `includeSignatures: boolean` option to config
|
||||||
|
|
||||||
**Зачем:** LLM не будет галлюцинировать параметры и return types.
|
**Why:** LLM won't hallucinate parameters and return types.
|
||||||
|
|
||||||
### 0.24.2 - Interface/Type Field Definitions ⭐
|
### 0.24.2 - Interface/Type Field Definitions ⭐ ✅
|
||||||
|
|
||||||
**Проблема:** LLM видит только `interface: User, UserDTO`
|
**Problem:** LLM only sees `interface: User, UserDTO`
|
||||||
**Решение:** Показать поля: `User { id: string, name: string, email: string }`
|
**Solution:** Show fields: `User { id: string, name: string, email: string }`
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
// БЫЛО:
|
// BEFORE:
|
||||||
// - src/types/user.ts [interface: User, UserDTO]
|
// - src/types/user.ts [interface: User, UserDTO]
|
||||||
|
|
||||||
// СТАНЕТ:
|
// AFTER:
|
||||||
// ### src/types/user.ts
|
// ### src/types/user.ts
|
||||||
// - interface User { id: string, name: string, email: string, createdAt: Date }
|
// - interface User { id: string, name: string, email: string, createdAt: Date }
|
||||||
// - interface UserDTO { name: string, email: string }
|
// - interface UserDTO { name: string, email: string }
|
||||||
// - type UserId = string
|
// - type UserId = string
|
||||||
```
|
```
|
||||||
|
|
||||||
**Изменения:**
|
**Changes:**
|
||||||
- [ ] Расширить `InterfaceInfo` в FileAST для хранения полей с типами
|
- [x] Extend `InterfaceInfo` in FileAST for field types (already existed)
|
||||||
- [ ] Обновить `ASTParser.ts` для извлечения полей интерфейсов
|
- [x] Update `ASTParser.ts` to extract interface fields (already existed)
|
||||||
- [ ] Обновить `formatFileSummary()` для вывода полей
|
- [x] Update `formatFileSummary()` to output fields
|
||||||
- [ ] Обработка type aliases с их определениями
|
- [x] Handle type aliases with their definitions
|
||||||
|
|
||||||
**Зачем:** LLM знает структуру данных, не придумывает поля.
|
**Why:** LLM knows data structure, won't invent fields.
|
||||||
|
|
||||||
### 0.24.3 - Enum Value Definitions
|
### 0.24.3 - Enum Value Definitions ⭐ ✅
|
||||||
|
|
||||||
**Проблема:** LLM видит только `type: Status`
|
**Problem:** LLM only sees `type: Status`
|
||||||
**Решение:** Показать значения: `Status { Active=1, Inactive=0, Pending=2 }`
|
**Solution:** Show values: `Status { Active=1, Inactive=0, Pending=2 }`
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
// БЫЛО:
|
// BEFORE:
|
||||||
// - src/types/enums.ts [type: Status, Role]
|
// - src/types/enums.ts [type: Status, Role]
|
||||||
|
|
||||||
// СТАНЕТ:
|
// AFTER:
|
||||||
// ### src/types/enums.ts
|
// ### src/types/enums.ts
|
||||||
// - enum Status { Active=1, Inactive=0, Pending=2 }
|
// - enum Status { Active=1, Inactive=0, Pending=2 }
|
||||||
// - enum Role { Admin="admin", User="user" }
|
// - enum Role { Admin="admin", User="user" }
|
||||||
```
|
```
|
||||||
|
|
||||||
**Изменения:**
|
**Changes:**
|
||||||
- [ ] Добавить `EnumInfo` в FileAST с members и values
|
- [x] Add `EnumInfo` to FileAST with members and values
|
||||||
- [ ] Обновить `ASTParser.ts` для извлечения enum members
|
- [x] Update `ASTParser.ts` to extract enum members
|
||||||
- [ ] Обновить `formatFileSummary()` для вывода enum values
|
- [x] Update `formatFileSummary()` to output enum values
|
||||||
|
|
||||||
**Зачем:** LLM знает допустимые значения enum.
|
**Why:** LLM knows valid enum values.
|
||||||
|
|
||||||
### 0.24.4 - Decorator Extraction
|
### 0.24.4 - Decorator Extraction ⭐ ✅
|
||||||
|
|
||||||
**Проблема:** LLM не видит декораторы (важно для NestJS, Angular)
|
**Problem:** LLM doesn't see decorators (important for NestJS, Angular)
|
||||||
**Решение:** Показать декораторы в контексте
|
**Solution:** Show decorators in context
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
// СТАНЕТ:
|
// AFTER:
|
||||||
// ### src/controllers/user.controller.ts
|
// ### src/controllers/user.controller.ts
|
||||||
// - @Controller('users') class UserController
|
// - @Controller('users') class UserController
|
||||||
// - @Get(':id') async getUser(id: string): Promise<User>
|
// - @Get(':id') async getUser(id: string): Promise<User>
|
||||||
// - @Post() @Body() async createUser(data: UserDTO): Promise<User>
|
// - @Post() @Body() async createUser(data: UserDTO): Promise<User>
|
||||||
```
|
```
|
||||||
|
|
||||||
**Изменения:**
|
**Changes:**
|
||||||
- [ ] Добавить `decorators: string[]` в FunctionInfo и ClassInfo
|
- [x] Add `decorators: string[]` to FunctionInfo, MethodInfo, and ClassInfo
|
||||||
- [ ] Обновить `ASTParser.ts` для извлечения декораторов
|
- [x] Update `ASTParser.ts` to extract decorators via `extractNodeDecorators()` and `extractDecoratorsFromSiblings()`
|
||||||
- [ ] Обновить контекст для отображения декораторов
|
- [x] Update `prompts.ts` to display decorators via `formatDecoratorsPrefix()`
|
||||||
|
|
||||||
**Зачем:** LLM понимает роутинг, DI, guards в NestJS/Angular.
|
**Why:** LLM understands routing, DI, guards in NestJS/Angular.
|
||||||
|
|
||||||
**Tests:**
|
**Tests:**
|
||||||
- [ ] Unit tests for enhanced ASTParser
|
- [x] Unit tests for ASTParser decorator extraction (14 tests)
|
||||||
- [ ] Unit tests for new context format
|
- [x] Unit tests for prompts decorator formatting (6 tests)
|
||||||
- [ ] Integration tests for full flow
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 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)
|
||||||
|
|
||||||
Добавление графовых метрик в initial context: граф зависимостей, circular dependencies, impact score.
|
### Description
|
||||||
|
|
||||||
### 0.25.1 - Inline Dependency Graph
|
**Problem:** LLM doesn't see file relationships without tool calls
|
||||||
|
**Solution:** Show dependency graph in context
|
||||||
**Проблема:** LLM не видит связи между файлами без tool calls
|
|
||||||
**Решение:** Показать граф зависимостей в контексте
|
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
// Добавить в initial context:
|
// Add to initial context:
|
||||||
|
|
||||||
// ## Dependency Graph
|
// ## Dependency Graph
|
||||||
// src/services/user.ts: → types/user, utils/validation ← controllers/user, api/routes
|
// src/services/user.ts: → types/user, utils/validation ← controllers/user, api/routes
|
||||||
@@ -1906,41 +1903,65 @@ export interface ScanResult {
|
|||||||
// src/utils/validation.ts: ← services/user, services/auth, controllers/*
|
// src/utils/validation.ts: ← services/user, services/auth, controllers/*
|
||||||
```
|
```
|
||||||
|
|
||||||
**Изменения:**
|
**Changes:**
|
||||||
- [ ] Добавить `formatDependencyGraph()` в prompts.ts
|
- [x] Add `formatDependencyGraph()` to prompts.ts
|
||||||
- [ ] Использовать данные из `FileMeta.dependencies` и `FileMeta.dependents`
|
- [x] Use data from `FileMeta.dependencies` and `FileMeta.dependents`
|
||||||
- [ ] Группировать по hub files (много connections)
|
- [x] Group by hub files (many connections)
|
||||||
- [ ] Добавить опцию `includeDepsGraph: boolean` в config
|
- [x] Add `includeDepsGraph: boolean` option to config
|
||||||
|
|
||||||
**Зачем:** LLM видит архитектуру без tool call.
|
**Tests:**
|
||||||
|
- [x] Unit tests for formatDependencyGraph() (16 tests)
|
||||||
|
- [x] Unit tests for includeDepsGraph config option (5 tests)
|
||||||
|
|
||||||
### 0.25.2 - Circular Dependencies in Context
|
**Why:** LLM sees architecture without tool call.
|
||||||
|
|
||||||
**Проблема:** Circular deps вычисляются, но не показываются в контексте
|
---
|
||||||
**Решение:** Показать циклы сразу
|
|
||||||
|
## 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
|
||||||
|
**Solution:** Show cycles immediately
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
// Добавить в initial context:
|
// Add to initial context:
|
||||||
|
|
||||||
// ## ⚠️ Circular Dependencies
|
// ## ⚠️ Circular Dependencies
|
||||||
// - services/user → services/auth → services/user
|
// - services/user → services/auth → services/user
|
||||||
// - utils/a → utils/b → utils/c → utils/a
|
// - utils/a → utils/b → utils/c → utils/a
|
||||||
```
|
```
|
||||||
|
|
||||||
**Изменения:**
|
**Changes:**
|
||||||
- [ ] Добавить `formatCircularDeps()` в prompts.ts
|
- [x] Add `formatCircularDeps()` to prompts.ts
|
||||||
- [ ] Получать circular deps из IndexBuilder
|
- [x] Add `includeCircularDeps: boolean` config option (default: true)
|
||||||
- [ ] Хранить в Redis как отдельный ключ или в meta
|
- [x] Add `circularDeps: string[][]` parameter to `BuildContextOptions`
|
||||||
|
- [x] Integrate into `buildInitialContext()`
|
||||||
|
|
||||||
**Зачем:** LLM сразу видит проблемы архитектуры.
|
**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)
|
||||||
|
|
||||||
### 0.25.3 - Impact Score
|
**Why:** LLM immediately sees architecture problems.
|
||||||
|
|
||||||
**Проблема:** LLM не знает какие файлы критичные
|
---
|
||||||
**Решение:** Показать 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
|
||||||
|
**Solution:** Show impact score (% of codebase that depends on file)
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
// Добавить в initial context:
|
// Add to initial context:
|
||||||
|
|
||||||
// ## High Impact Files
|
// ## High Impact Files
|
||||||
// | File | Impact | Dependents |
|
// | File | Impact | Dependents |
|
||||||
@@ -1950,37 +1971,55 @@ export interface ScanResult {
|
|||||||
// | src/services/user.ts | 34% | 6 files |
|
// | src/services/user.ts | 34% | 6 files |
|
||||||
```
|
```
|
||||||
|
|
||||||
**Изменения:**
|
**Changes:**
|
||||||
- [ ] Добавить `impactScore: number` в FileMeta (0-100)
|
- [x] Add `impactScore: number` to FileMeta (0-100)
|
||||||
- [ ] Вычислять в MetaAnalyzer: (transitiveDepByCount / totalFiles) * 100
|
- [x] Compute in MetaAnalyzer: (dependents.length / (totalFiles - 1)) * 100
|
||||||
- [ ] Добавить `formatHighImpactFiles()` в prompts.ts
|
- [x] Add `formatHighImpactFiles()` to prompts.ts
|
||||||
- [ ] Показывать top-10 high impact files
|
- [x] Show top-10 high impact files
|
||||||
|
- [x] Add `includeHighImpactFiles` config option (default: true)
|
||||||
|
|
||||||
**Зачем:** LLM понимает какие файлы критичные для изменений.
|
**Tests:**
|
||||||
|
- [x] Unit tests for calculateImpactScore (9 tests)
|
||||||
|
- [x] Unit tests for formatHighImpactFiles (14 tests)
|
||||||
|
- [x] Unit tests for includeHighImpactFiles config (5 tests)
|
||||||
|
|
||||||
### 0.25.4 - Transitive Dependencies Count
|
**Why:** LLM understands which files are critical for changes.
|
||||||
|
|
||||||
**Проблема:** Сейчас считаем только прямые зависимости
|
---
|
||||||
**Решение:** Добавить транзитивные зависимости в meta
|
|
||||||
|
## Version 0.30.0 - Transitive Dependencies Count 🔢 ✅
|
||||||
|
|
||||||
|
**Priority:** MEDIUM
|
||||||
|
**Status:** Complete (v0.30.0 released)
|
||||||
|
|
||||||
|
### Description
|
||||||
|
|
||||||
|
**Problem:** Currently only counting direct dependencies
|
||||||
|
**Solution:** Add transitive dependencies to meta
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
// FileMeta additions:
|
// FileMeta additions:
|
||||||
interface FileMeta {
|
interface FileMeta {
|
||||||
// existing...
|
// existing...
|
||||||
transitiveDepCount: number; // сколько файлов зависит от этого (транзитивно)
|
transitiveDepCount: number; // how many files depend on this (transitively)
|
||||||
transitiveDepByCount: number; // от скольких файлов зависит этот (транзитивно)
|
transitiveDepByCount: number; // how many files this depends on (transitively)
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
**Изменения:**
|
**Changes:**
|
||||||
- [ ] Добавить `computeTransitiveDeps()` в MetaAnalyzer
|
- [x] Add `transitiveDepCount` and `transitiveDepByCount` to FileMeta
|
||||||
- [ ] Использовать DFS с memoization для эффективности
|
- [x] Add `computeTransitiveCounts()` to MetaAnalyzer
|
||||||
- [ ] Сохранять в FileMeta
|
- [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:**
|
**Tests:**
|
||||||
- [ ] Unit tests for graph metrics computation
|
- [x] Unit tests for transitive dependencies computation (14 tests)
|
||||||
- [ ] Unit tests for new context sections
|
- [x] Tests for circular dependencies
|
||||||
- [ ] Performance tests for large codebases
|
- [x] Tests for diamond dependency patterns
|
||||||
|
- [x] Tests for deep dependency chains
|
||||||
|
- [x] Cache behavior tests
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -1995,12 +2034,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.5% branches, 97.58% lines, 98.64% functions, 97.58% statements - 1840 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 ✅
|
||||||
- [ ] Rich initial context (v0.24.0) — function signatures, interface fields, enum values
|
- [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
|
- [x] Graph metrics in context (v0.27.0-v0.30.0) — dependency graph ✅, circular deps ✅, impact score ✅, transitive deps ✅
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -2077,9 +2116,9 @@ sessions:list # List<session_id>
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
**Last Updated:** 2025-12-04
|
**Last Updated:** 2025-12-05
|
||||||
**Target Version:** 1.0.0
|
**Target Version:** 1.0.0
|
||||||
**Current Version:** 0.24.0
|
**Current Version:** 0.30.0
|
||||||
**Next Milestones:** v0.24.0 (Rich Context - 1/4 complete), v0.25.0 (Graph Metrics)
|
**Next Milestones:** v1.0.0 (Production Ready)
|
||||||
|
|
||||||
> **Note:** v0.24.0 and v0.25.0 are required for 1.0.0 release. They enable LLM to answer questions about types, signatures, and architecture without tool calls.
|
> **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",
|
"name": "@samiyev/ipuaro",
|
||||||
"version": "0.24.0",
|
"version": "0.30.1",
|
||||||
"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",
|
||||||
|
|||||||
@@ -52,6 +52,8 @@ export interface FunctionInfo {
|
|||||||
isExported: boolean
|
isExported: boolean
|
||||||
/** Return type (if available) */
|
/** Return type (if available) */
|
||||||
returnType?: string
|
returnType?: string
|
||||||
|
/** Decorators applied to the function (e.g., ["@Get(':id')", "@Auth()"]) */
|
||||||
|
decorators?: string[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MethodInfo {
|
export interface MethodInfo {
|
||||||
@@ -69,6 +71,8 @@ export interface MethodInfo {
|
|||||||
visibility: "public" | "private" | "protected"
|
visibility: "public" | "private" | "protected"
|
||||||
/** Whether it's static */
|
/** Whether it's static */
|
||||||
isStatic: boolean
|
isStatic: boolean
|
||||||
|
/** Decorators applied to the method (e.g., ["@Get(':id')", "@UseGuards(AuthGuard)"]) */
|
||||||
|
decorators?: string[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PropertyInfo {
|
export interface PropertyInfo {
|
||||||
@@ -105,6 +109,8 @@ export interface ClassInfo {
|
|||||||
isExported: boolean
|
isExported: boolean
|
||||||
/** Whether class is abstract */
|
/** Whether class is abstract */
|
||||||
isAbstract: boolean
|
isAbstract: boolean
|
||||||
|
/** Decorators applied to the class (e.g., ["@Controller('users')", "@Injectable()"]) */
|
||||||
|
decorators?: string[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface InterfaceInfo {
|
export interface InterfaceInfo {
|
||||||
@@ -129,6 +135,30 @@ export interface TypeAliasInfo {
|
|||||||
line: number
|
line: number
|
||||||
/** Whether it's exported */
|
/** Whether it's exported */
|
||||||
isExported: boolean
|
isExported: boolean
|
||||||
|
/** Type definition (e.g., "string", "User & Admin", "{ id: string }") */
|
||||||
|
definition?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface EnumMemberInfo {
|
||||||
|
/** Member name */
|
||||||
|
name: string
|
||||||
|
/** Member value (string or number, if specified) */
|
||||||
|
value?: string | number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface EnumInfo {
|
||||||
|
/** Enum name */
|
||||||
|
name: string
|
||||||
|
/** Start line number */
|
||||||
|
lineStart: number
|
||||||
|
/** End line number */
|
||||||
|
lineEnd: number
|
||||||
|
/** Enum members with values */
|
||||||
|
members: EnumMemberInfo[]
|
||||||
|
/** Whether it's exported */
|
||||||
|
isExported: boolean
|
||||||
|
/** Whether it's a const enum */
|
||||||
|
isConst: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface FileAST {
|
export interface FileAST {
|
||||||
@@ -144,6 +174,8 @@ export interface FileAST {
|
|||||||
interfaces: InterfaceInfo[]
|
interfaces: InterfaceInfo[]
|
||||||
/** Type alias declarations */
|
/** Type alias declarations */
|
||||||
typeAliases: TypeAliasInfo[]
|
typeAliases: TypeAliasInfo[]
|
||||||
|
/** Enum declarations */
|
||||||
|
enums: EnumInfo[]
|
||||||
/** Whether parsing encountered errors */
|
/** Whether parsing encountered errors */
|
||||||
parseError: boolean
|
parseError: boolean
|
||||||
/** Parse error message if any */
|
/** Parse error message if any */
|
||||||
@@ -158,6 +190,7 @@ export function createEmptyFileAST(): FileAST {
|
|||||||
classes: [],
|
classes: [],
|
||||||
interfaces: [],
|
interfaces: [],
|
||||||
typeAliases: [],
|
typeAliases: [],
|
||||||
|
enums: [],
|
||||||
parseError: false,
|
parseError: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,6 +26,12 @@ 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
|
||||||
|
/** 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 {
|
export function createFileMeta(partial: Partial<FileMeta> = {}): FileMeta {
|
||||||
@@ -41,6 +47,9 @@ export function createFileMeta(partial: Partial<FileMeta> = {}): FileMeta {
|
|||||||
isHub: false,
|
isHub: false,
|
||||||
isEntryPoint: false,
|
isEntryPoint: false,
|
||||||
fileType: "unknown",
|
fileType: "unknown",
|
||||||
|
impactScore: 0,
|
||||||
|
transitiveDepCount: 0,
|
||||||
|
transitiveDepByCount: 0,
|
||||||
...partial,
|
...partial,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -48,3 +57,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))
|
||||||
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import JSON from "tree-sitter-json"
|
|||||||
import * as yamlParser from "yaml"
|
import * as yamlParser from "yaml"
|
||||||
import {
|
import {
|
||||||
createEmptyFileAST,
|
createEmptyFileAST,
|
||||||
|
type EnumMemberInfo,
|
||||||
type ExportInfo,
|
type ExportInfo,
|
||||||
type FileAST,
|
type FileAST,
|
||||||
type ImportInfo,
|
type ImportInfo,
|
||||||
@@ -192,6 +193,11 @@ export class ASTParser {
|
|||||||
this.extractTypeAlias(node, ast, false)
|
this.extractTypeAlias(node, ast, false)
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
|
case NodeType.ENUM_DECLARATION:
|
||||||
|
if (isTypeScript) {
|
||||||
|
this.extractEnum(node, ast, false)
|
||||||
|
}
|
||||||
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -258,13 +264,15 @@ export class ASTParser {
|
|||||||
const declaration = node.childForFieldName(FieldName.DECLARATION)
|
const declaration = node.childForFieldName(FieldName.DECLARATION)
|
||||||
|
|
||||||
if (declaration) {
|
if (declaration) {
|
||||||
|
const decorators = this.extractDecoratorsFromSiblings(declaration)
|
||||||
|
|
||||||
switch (declaration.type) {
|
switch (declaration.type) {
|
||||||
case NodeType.FUNCTION_DECLARATION:
|
case NodeType.FUNCTION_DECLARATION:
|
||||||
this.extractFunction(declaration, ast, true)
|
this.extractFunction(declaration, ast, true, decorators)
|
||||||
this.addExportInfo(ast, declaration, "function", isDefault)
|
this.addExportInfo(ast, declaration, "function", isDefault)
|
||||||
break
|
break
|
||||||
case NodeType.CLASS_DECLARATION:
|
case NodeType.CLASS_DECLARATION:
|
||||||
this.extractClass(declaration, ast, true)
|
this.extractClass(declaration, ast, true, decorators)
|
||||||
this.addExportInfo(ast, declaration, "class", isDefault)
|
this.addExportInfo(ast, declaration, "class", isDefault)
|
||||||
break
|
break
|
||||||
case NodeType.INTERFACE_DECLARATION:
|
case NodeType.INTERFACE_DECLARATION:
|
||||||
@@ -275,6 +283,10 @@ export class ASTParser {
|
|||||||
this.extractTypeAlias(declaration, ast, true)
|
this.extractTypeAlias(declaration, ast, true)
|
||||||
this.addExportInfo(ast, declaration, "type", isDefault)
|
this.addExportInfo(ast, declaration, "type", isDefault)
|
||||||
break
|
break
|
||||||
|
case NodeType.ENUM_DECLARATION:
|
||||||
|
this.extractEnum(declaration, ast, true)
|
||||||
|
this.addExportInfo(ast, declaration, "type", isDefault)
|
||||||
|
break
|
||||||
case NodeType.LEXICAL_DECLARATION:
|
case NodeType.LEXICAL_DECLARATION:
|
||||||
this.extractLexicalDeclaration(declaration, ast, true)
|
this.extractLexicalDeclaration(declaration, ast, true)
|
||||||
break
|
break
|
||||||
@@ -299,7 +311,12 @@ export class ASTParser {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private extractFunction(node: SyntaxNode, ast: FileAST, isExported: boolean): void {
|
private extractFunction(
|
||||||
|
node: SyntaxNode,
|
||||||
|
ast: FileAST,
|
||||||
|
isExported: boolean,
|
||||||
|
externalDecorators: string[] = [],
|
||||||
|
): void {
|
||||||
const nameNode = node.childForFieldName(FieldName.NAME)
|
const nameNode = node.childForFieldName(FieldName.NAME)
|
||||||
if (!nameNode) {
|
if (!nameNode) {
|
||||||
return
|
return
|
||||||
@@ -309,6 +326,9 @@ export class ASTParser {
|
|||||||
const isAsync = node.children.some((c) => c.type === NodeType.ASYNC)
|
const isAsync = node.children.some((c) => c.type === NodeType.ASYNC)
|
||||||
const returnTypeNode = node.childForFieldName(FieldName.RETURN_TYPE)
|
const returnTypeNode = node.childForFieldName(FieldName.RETURN_TYPE)
|
||||||
|
|
||||||
|
const nodeDecorators = this.extractNodeDecorators(node)
|
||||||
|
const decorators = [...externalDecorators, ...nodeDecorators]
|
||||||
|
|
||||||
ast.functions.push({
|
ast.functions.push({
|
||||||
name: nameNode.text,
|
name: nameNode.text,
|
||||||
lineStart: node.startPosition.row + 1,
|
lineStart: node.startPosition.row + 1,
|
||||||
@@ -317,6 +337,7 @@ export class ASTParser {
|
|||||||
isAsync,
|
isAsync,
|
||||||
isExported,
|
isExported,
|
||||||
returnType: returnTypeNode?.text?.replace(/^:\s*/, ""),
|
returnType: returnTypeNode?.text?.replace(/^:\s*/, ""),
|
||||||
|
decorators,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -342,6 +363,7 @@ export class ASTParser {
|
|||||||
isAsync,
|
isAsync,
|
||||||
isExported,
|
isExported,
|
||||||
returnType: returnTypeNode?.text?.replace(/^:\s*/, ""),
|
returnType: returnTypeNode?.text?.replace(/^:\s*/, ""),
|
||||||
|
decorators: [],
|
||||||
})
|
})
|
||||||
|
|
||||||
if (isExported) {
|
if (isExported) {
|
||||||
@@ -364,7 +386,12 @@ export class ASTParser {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private extractClass(node: SyntaxNode, ast: FileAST, isExported: boolean): void {
|
private extractClass(
|
||||||
|
node: SyntaxNode,
|
||||||
|
ast: FileAST,
|
||||||
|
isExported: boolean,
|
||||||
|
externalDecorators: string[] = [],
|
||||||
|
): void {
|
||||||
const nameNode = node.childForFieldName(FieldName.NAME)
|
const nameNode = node.childForFieldName(FieldName.NAME)
|
||||||
if (!nameNode) {
|
if (!nameNode) {
|
||||||
return
|
return
|
||||||
@@ -375,14 +402,19 @@ export class ASTParser {
|
|||||||
const properties: PropertyInfo[] = []
|
const properties: PropertyInfo[] = []
|
||||||
|
|
||||||
if (body) {
|
if (body) {
|
||||||
|
let pendingDecorators: string[] = []
|
||||||
for (const member of body.children) {
|
for (const member of body.children) {
|
||||||
if (member.type === NodeType.METHOD_DEFINITION) {
|
if (member.type === NodeType.DECORATOR) {
|
||||||
methods.push(this.extractMethod(member))
|
pendingDecorators.push(this.formatDecorator(member))
|
||||||
|
} else if (member.type === NodeType.METHOD_DEFINITION) {
|
||||||
|
methods.push(this.extractMethod(member, pendingDecorators))
|
||||||
|
pendingDecorators = []
|
||||||
} else if (
|
} else if (
|
||||||
member.type === NodeType.PUBLIC_FIELD_DEFINITION ||
|
member.type === NodeType.PUBLIC_FIELD_DEFINITION ||
|
||||||
member.type === NodeType.FIELD_DEFINITION
|
member.type === NodeType.FIELD_DEFINITION
|
||||||
) {
|
) {
|
||||||
properties.push(this.extractProperty(member))
|
properties.push(this.extractProperty(member))
|
||||||
|
pendingDecorators = []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -390,6 +422,9 @@ export class ASTParser {
|
|||||||
const { extendsName, implementsList } = this.extractClassHeritage(node)
|
const { extendsName, implementsList } = this.extractClassHeritage(node)
|
||||||
const isAbstract = node.children.some((c) => c.type === NodeType.ABSTRACT)
|
const isAbstract = node.children.some((c) => c.type === NodeType.ABSTRACT)
|
||||||
|
|
||||||
|
const nodeDecorators = this.extractNodeDecorators(node)
|
||||||
|
const decorators = [...externalDecorators, ...nodeDecorators]
|
||||||
|
|
||||||
ast.classes.push({
|
ast.classes.push({
|
||||||
name: nameNode.text,
|
name: nameNode.text,
|
||||||
lineStart: node.startPosition.row + 1,
|
lineStart: node.startPosition.row + 1,
|
||||||
@@ -400,6 +435,7 @@ export class ASTParser {
|
|||||||
implements: implementsList,
|
implements: implementsList,
|
||||||
isExported,
|
isExported,
|
||||||
isAbstract,
|
isAbstract,
|
||||||
|
decorators,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -453,7 +489,7 @@ export class ASTParser {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private extractMethod(node: SyntaxNode): MethodInfo {
|
private extractMethod(node: SyntaxNode, decorators: string[] = []): MethodInfo {
|
||||||
const nameNode = node.childForFieldName(FieldName.NAME)
|
const nameNode = node.childForFieldName(FieldName.NAME)
|
||||||
const params = this.extractParameters(node)
|
const params = this.extractParameters(node)
|
||||||
const isAsync = node.children.some((c) => c.type === NodeType.ASYNC)
|
const isAsync = node.children.some((c) => c.type === NodeType.ASYNC)
|
||||||
@@ -475,6 +511,7 @@ export class ASTParser {
|
|||||||
isAsync,
|
isAsync,
|
||||||
visibility,
|
visibility,
|
||||||
isStatic,
|
isStatic,
|
||||||
|
decorators,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -554,13 +591,86 @@ export class ASTParser {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const valueNode = node.childForFieldName(FieldName.VALUE)
|
||||||
|
const definition = valueNode?.text
|
||||||
|
|
||||||
ast.typeAliases.push({
|
ast.typeAliases.push({
|
||||||
name: nameNode.text,
|
name: nameNode.text,
|
||||||
line: node.startPosition.row + 1,
|
line: node.startPosition.row + 1,
|
||||||
isExported,
|
isExported,
|
||||||
|
definition,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private extractEnum(node: SyntaxNode, ast: FileAST, isExported: boolean): void {
|
||||||
|
const nameNode = node.childForFieldName(FieldName.NAME)
|
||||||
|
if (!nameNode) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const body = node.childForFieldName(FieldName.BODY)
|
||||||
|
const members: EnumMemberInfo[] = []
|
||||||
|
|
||||||
|
if (body) {
|
||||||
|
for (const child of body.children) {
|
||||||
|
if (child.type === NodeType.ENUM_ASSIGNMENT) {
|
||||||
|
const memberName = child.childForFieldName(FieldName.NAME)
|
||||||
|
const memberValue = child.childForFieldName(FieldName.VALUE)
|
||||||
|
if (memberName) {
|
||||||
|
members.push({
|
||||||
|
name: memberName.text,
|
||||||
|
value: this.parseEnumValue(memberValue),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} else if (
|
||||||
|
child.type === NodeType.IDENTIFIER ||
|
||||||
|
child.type === NodeType.PROPERTY_IDENTIFIER
|
||||||
|
) {
|
||||||
|
members.push({
|
||||||
|
name: child.text,
|
||||||
|
value: undefined,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const isConst = node.children.some((c) => c.text === "const")
|
||||||
|
|
||||||
|
ast.enums.push({
|
||||||
|
name: nameNode.text,
|
||||||
|
lineStart: node.startPosition.row + 1,
|
||||||
|
lineEnd: node.endPosition.row + 1,
|
||||||
|
members,
|
||||||
|
isExported,
|
||||||
|
isConst,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
private parseEnumValue(valueNode: SyntaxNode | null): string | number | undefined {
|
||||||
|
if (!valueNode) {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
const text = valueNode.text
|
||||||
|
|
||||||
|
if (valueNode.type === "number") {
|
||||||
|
return Number(text)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (valueNode.type === "string") {
|
||||||
|
return this.getStringValue(valueNode)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (valueNode.type === "unary_expression" && text.startsWith("-")) {
|
||||||
|
const num = Number(text)
|
||||||
|
if (!isNaN(num)) {
|
||||||
|
return num
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return text
|
||||||
|
}
|
||||||
|
|
||||||
private extractParameters(node: SyntaxNode): ParameterInfo[] {
|
private extractParameters(node: SyntaxNode): ParameterInfo[] {
|
||||||
const params: ParameterInfo[] = []
|
const params: ParameterInfo[] = []
|
||||||
const paramsNode = node.childForFieldName(FieldName.PARAMETERS)
|
const paramsNode = node.childForFieldName(FieldName.PARAMETERS)
|
||||||
@@ -609,6 +719,49 @@ export class ASTParser {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format a decorator node to a string like "@Get(':id')" or "@Injectable()".
|
||||||
|
*/
|
||||||
|
private formatDecorator(node: SyntaxNode): string {
|
||||||
|
return node.text.replace(/\s+/g, " ").trim()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract decorators that are direct children of a node.
|
||||||
|
* In tree-sitter, decorators are children of the class/function declaration.
|
||||||
|
*/
|
||||||
|
private extractNodeDecorators(node: SyntaxNode): string[] {
|
||||||
|
const decorators: string[] = []
|
||||||
|
for (const child of node.children) {
|
||||||
|
if (child.type === NodeType.DECORATOR) {
|
||||||
|
decorators.push(this.formatDecorator(child))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return decorators
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract decorators from sibling nodes before the current node.
|
||||||
|
* Decorators appear as children before the declaration in export statements.
|
||||||
|
*/
|
||||||
|
private extractDecoratorsFromSiblings(node: SyntaxNode): string[] {
|
||||||
|
const decorators: string[] = []
|
||||||
|
const parent = node.parent
|
||||||
|
if (!parent) {
|
||||||
|
return decorators
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const sibling of parent.children) {
|
||||||
|
if (sibling.type === NodeType.DECORATOR) {
|
||||||
|
decorators.push(this.formatDecorator(sibling))
|
||||||
|
} else if (sibling === node) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return decorators
|
||||||
|
}
|
||||||
|
|
||||||
private classifyImport(from: string): ImportInfo["type"] {
|
private classifyImport(from: string): ImportInfo["type"] {
|
||||||
if (from.startsWith(".") || from.startsWith("/")) {
|
if (from.startsWith(".") || from.startsWith("/")) {
|
||||||
return "internal"
|
return "internal"
|
||||||
|
|||||||
@@ -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 and transitive dependencies 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,171 @@ 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compute transitive dependency counts
|
||||||
|
this.computeTransitiveCounts(results)
|
||||||
|
|
||||||
return 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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ export const NodeType = {
|
|||||||
CLASS_DECLARATION: "class_declaration",
|
CLASS_DECLARATION: "class_declaration",
|
||||||
INTERFACE_DECLARATION: "interface_declaration",
|
INTERFACE_DECLARATION: "interface_declaration",
|
||||||
TYPE_ALIAS_DECLARATION: "type_alias_declaration",
|
TYPE_ALIAS_DECLARATION: "type_alias_declaration",
|
||||||
|
ENUM_DECLARATION: "enum_declaration",
|
||||||
|
|
||||||
// Clauses
|
// Clauses
|
||||||
IMPORT_CLAUSE: "import_clause",
|
IMPORT_CLAUSE: "import_clause",
|
||||||
@@ -37,6 +38,11 @@ export const NodeType = {
|
|||||||
FIELD_DEFINITION: "field_definition",
|
FIELD_DEFINITION: "field_definition",
|
||||||
PROPERTY_SIGNATURE: "property_signature",
|
PROPERTY_SIGNATURE: "property_signature",
|
||||||
|
|
||||||
|
// Enum members
|
||||||
|
ENUM_BODY: "enum_body",
|
||||||
|
ENUM_ASSIGNMENT: "enum_assignment",
|
||||||
|
PROPERTY_IDENTIFIER: "property_identifier",
|
||||||
|
|
||||||
// Parameters
|
// Parameters
|
||||||
REQUIRED_PARAMETER: "required_parameter",
|
REQUIRED_PARAMETER: "required_parameter",
|
||||||
OPTIONAL_PARAMETER: "optional_parameter",
|
OPTIONAL_PARAMETER: "optional_parameter",
|
||||||
@@ -57,6 +63,9 @@ export const NodeType = {
|
|||||||
DEFAULT: "default",
|
DEFAULT: "default",
|
||||||
ACCESSIBILITY_MODIFIER: "accessibility_modifier",
|
ACCESSIBILITY_MODIFIER: "accessibility_modifier",
|
||||||
READONLY: "readonly",
|
READONLY: "readonly",
|
||||||
|
|
||||||
|
// Decorators
|
||||||
|
DECORATOR: "decorator",
|
||||||
} as const
|
} as const
|
||||||
|
|
||||||
export type NodeTypeValue = (typeof NodeType)[keyof typeof NodeType]
|
export type NodeTypeValue = (typeof NodeType)[keyof typeof NodeType]
|
||||||
|
|||||||
@@ -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")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -187,10 +215,22 @@ function formatFileOverview(
|
|||||||
return lines.join("\n")
|
return lines.join("\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format decorators as a prefix string.
|
||||||
|
* Example: "@Get(':id') @Auth() "
|
||||||
|
*/
|
||||||
|
function formatDecoratorsPrefix(decorators: string[] | undefined): string {
|
||||||
|
if (!decorators || decorators.length === 0) {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return `${decorators.join(" ")} `
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Format a function signature.
|
* Format a function signature.
|
||||||
*/
|
*/
|
||||||
function formatFunctionSignature(fn: FileAST["functions"][0]): string {
|
function formatFunctionSignature(fn: FileAST["functions"][0]): string {
|
||||||
|
const decoratorsPrefix = formatDecoratorsPrefix(fn.decorators)
|
||||||
const asyncPrefix = fn.isAsync ? "async " : ""
|
const asyncPrefix = fn.isAsync ? "async " : ""
|
||||||
const params = fn.params
|
const params = fn.params
|
||||||
.map((p) => {
|
.map((p) => {
|
||||||
@@ -200,7 +240,86 @@ function formatFunctionSignature(fn: FileAST["functions"][0]): string {
|
|||||||
})
|
})
|
||||||
.join(", ")
|
.join(", ")
|
||||||
const returnType = fn.returnType ? `: ${fn.returnType}` : ""
|
const returnType = fn.returnType ? `: ${fn.returnType}` : ""
|
||||||
return `${asyncPrefix}${fn.name}(${params})${returnType}`
|
return `${decoratorsPrefix}${asyncPrefix}${fn.name}(${params})${returnType}`
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format an interface signature with fields.
|
||||||
|
* Example: "interface User extends Base { id: string, name: string, email?: string }"
|
||||||
|
*/
|
||||||
|
function formatInterfaceSignature(iface: FileAST["interfaces"][0]): string {
|
||||||
|
const extList = iface.extends ?? []
|
||||||
|
const ext = extList.length > 0 ? ` extends ${extList.join(", ")}` : ""
|
||||||
|
|
||||||
|
if (iface.properties.length === 0) {
|
||||||
|
return `interface ${iface.name}${ext}`
|
||||||
|
}
|
||||||
|
|
||||||
|
const fields = iface.properties
|
||||||
|
.map((p) => {
|
||||||
|
const readonly = p.isReadonly ? "readonly " : ""
|
||||||
|
const optional = p.name.endsWith("?") ? "" : ""
|
||||||
|
const type = p.type ? `: ${p.type}` : ""
|
||||||
|
return `${readonly}${p.name}${optional}${type}`
|
||||||
|
})
|
||||||
|
.join(", ")
|
||||||
|
|
||||||
|
return `interface ${iface.name}${ext} { ${fields} }`
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format a type alias signature with definition.
|
||||||
|
* Example: "type UserId = string" or "type Handler = (event: Event) => void"
|
||||||
|
*/
|
||||||
|
function formatTypeAliasSignature(type: FileAST["typeAliases"][0]): string {
|
||||||
|
if (!type.definition) {
|
||||||
|
return `type ${type.name}`
|
||||||
|
}
|
||||||
|
|
||||||
|
const definition = truncateDefinition(type.definition, 80)
|
||||||
|
return `type ${type.name} = ${definition}`
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format an enum signature with members and values.
|
||||||
|
* Example: "enum Status { Active=1, Inactive=0, Pending=2 }"
|
||||||
|
* Example: "const enum Role { Admin="admin", User="user" }"
|
||||||
|
*/
|
||||||
|
function formatEnumSignature(enumInfo: FileAST["enums"][0]): string {
|
||||||
|
const constPrefix = enumInfo.isConst ? "const " : ""
|
||||||
|
|
||||||
|
if (enumInfo.members.length === 0) {
|
||||||
|
return `${constPrefix}enum ${enumInfo.name}`
|
||||||
|
}
|
||||||
|
|
||||||
|
const membersStr = enumInfo.members
|
||||||
|
.map((m) => {
|
||||||
|
if (m.value === undefined) {
|
||||||
|
return m.name
|
||||||
|
}
|
||||||
|
const valueStr = typeof m.value === "string" ? `"${m.value}"` : String(m.value)
|
||||||
|
return `${m.name}=${valueStr}`
|
||||||
|
})
|
||||||
|
.join(", ")
|
||||||
|
|
||||||
|
const result = `${constPrefix}enum ${enumInfo.name} { ${membersStr} }`
|
||||||
|
|
||||||
|
if (result.length > 100) {
|
||||||
|
return truncateDefinition(result, 100)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Truncate long type definitions for display.
|
||||||
|
*/
|
||||||
|
function truncateDefinition(definition: string, maxLength: number): string {
|
||||||
|
const normalized = definition.replace(/\s+/g, " ").trim()
|
||||||
|
if (normalized.length <= maxLength) {
|
||||||
|
return normalized
|
||||||
|
}
|
||||||
|
return `${normalized.slice(0, maxLength - 3)}...`
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -231,23 +350,28 @@ function formatFileSummary(
|
|||||||
|
|
||||||
if (ast.classes.length > 0) {
|
if (ast.classes.length > 0) {
|
||||||
for (const cls of ast.classes) {
|
for (const cls of ast.classes) {
|
||||||
|
const decoratorsPrefix = formatDecoratorsPrefix(cls.decorators)
|
||||||
const ext = cls.extends ? ` extends ${cls.extends}` : ""
|
const ext = cls.extends ? ` extends ${cls.extends}` : ""
|
||||||
const impl = cls.implements.length > 0 ? ` implements ${cls.implements.join(", ")}` : ""
|
const impl = cls.implements.length > 0 ? ` implements ${cls.implements.join(", ")}` : ""
|
||||||
lines.push(`- class ${cls.name}${ext}${impl}`)
|
lines.push(`- ${decoratorsPrefix}class ${cls.name}${ext}${impl}`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ast.interfaces.length > 0) {
|
if (ast.interfaces.length > 0) {
|
||||||
for (const iface of ast.interfaces) {
|
for (const iface of ast.interfaces) {
|
||||||
const extList = iface.extends ?? []
|
lines.push(`- ${formatInterfaceSignature(iface)}`)
|
||||||
const ext = extList.length > 0 ? ` extends ${extList.join(", ")}` : ""
|
|
||||||
lines.push(`- interface ${iface.name}${ext}`)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ast.typeAliases.length > 0) {
|
if (ast.typeAliases.length > 0) {
|
||||||
for (const type of ast.typeAliases) {
|
for (const type of ast.typeAliases) {
|
||||||
lines.push(`- type ${type.name}`)
|
lines.push(`- ${formatTypeAliasSignature(type)}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ast.enums && ast.enums.length > 0) {
|
||||||
|
for (const enumInfo of ast.enums) {
|
||||||
|
lines.push(`- ${formatEnumSignature(enumInfo)}`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -284,6 +408,11 @@ function formatFileSummaryCompact(path: string, ast: FileAST, flags: string): st
|
|||||||
parts.push(`type: ${names}`)
|
parts.push(`type: ${names}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (ast.enums && ast.enums.length > 0) {
|
||||||
|
const names = ast.enums.map((e) => e.name).join(", ")
|
||||||
|
parts.push(`enum: ${names}`)
|
||||||
|
}
|
||||||
|
|
||||||
const summary = parts.length > 0 ? ` [${parts.join(" | ")}]` : ""
|
const summary = parts.length > 0 ? ` [${parts.join(" | ")}]` : ""
|
||||||
return `- ${path}${summary}${flags}`
|
return `- ${path}${summary}${flags}`
|
||||||
}
|
}
|
||||||
@@ -313,6 +442,220 @@ 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).
|
||||||
|
* Includes both direct and transitive dependent counts.
|
||||||
|
*
|
||||||
|
* Format:
|
||||||
|
* ## High Impact Files
|
||||||
|
* | File | Impact | Direct | Transitive |
|
||||||
|
* |------|--------|--------|------------|
|
||||||
|
* | src/utils/validation.ts | 67% | 12 | 24 |
|
||||||
|
*
|
||||||
|
* @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
|
||||||
|
transitive: number
|
||||||
|
}[] = []
|
||||||
|
|
||||||
|
for (const [path, meta] of metas) {
|
||||||
|
if (meta.impactScore >= minImpact) {
|
||||||
|
impactFiles.push({
|
||||||
|
path,
|
||||||
|
impact: meta.impactScore,
|
||||||
|
dependents: meta.dependents.length,
|
||||||
|
transitive: meta.transitiveDepCount,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (impactFiles.length === 0) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort by transitive count descending, then by impact, then by path
|
||||||
|
impactFiles.sort((a, b) => {
|
||||||
|
if (a.transitive !== b.transitive) {
|
||||||
|
return b.transitive - a.transitive
|
||||||
|
}
|
||||||
|
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 | Direct | Transitive |",
|
||||||
|
"|------|--------|--------|------------|",
|
||||||
|
]
|
||||||
|
|
||||||
|
for (const file of topFiles) {
|
||||||
|
const shortPath = shortenPath(file.path)
|
||||||
|
const impact = `${String(file.impact)}%`
|
||||||
|
const direct = String(file.dependents)
|
||||||
|
const transitive = String(file.transitive)
|
||||||
|
lines.push(`| ${shortPath} | ${impact} | ${direct} | ${transitive} |`)
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -224,6 +224,62 @@ describe("ASTParser", () => {
|
|||||||
const ast = parser.parse(code, "ts")
|
const ast = parser.parse(code, "ts")
|
||||||
expect(ast.typeAliases[0].isExported).toBe(true)
|
expect(ast.typeAliases[0].isExported).toBe(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it("should extract type alias definition (simple)", () => {
|
||||||
|
const code = `type UserId = string`
|
||||||
|
const ast = parser.parse(code, "ts")
|
||||||
|
expect(ast.typeAliases).toHaveLength(1)
|
||||||
|
expect(ast.typeAliases[0].definition).toBe("string")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should extract type alias definition (union)", () => {
|
||||||
|
const code = `type Status = "pending" | "active" | "done"`
|
||||||
|
const ast = parser.parse(code, "ts")
|
||||||
|
expect(ast.typeAliases).toHaveLength(1)
|
||||||
|
expect(ast.typeAliases[0].definition).toBe('"pending" | "active" | "done"')
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should extract type alias definition (intersection)", () => {
|
||||||
|
const code = `type AdminUser = User & Admin`
|
||||||
|
const ast = parser.parse(code, "ts")
|
||||||
|
expect(ast.typeAliases).toHaveLength(1)
|
||||||
|
expect(ast.typeAliases[0].definition).toBe("User & Admin")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should extract type alias definition (object type)", () => {
|
||||||
|
const code = `type Point = { x: number; y: number }`
|
||||||
|
const ast = parser.parse(code, "ts")
|
||||||
|
expect(ast.typeAliases).toHaveLength(1)
|
||||||
|
expect(ast.typeAliases[0].definition).toBe("{ x: number; y: number }")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should extract type alias definition (function type)", () => {
|
||||||
|
const code = `type Handler = (event: Event) => void`
|
||||||
|
const ast = parser.parse(code, "ts")
|
||||||
|
expect(ast.typeAliases).toHaveLength(1)
|
||||||
|
expect(ast.typeAliases[0].definition).toBe("(event: Event) => void")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should extract type alias definition (generic)", () => {
|
||||||
|
const code = `type Result<T> = { success: boolean; data: T }`
|
||||||
|
const ast = parser.parse(code, "ts")
|
||||||
|
expect(ast.typeAliases).toHaveLength(1)
|
||||||
|
expect(ast.typeAliases[0].definition).toBe("{ success: boolean; data: T }")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should extract type alias definition (array)", () => {
|
||||||
|
const code = `type UserIds = string[]`
|
||||||
|
const ast = parser.parse(code, "ts")
|
||||||
|
expect(ast.typeAliases).toHaveLength(1)
|
||||||
|
expect(ast.typeAliases[0].definition).toBe("string[]")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should extract type alias definition (tuple)", () => {
|
||||||
|
const code = `type Pair = [string, number]`
|
||||||
|
const ast = parser.parse(code, "ts")
|
||||||
|
expect(ast.typeAliases).toHaveLength(1)
|
||||||
|
expect(ast.typeAliases[0].definition).toBe("[string, number]")
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe("exports", () => {
|
describe("exports", () => {
|
||||||
@@ -506,4 +562,274 @@ third: value3`
|
|||||||
expect(ast.exports[2].line).toBe(3)
|
expect(ast.exports[2].line).toBe(3)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe("enums (0.24.3)", () => {
|
||||||
|
it("should extract enum with numeric values", () => {
|
||||||
|
const code = `enum Status {
|
||||||
|
Active = 1,
|
||||||
|
Inactive = 0,
|
||||||
|
Pending = 2
|
||||||
|
}`
|
||||||
|
const ast = parser.parse(code, "ts")
|
||||||
|
|
||||||
|
expect(ast.enums).toHaveLength(1)
|
||||||
|
expect(ast.enums[0]).toMatchObject({
|
||||||
|
name: "Status",
|
||||||
|
isExported: false,
|
||||||
|
isConst: false,
|
||||||
|
})
|
||||||
|
expect(ast.enums[0].members).toHaveLength(3)
|
||||||
|
expect(ast.enums[0].members[0]).toMatchObject({ name: "Active", value: 1 })
|
||||||
|
expect(ast.enums[0].members[1]).toMatchObject({ name: "Inactive", value: 0 })
|
||||||
|
expect(ast.enums[0].members[2]).toMatchObject({ name: "Pending", value: 2 })
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should extract enum with string values", () => {
|
||||||
|
const code = `enum Role {
|
||||||
|
Admin = "admin",
|
||||||
|
User = "user",
|
||||||
|
Guest = "guest"
|
||||||
|
}`
|
||||||
|
const ast = parser.parse(code, "ts")
|
||||||
|
|
||||||
|
expect(ast.enums).toHaveLength(1)
|
||||||
|
expect(ast.enums[0].members).toHaveLength(3)
|
||||||
|
expect(ast.enums[0].members[0]).toMatchObject({ name: "Admin", value: "admin" })
|
||||||
|
expect(ast.enums[0].members[1]).toMatchObject({ name: "User", value: "user" })
|
||||||
|
expect(ast.enums[0].members[2]).toMatchObject({ name: "Guest", value: "guest" })
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should extract enum without explicit values", () => {
|
||||||
|
const code = `enum Direction {
|
||||||
|
Up,
|
||||||
|
Down,
|
||||||
|
Left,
|
||||||
|
Right
|
||||||
|
}`
|
||||||
|
const ast = parser.parse(code, "ts")
|
||||||
|
|
||||||
|
expect(ast.enums).toHaveLength(1)
|
||||||
|
expect(ast.enums[0].members).toHaveLength(4)
|
||||||
|
expect(ast.enums[0].members[0]).toMatchObject({ name: "Up", value: undefined })
|
||||||
|
expect(ast.enums[0].members[1]).toMatchObject({ name: "Down", value: undefined })
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should extract exported enum", () => {
|
||||||
|
const code = `export enum Color {
|
||||||
|
Red = "#FF0000",
|
||||||
|
Green = "#00FF00",
|
||||||
|
Blue = "#0000FF"
|
||||||
|
}`
|
||||||
|
const ast = parser.parse(code, "ts")
|
||||||
|
|
||||||
|
expect(ast.enums).toHaveLength(1)
|
||||||
|
expect(ast.enums[0].isExported).toBe(true)
|
||||||
|
expect(ast.exports).toHaveLength(1)
|
||||||
|
expect(ast.exports[0].kind).toBe("type")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should extract const enum", () => {
|
||||||
|
const code = `const enum HttpStatus {
|
||||||
|
OK = 200,
|
||||||
|
NotFound = 404,
|
||||||
|
InternalError = 500
|
||||||
|
}`
|
||||||
|
const ast = parser.parse(code, "ts")
|
||||||
|
|
||||||
|
expect(ast.enums).toHaveLength(1)
|
||||||
|
expect(ast.enums[0].isConst).toBe(true)
|
||||||
|
expect(ast.enums[0].members[0]).toMatchObject({ name: "OK", value: 200 })
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should extract exported const enum", () => {
|
||||||
|
const code = `export const enum LogLevel {
|
||||||
|
Debug = 0,
|
||||||
|
Info = 1,
|
||||||
|
Warn = 2,
|
||||||
|
Error = 3
|
||||||
|
}`
|
||||||
|
const ast = parser.parse(code, "ts")
|
||||||
|
|
||||||
|
expect(ast.enums).toHaveLength(1)
|
||||||
|
expect(ast.enums[0].isExported).toBe(true)
|
||||||
|
expect(ast.enums[0].isConst).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should extract line range for enum", () => {
|
||||||
|
const code = `enum Test {
|
||||||
|
A = 1,
|
||||||
|
B = 2
|
||||||
|
}`
|
||||||
|
const ast = parser.parse(code, "ts")
|
||||||
|
|
||||||
|
expect(ast.enums[0].lineStart).toBe(1)
|
||||||
|
expect(ast.enums[0].lineEnd).toBe(4)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should handle enum with negative values", () => {
|
||||||
|
const code = `enum Temperature {
|
||||||
|
Cold = -10,
|
||||||
|
Freezing = -20,
|
||||||
|
Hot = 40
|
||||||
|
}`
|
||||||
|
const ast = parser.parse(code, "ts")
|
||||||
|
|
||||||
|
expect(ast.enums).toHaveLength(1)
|
||||||
|
expect(ast.enums[0].members[0]).toMatchObject({ name: "Cold", value: -10 })
|
||||||
|
expect(ast.enums[0].members[1]).toMatchObject({ name: "Freezing", value: -20 })
|
||||||
|
expect(ast.enums[0].members[2]).toMatchObject({ name: "Hot", value: 40 })
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should handle empty enum", () => {
|
||||||
|
const code = `enum Empty {}`
|
||||||
|
const ast = parser.parse(code, "ts")
|
||||||
|
|
||||||
|
expect(ast.enums).toHaveLength(1)
|
||||||
|
expect(ast.enums[0].name).toBe("Empty")
|
||||||
|
expect(ast.enums[0].members).toHaveLength(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should not extract enum from JavaScript", () => {
|
||||||
|
const code = `enum Status { Active = 1 }`
|
||||||
|
const ast = parser.parse(code, "js")
|
||||||
|
|
||||||
|
expect(ast.enums).toHaveLength(0)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("decorators (0.24.4)", () => {
|
||||||
|
it("should extract class decorator", () => {
|
||||||
|
const code = `@Controller('users')
|
||||||
|
class UserController {}`
|
||||||
|
const ast = parser.parse(code, "ts")
|
||||||
|
|
||||||
|
expect(ast.classes).toHaveLength(1)
|
||||||
|
expect(ast.classes[0].decorators).toHaveLength(1)
|
||||||
|
expect(ast.classes[0].decorators[0]).toBe("@Controller('users')")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should extract multiple class decorators", () => {
|
||||||
|
const code = `@Controller('api')
|
||||||
|
@Injectable()
|
||||||
|
@UseGuards(AuthGuard)
|
||||||
|
class ApiController {}`
|
||||||
|
const ast = parser.parse(code, "ts")
|
||||||
|
|
||||||
|
expect(ast.classes).toHaveLength(1)
|
||||||
|
expect(ast.classes[0].decorators).toHaveLength(3)
|
||||||
|
expect(ast.classes[0].decorators[0]).toBe("@Controller('api')")
|
||||||
|
expect(ast.classes[0].decorators[1]).toBe("@Injectable()")
|
||||||
|
expect(ast.classes[0].decorators[2]).toBe("@UseGuards(AuthGuard)")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should extract method decorators", () => {
|
||||||
|
const code = `class UserController {
|
||||||
|
@Get(':id')
|
||||||
|
@Auth()
|
||||||
|
async getUser() {}
|
||||||
|
}`
|
||||||
|
const ast = parser.parse(code, "ts")
|
||||||
|
|
||||||
|
expect(ast.classes).toHaveLength(1)
|
||||||
|
expect(ast.classes[0].methods).toHaveLength(1)
|
||||||
|
expect(ast.classes[0].methods[0].decorators).toHaveLength(2)
|
||||||
|
expect(ast.classes[0].methods[0].decorators[0]).toBe("@Get(':id')")
|
||||||
|
expect(ast.classes[0].methods[0].decorators[1]).toBe("@Auth()")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should extract exported decorated class", () => {
|
||||||
|
const code = `@Injectable()
|
||||||
|
export class UserService {}`
|
||||||
|
const ast = parser.parse(code, "ts")
|
||||||
|
|
||||||
|
expect(ast.classes).toHaveLength(1)
|
||||||
|
expect(ast.classes[0].isExported).toBe(true)
|
||||||
|
expect(ast.classes[0].decorators).toHaveLength(1)
|
||||||
|
expect(ast.classes[0].decorators[0]).toBe("@Injectable()")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should extract decorator with complex arguments", () => {
|
||||||
|
const code = `@Module({
|
||||||
|
imports: [UserModule],
|
||||||
|
controllers: [AppController],
|
||||||
|
providers: [AppService]
|
||||||
|
})
|
||||||
|
class AppModule {}`
|
||||||
|
const ast = parser.parse(code, "ts")
|
||||||
|
|
||||||
|
expect(ast.classes).toHaveLength(1)
|
||||||
|
expect(ast.classes[0].decorators).toHaveLength(1)
|
||||||
|
expect(ast.classes[0].decorators[0]).toContain("@Module")
|
||||||
|
expect(ast.classes[0].decorators[0]).toContain("imports")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should extract decorated class with extends", () => {
|
||||||
|
const code = `@Entity()
|
||||||
|
class User extends BaseEntity {}`
|
||||||
|
const ast = parser.parse(code, "ts")
|
||||||
|
|
||||||
|
expect(ast.classes).toHaveLength(1)
|
||||||
|
expect(ast.classes[0].extends).toBe("BaseEntity")
|
||||||
|
expect(ast.classes[0].decorators).toHaveLength(1)
|
||||||
|
expect(ast.classes[0].decorators![0]).toBe("@Entity()")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should handle class without decorators", () => {
|
||||||
|
const code = `class SimpleClass {}`
|
||||||
|
const ast = parser.parse(code, "ts")
|
||||||
|
|
||||||
|
expect(ast.classes).toHaveLength(1)
|
||||||
|
expect(ast.classes[0].decorators).toHaveLength(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should handle method without decorators", () => {
|
||||||
|
const code = `class SimpleClass {
|
||||||
|
simpleMethod() {}
|
||||||
|
}`
|
||||||
|
const ast = parser.parse(code, "ts")
|
||||||
|
|
||||||
|
expect(ast.classes).toHaveLength(1)
|
||||||
|
expect(ast.classes[0].methods).toHaveLength(1)
|
||||||
|
expect(ast.classes[0].methods[0].decorators).toHaveLength(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should handle function without decorators", () => {
|
||||||
|
const code = `function simpleFunc() {}`
|
||||||
|
const ast = parser.parse(code, "ts")
|
||||||
|
|
||||||
|
expect(ast.functions).toHaveLength(1)
|
||||||
|
expect(ast.functions[0].decorators).toHaveLength(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should handle arrow function without decorators", () => {
|
||||||
|
const code = `const arrowFn = () => {}`
|
||||||
|
const ast = parser.parse(code, "ts")
|
||||||
|
|
||||||
|
expect(ast.functions).toHaveLength(1)
|
||||||
|
expect(ast.functions[0].decorators).toHaveLength(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should extract NestJS controller pattern", () => {
|
||||||
|
const code = `@Controller('users')
|
||||||
|
export class UserController {
|
||||||
|
@Get()
|
||||||
|
findAll() {}
|
||||||
|
|
||||||
|
@Get(':id')
|
||||||
|
findOne() {}
|
||||||
|
|
||||||
|
@Post()
|
||||||
|
@Body()
|
||||||
|
create() {}
|
||||||
|
}`
|
||||||
|
const ast = parser.parse(code, "ts")
|
||||||
|
|
||||||
|
expect(ast.classes).toHaveLength(1)
|
||||||
|
expect(ast.classes[0].decorators).toContain("@Controller('users')")
|
||||||
|
expect(ast.classes[0].methods).toHaveLength(3)
|
||||||
|
expect(ast.classes[0].methods[0].decorators).toContain("@Get()")
|
||||||
|
expect(ast.classes[0].methods[1].decorators).toContain("@Get(':id')")
|
||||||
|
expect(ast.classes[0].methods[2].decorators).toContain("@Post()")
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { MetaAnalyzer } from "../../../../src/infrastructure/indexer/MetaAnalyze
|
|||||||
import { ASTParser } from "../../../../src/infrastructure/indexer/ASTParser.js"
|
import { ASTParser } from "../../../../src/infrastructure/indexer/ASTParser.js"
|
||||||
import type { FileAST } from "../../../../src/domain/value-objects/FileAST.js"
|
import type { FileAST } from "../../../../src/domain/value-objects/FileAST.js"
|
||||||
import { createEmptyFileAST } 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", () => {
|
describe("MetaAnalyzer", () => {
|
||||||
let analyzer: MetaAnalyzer
|
let analyzer: MetaAnalyzer
|
||||||
@@ -737,4 +738,368 @@ export function createComponent(): MyComponent {
|
|||||||
expect(meta.fileType).toBe("source")
|
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)
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -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