Compare commits

...

7 Commits

Author SHA1 Message Date
imfozilbek
d63d85d850 feat(ipuaro): add inline dependency graph to initial context
- Add formatDependencyGraph() to show file relationships in LLM context
- Add includeDepsGraph option to ContextConfigSchema (default: true)
- Format: "services/user: → types/user ← controllers/user"
- Hub files shown first, sorted by total connections
- 21 new tests for dependency graph functionality
2025-12-05 14:38:45 +05:00
imfozilbek
41cfc21f20 docs(ipuaro): align roadmap versions with package versions 2025-12-05 14:20:14 +05:00
imfozilbek
eeaa223436 chore(ipuaro): release v0.26.0 2025-12-05 13:51:13 +05:00
imfozilbek
36768c06d1 feat(ipuaro): add decorator extraction to initial context
Extract decorators from classes and methods for NestJS/Angular support.
Decorators are now shown in initial context:
- @Controller('users') class UserController
- @Get(':id') async getUser(id: string): Promise<User>

Changes:
- Add decorators field to FunctionInfo, MethodInfo, ClassInfo
- Update ASTParser to extract decorators from tree-sitter nodes
- Update formatFileSummary to display decorators prefix
- Add 18 unit tests for decorator extraction and formatting
2025-12-05 13:38:46 +05:00
imfozilbek
5a22cd5c9b feat(ipuaro): add enum value definitions to initial context
Extract enum declarations with member names and values from TypeScript
AST and display them in the initial LLM context. This allows the LLM
to know valid enum values without making tool calls.

Features:
- Numeric values (Active=1)
- String values (Admin="admin")
- Implicit values (Up, Down)
- Negative numbers (Cold=-10)
- const enum modifier
- export enum modifier
- Long enum truncation (>100 chars)

Adds EnumInfo and EnumMemberInfo interfaces, extractEnum() method in
ASTParser, formatEnumSignature() in prompts.ts, and 17 new unit tests.
2025-12-05 13:14:51 +05:00
imfozilbek
806c9281b0 chore(ipuaro): release v0.25.0 2025-12-04 22:49:35 +05:00
imfozilbek
12197a9624 feat(ipuaro): add interface fields and type alias definitions to context
- Add interface field display in initial context: interface User { id: string, name: string }
- Add type alias definition display: type UserId = string
- Support readonly fields, extends, union/intersection types
- Add definition field to TypeAliasInfo in FileAST
- Update ASTParser to extract type alias definitions
- Add formatInterfaceSignature() and formatTypeAliasSignature() helpers
- Truncate long type definitions at 80 characters
- Translate ROADMAP.md from Russian to English
- Add 18 new tests for interface fields and type aliases
2025-12-04 22:49:03 +05:00
11 changed files with 2434 additions and 158 deletions

View File

@@ -5,6 +5,184 @@ 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.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

View File

@@ -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,59 @@ 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:** Planned
### 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 - [ ] Add `formatCircularDeps()` to prompts.ts
- [ ] Получать circular deps из IndexBuilder - [ ] Get circular deps from IndexBuilder
- [ ] Хранить в Redis как отдельный ключ или в meta - [ ] Store in Redis as separate key or in meta
**Зачем:** LLM сразу видит проблемы архитектуры. **Why:** LLM immediately sees architecture problems.
### 0.25.3 - Impact Score ---
**Проблема:** LLM не знает какие файлы критичные ## Version 0.29.0 - Impact Score 📈
**Решение:** Показать impact score (% кодовой базы, который зависит от файла)
**Priority:** MEDIUM
**Status:** Planned
### 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,36 +1965,42 @@ export interface ScanResult {
// | src/services/user.ts | 34% | 6 files | // | src/services/user.ts | 34% | 6 files |
``` ```
**Изменения:** **Changes:**
- [ ] Добавить `impactScore: number` в FileMeta (0-100) - [ ] Add `impactScore: number` to FileMeta (0-100)
- [ ] Вычислять в MetaAnalyzer: (transitiveDepByCount / totalFiles) * 100 - [ ] Compute in MetaAnalyzer: (transitiveDepByCount / totalFiles) * 100
- [ ] Добавить `formatHighImpactFiles()` в prompts.ts - [ ] Add `formatHighImpactFiles()` to prompts.ts
- [ ] Показывать top-10 high impact files - [ ] Show top-10 high impact files
**Зачем:** LLM понимает какие файлы критичные для изменений. **Why:** LLM understands which files are critical for changes.
### 0.25.4 - Transitive Dependencies Count ---
**Проблема:** Сейчас считаем только прямые зависимости ## Version 0.30.0 - Transitive Dependencies Count 🔢
**Решение:** Добавить транзитивные зависимости в meta
**Priority:** MEDIUM
**Status:** Planned
### 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 - [ ] Add `computeTransitiveDeps()` to MetaAnalyzer
- [ ] Использовать DFS с memoization для эффективности - [ ] Use DFS with memoization for efficiency
- [ ] Сохранять в FileMeta - [ ] Store in FileMeta
**Tests:** **Tests:**
- [ ] Unit tests for graph metrics computation - [ ] Unit tests for transitive dependencies computation
- [ ] Unit tests for new context sections
- [ ] Performance tests for large codebases - [ ] Performance tests for large codebases
--- ---
@@ -1999,8 +2020,8 @@ interface FileMeta {
- [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 - [ ] Graph metrics in context (v0.27.0-v0.30.0) — dependency graph, circular deps, impact score, transitive deps
--- ---
@@ -2077,9 +2098,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.27.0
**Next Milestones:** v0.24.0 (Rich Context - 1/4 complete), v0.25.0 (Graph Metrics) **Next Milestones:** v0.28.0 (Circular Deps), v0.29.0 (Impact Score), v0.30.0 (Transitive Deps)
> **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 in progress (v0.27.0 ✅, v0.28.0-v0.30.0 pending) for 1.0.0 release.

View File

@@ -1,6 +1,6 @@
{ {
"name": "@samiyev/ipuaro", "name": "@samiyev/ipuaro",
"version": "0.24.0", "version": "0.27.0",
"description": "Local AI agent for codebase operations with infinite context feeling", "description": "Local AI agent for codebase operations with infinite context feeling",
"author": "Fozilbek Samiyev <fozilbek.samiyev@gmail.com>", "author": "Fozilbek Samiyev <fozilbek.samiyev@gmail.com>",
"license": "MIT", "license": "MIT",

View File

@@ -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,
} }
} }

View File

@@ -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"

View File

@@ -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]

View File

@@ -16,6 +16,7 @@ export interface ProjectStructure {
*/ */
export interface BuildContextOptions { export interface BuildContextOptions {
includeSignatures?: boolean includeSignatures?: boolean
includeDepsGraph?: boolean
} }
/** /**
@@ -127,11 +128,19 @@ 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
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)
}
}
return sections.join("\n\n") return sections.join("\n\n")
} }
@@ -187,10 +196,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 +221,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 +331,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 +389,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 +423,109 @@ 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 line range for display. * Format line range for display.
*/ */

View File

@@ -115,6 +115,7 @@ 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),
}) })
/** /**

View File

@@ -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()")
})
})
}) })

File diff suppressed because it is too large Load Diff

View File

@@ -16,6 +16,7 @@ describe("ContextConfigSchema", () => {
autoCompressAt: 0.8, autoCompressAt: 0.8,
compressionMethod: "llm-summary", compressionMethod: "llm-summary",
includeSignatures: true, includeSignatures: true,
includeDepsGraph: true,
}) })
}) })
@@ -28,6 +29,7 @@ describe("ContextConfigSchema", () => {
autoCompressAt: 0.8, autoCompressAt: 0.8,
compressionMethod: "llm-summary", compressionMethod: "llm-summary",
includeSignatures: true, includeSignatures: true,
includeDepsGraph: true,
}) })
}) })
}) })
@@ -165,6 +167,7 @@ describe("ContextConfigSchema", () => {
autoCompressAt: 0.8, autoCompressAt: 0.8,
compressionMethod: "llm-summary", compressionMethod: "llm-summary",
includeSignatures: true, includeSignatures: true,
includeDepsGraph: true,
}) })
}) })
@@ -179,6 +182,7 @@ describe("ContextConfigSchema", () => {
autoCompressAt: 0.9, autoCompressAt: 0.9,
compressionMethod: "llm-summary", compressionMethod: "llm-summary",
includeSignatures: true, includeSignatures: true,
includeDepsGraph: true,
}) })
}) })
@@ -194,6 +198,7 @@ describe("ContextConfigSchema", () => {
autoCompressAt: 0.8, autoCompressAt: 0.8,
compressionMethod: "truncate", compressionMethod: "truncate",
includeSignatures: true, includeSignatures: true,
includeDepsGraph: true,
}) })
}) })
}) })
@@ -206,6 +211,7 @@ describe("ContextConfigSchema", () => {
autoCompressAt: 0.85, autoCompressAt: 0.85,
compressionMethod: "truncate" as const, compressionMethod: "truncate" as const,
includeSignatures: false, includeSignatures: false,
includeDepsGraph: false,
} }
const result = ContextConfigSchema.parse(config) const result = ContextConfigSchema.parse(config)
@@ -219,6 +225,7 @@ describe("ContextConfigSchema", () => {
autoCompressAt: 0.8, autoCompressAt: 0.8,
compressionMethod: "llm-summary" as const, compressionMethod: "llm-summary" as const,
includeSignatures: true, includeSignatures: true,
includeDepsGraph: true,
} }
const result = ContextConfigSchema.parse(config) const result = ContextConfigSchema.parse(config)
@@ -250,4 +257,29 @@ 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()
})
})
}) })