Compare commits

...

5 Commits

Author SHA1 Message Date
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
imfozilbek
1489b69e69 chore(ipuaro): release v0.24.0 2025-12-04 22:29:31 +05:00
imfozilbek
2dcb22812c feat(ipuaro): add function signatures to initial context
- Add full function signatures with parameter types and return types
- Arrow functions now extract returnType in ASTParser
- New formatFunctionSignature() helper in prompts.ts
- Add includeSignatures config option (default: true)
- Support compact format when includeSignatures: false
- 15 new tests, coverage 91.14% branches
2025-12-04 22:29:02 +05:00
imfozilbek
7d7c99fe4d docs(ipuaro): add v0.24.0 and v0.25.0 to roadmap for rich context
Add two new milestones before 1.0.0 release:

v0.24.0 - Rich Initial Context:
- Function signatures with types
- Interface/Type field definitions
- Enum value definitions
- Decorator extraction

v0.25.0 - Graph Metrics in Context:
- Inline dependency graph
- Circular dependencies display
- Impact score for critical files
- Transitive dependencies count

Update 1.0.0 checklist to require both milestones.
Update context budget table with new token estimates.
2025-12-04 22:07:38 +05:00
10 changed files with 1420 additions and 67 deletions

View File

@@ -5,6 +5,143 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [0.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
### Added
- **Function Signatures in Context (0.24.1)**
- Full function signatures with parameter types and return types in initial context
- New format: `async getUser(id: string): Promise<User>` instead of `fn: getUser`
- Classes show inheritance: `class UserService extends BaseService implements IService`
- Interfaces show extends: `interface AdminUser extends User, Admin`
- Optional parameters marked with `?`: `format(value: string, options?: FormatOptions)`
- **BuildContextOptions Interface**
- New `includeSignatures?: boolean` option for `buildInitialContext()`
- Controls signature vs compact format (default: `true` for signatures)
- **Configuration**
- Added `includeSignatures: boolean` to `ContextConfigSchema` (default: `true`)
- Users can disable signatures to save tokens: `context.includeSignatures: false`
### Changed
- **ASTParser**
- Arrow functions now extract `returnType` in `extractLexicalDeclaration()`
- Return type format normalized (strips leading `: `)
- **prompts.ts**
- New `formatFunctionSignature()` helper function
- `formatFileSummary()` now shows full signatures by default
- Added `formatFileSummaryCompact()` for legacy format
- `formatFileOverview()` accepts `includeSignatures` parameter
- Defensive handling for missing interface `extends` array
### New Context Format (default)
```
### src/services/user.ts
- async getUser(id: string): Promise<User>
- async createUser(data: UserDTO): Promise<User>
- validateEmail(email: string): boolean
- class UserService extends BaseService
- interface IUserService extends IService
- type UserId
```
### Compact Format (includeSignatures: false)
```
- src/services/user.ts [fn: getUser, createUser | class: UserService | interface: IUserService | type: UserId]
```
### Technical Details
- Total tests: 1702 passed (was 1687, +15 new tests)
- 8 new tests for function signature formatting
- 5 new tests for `includeSignatures` configuration
- 1 new test for compact format
- 1 new test for undefined AST entries
- Coverage: 97.54% lines, 91.14% branches, 98.59% functions
- 0 ESLint errors, 2 warnings (complexity in ASTParser and prompts)
- Build successful
### Notes
This is the first part of 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
---
## [0.23.0] - 2025-12-04 - JSON/YAML & Symlinks
### Added

View File

@@ -1333,40 +1333,40 @@ class ErrorHandler {
**Priority:** HIGH
**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
```typescript
// src/infrastructure/llm/OllamaClient.ts
// БЫЛО:
// - Передаём tools в Ollama SDK format
// - Извлекаем tool_calls из response.message.tool_calls
// BEFORE:
// - Pass tools in Ollama SDK format
// - Extract tool_calls from response.message.tool_calls
// СТАНЕТ:
// - НЕ передаём tools в SDK
// - Tools описаны в system prompt как XML
// - LLM возвращает XML в content
// - Парсим через ResponseParser
// AFTER:
// - DON'T pass tools to SDK
// - Tools described in system prompt as XML
// - LLM returns XML in content
// - Parse via ResponseParser
```
**Изменения:**
- [x] Удалить `convertTools()` метод
- [x] Удалить `extractToolCalls()` метод
- [x] Убрать передачу `tools` в `client.chat()`
- [x] Возвращать только `content` без `toolCalls`
**Changes:**
- [x] Remove `convertTools()` method
- [x] Remove `extractToolCalls()` method
- [x] Remove `tools` from `client.chat()` call
- [x] Return only `content` without `toolCalls`
### 0.19.2 - System Prompt Update
```typescript
// src/infrastructure/llm/prompts.ts
// Добавить в SYSTEM_PROMPT полное описание XML формата:
// Add full XML format description to SYSTEM_PROMPT:
const TOOL_FORMAT_INSTRUCTIONS = `
## Tool Calling Format
@@ -1397,73 +1397,73 @@ Always wait for tool results before making conclusions.
`
```
**Изменения:**
- [x] Добавить `TOOL_FORMAT_INSTRUCTIONS` в prompts.ts
- [x] Включить в `SYSTEM_PROMPT`
- [x] Добавить примеры для всех 18 tools
**Changes:**
- [x] Add `TOOL_FORMAT_INSTRUCTIONS` to prompts.ts
- [x] Include in `SYSTEM_PROMPT`
- [x] Add examples for all 18 tools
### 0.19.3 - HandleMessage Simplification
```typescript
// src/application/use-cases/HandleMessage.ts
// БЫЛО:
// BEFORE:
// const response = await this.llm.chat(messages)
// const parsed = parseToolCalls(response.content)
// СТАНЕТ:
// const response = await this.llm.chat(messages) // без tools
// const parsed = parseToolCalls(response.content) // единственный источник
// AFTER:
// const response = await this.llm.chat(messages) // without tools
// const parsed = parseToolCalls(response.content) // single source
```
**Изменения:**
- [x] Убрать передачу tool definitions в `llm.chat()`
- [x] ResponseParser — единственный источник tool calls
- [x] Упростить логику обработки
**Changes:**
- [x] Remove tool definitions from `llm.chat()`
- [x] ResponseParser — single source of tool calls
- [x] Simplify processing logic
### 0.19.4 - ILLMClient Interface Update
```typescript
// src/domain/services/ILLMClient.ts
// БЫЛО:
// BEFORE:
interface ILLMClient {
chat(messages: ChatMessage[], tools?: ToolDef[]): Promise<LLMResponse>
}
// СТАНЕТ:
// AFTER:
interface ILLMClient {
chat(messages: ChatMessage[]): Promise<LLMResponse>
// tools больше не передаются - они в system prompt
// tools no longer passed - they're in system prompt
}
```
**Изменения:**
- [x] Убрать `tools` параметр из `chat()`
- [x] Убрать `toolCalls` из `LLMResponse` (парсятся из content)
- [x] Обновить все реализации
**Changes:**
- [x] Remove `tools` parameter from `chat()`
- [x] Remove `toolCalls` from `LLMResponse` (parsed from content)
- [x] Update all implementations
### 0.19.5 - ResponseParser Enhancements
```typescript
// src/infrastructure/llm/ResponseParser.ts
// Улучшения:
// - Лучшая обработка ошибок парсинга
// - Поддержка CDATA для многострочного content
// - Валидация имён tools
// Improvements:
// - Better error handling for parsing
// - CDATA support for multiline content
// - Tool name validation
```
**Изменения:**
- [x] Добавить поддержку `<![CDATA[...]]>` для content
- [x] Валидация: tool name должен быть из известного списка
- [x] Улучшить сообщения об ошибках парсинга
**Changes:**
- [x] Add `<![CDATA[...]]>` support for content
- [x] Validation: tool name must be from known list
- [x] Improve parsing error messages
**Tests:**
- [x] Обновить тесты OllamaClient
- [x] Обновить тесты HandleMessage
- [x] Добавить тесты ResponseParser для edge cases
- [ ] E2E тест полного flow с XML (опционально, может быть в 0.20.0)
- [x] Update OllamaClient tests
- [x] Update HandleMessage tests
- [x] Add ResponseParser tests for edge cases
- [ ] E2E test for full XML flow (optional, may be in 0.20.0)
---
@@ -1779,6 +1779,211 @@ export interface ScanResult {
---
## Version 0.24.0 - Rich Initial Context 📋
**Priority:** HIGH
**Status:** In Progress (2/4 complete)
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 ⭐ ✅
**Problem:** Currently LLM only sees function names: `fn: getUser, createUser`
**Solution:** Show full signatures: `async getUser(id: string): Promise<User>`
```typescript
// src/infrastructure/llm/prompts.ts changes
// BEFORE:
// - src/services/user.ts [fn: getUser, createUser]
// AFTER:
// ### src/services/user.ts
// - async getUser(id: string): Promise<User>
// - async createUser(data: UserDTO): Promise<User>
// - validateEmail(email: string): boolean
```
**Changes:**
- [x] Extend `FunctionInfo` in FileAST for parameter types and return type (already existed)
- [x] Update `ASTParser.ts` to extract parameter types and return types (arrow functions fixed)
- [x] Update `formatFileSummary()` in prompts.ts to output signatures
- [x] Add `includeSignatures: boolean` option to config
**Why:** LLM won't hallucinate parameters and return types.
### 0.24.2 - Interface/Type Field Definitions ⭐ ✅
**Problem:** LLM only sees `interface: User, UserDTO`
**Solution:** Show fields: `User { id: string, name: string, email: string }`
```typescript
// BEFORE:
// - src/types/user.ts [interface: User, UserDTO]
// AFTER:
// ### src/types/user.ts
// - interface User { id: string, name: string, email: string, createdAt: Date }
// - interface UserDTO { name: string, email: string }
// - type UserId = string
```
**Changes:**
- [x] Extend `InterfaceInfo` in FileAST for field types (already existed)
- [x] Update `ASTParser.ts` to extract interface fields (already existed)
- [x] Update `formatFileSummary()` to output fields
- [x] Handle type aliases with their definitions
**Why:** LLM knows data structure, won't invent fields.
### 0.24.3 - Enum Value Definitions
**Problem:** LLM only sees `type: Status`
**Solution:** Show values: `Status { Active=1, Inactive=0, Pending=2 }`
```typescript
// BEFORE:
// - src/types/enums.ts [type: Status, Role]
// AFTER:
// ### src/types/enums.ts
// - enum Status { Active=1, Inactive=0, Pending=2 }
// - enum Role { Admin="admin", User="user" }
```
**Changes:**
- [ ] Add `EnumInfo` to FileAST with members and values
- [ ] Update `ASTParser.ts` to extract enum members
- [ ] Update `formatFileSummary()` to output enum values
**Why:** LLM knows valid enum values.
### 0.24.4 - Decorator Extraction
**Problem:** LLM doesn't see decorators (important for NestJS, Angular)
**Solution:** Show decorators in context
```typescript
// AFTER:
// ### src/controllers/user.controller.ts
// - @Controller('users') class UserController
// - @Get(':id') async getUser(id: string): Promise<User>
// - @Post() @Body() async createUser(data: UserDTO): Promise<User>
```
**Changes:**
- [ ] Add `decorators: string[]` to FunctionInfo and ClassInfo
- [ ] Update `ASTParser.ts` to extract decorators
- [ ] Update context to display decorators
**Why:** LLM understands routing, DI, guards in NestJS/Angular.
**Tests:**
- [ ] Unit tests for enhanced ASTParser
- [ ] Unit tests for new context format
- [ ] Integration tests for full flow
---
## Version 0.25.0 - Graph Metrics in Context 📊
**Priority:** MEDIUM
**Status:** Planned
Add graph metrics to initial context: dependency graph, circular dependencies, impact score.
### 0.25.1 - Inline Dependency Graph
**Problem:** LLM doesn't see file relationships without tool calls
**Solution:** Show dependency graph in context
```typescript
// Add to initial context:
// ## Dependency Graph
// src/services/user.ts: → types/user, utils/validation ← controllers/user, api/routes
// src/services/auth.ts: → services/user, utils/jwt ← controllers/auth
// src/utils/validation.ts: ← services/user, services/auth, controllers/*
```
**Changes:**
- [ ] Add `formatDependencyGraph()` to prompts.ts
- [ ] Use data from `FileMeta.dependencies` and `FileMeta.dependents`
- [ ] Group by hub files (many connections)
- [ ] Add `includeDepsGraph: boolean` option to config
**Why:** LLM sees architecture without tool call.
### 0.25.2 - Circular Dependencies in Context
**Problem:** Circular deps are computed but not shown in context
**Solution:** Show cycles immediately
```typescript
// Add to initial context:
// ## ⚠️ Circular Dependencies
// - services/user → services/auth → services/user
// - utils/a → utils/b → utils/c → utils/a
```
**Changes:**
- [ ] Add `formatCircularDeps()` to prompts.ts
- [ ] Get circular deps from IndexBuilder
- [ ] Store in Redis as separate key or in meta
**Why:** LLM immediately sees architecture problems.
### 0.25.3 - Impact Score
**Problem:** LLM doesn't know which files are critical
**Solution:** Show impact score (% of codebase that depends on file)
```typescript
// Add to initial context:
// ## High Impact Files
// | File | Impact | Dependents |
// |------|--------|------------|
// | src/utils/validation.ts | 67% | 12 files |
// | src/types/user.ts | 45% | 8 files |
// | src/services/user.ts | 34% | 6 files |
```
**Changes:**
- [ ] Add `impactScore: number` to FileMeta (0-100)
- [ ] Compute in MetaAnalyzer: (transitiveDepByCount / totalFiles) * 100
- [ ] Add `formatHighImpactFiles()` to prompts.ts
- [ ] Show top-10 high impact files
**Why:** LLM understands which files are critical for changes.
### 0.25.4 - Transitive Dependencies Count
**Problem:** Currently only counting direct dependencies
**Solution:** Add transitive dependencies to meta
```typescript
// FileMeta additions:
interface FileMeta {
// existing...
transitiveDepCount: number; // how many files depend on this (transitively)
transitiveDepByCount: number; // how many files this depends on (transitively)
}
```
**Changes:**
- [ ] Add `computeTransitiveDeps()` to MetaAnalyzer
- [ ] Use DFS with memoization for efficiency
- [ ] Store in FileMeta
**Tests:**
- [ ] Unit tests for graph metrics computation
- [ ] Unit tests for new context sections
- [ ] Performance tests for large codebases
---
## Version 1.0.0 - Production Ready 🚀
**Target:** Stable release
@@ -1794,6 +1999,8 @@ export interface ScanResult {
- [x] 0 ESLint errors ✅
- [x] Examples working ✅ (v0.18.0)
- [x] CHANGELOG.md up to date ✅
- [ ] Rich initial context (v0.24.0) — function signatures, interface fields, enum values
- [ ] Graph metrics in context (v0.25.0) — dependency graph, circular deps, impact score
---
@@ -1862,13 +2069,17 @@ sessions:list # List<session_id>
| Component | Tokens | % |
|-----------|--------|---|
| System prompt | ~2,000 | 1.5% |
| Structure + AST | ~10,000 | 8% |
| **Available** | ~116,000 | 90% |
| Structure + AST (v0.23) | ~10,000 | 8% |
| Signatures + Types (v0.24) | ~5,000 | 4% |
| Graph Metrics (v0.25) | ~3,000 | 2.5% |
| **Total Initial Context** | ~20,000 | 16% |
| **Available for Chat** | ~108,000 | 84% |
---
**Last Updated:** 2025-12-04
**Target Version:** 1.0.0
**Current Version:** 0.23.0
**Current Version:** 0.25.0
**Next Milestones:** v0.24.0 (Rich Context - 2/4 complete), v0.25.0 (Graph Metrics)
> **Note:** Versions 0.20.0, 0.21.0, 0.22.0, 0.23.0 were implemented but ROADMAP was not updated. All features verified as complete.
> **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.

View File

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

View File

@@ -129,6 +129,8 @@ export interface TypeAliasInfo {
line: number
/** Whether it's exported */
isExported: boolean
/** Type definition (e.g., "string", "User & Admin", "{ id: string }") */
definition?: string
}
export interface FileAST {

View File

@@ -332,6 +332,7 @@ export class ASTParser {
) {
const params = this.extractParameters(valueNode)
const isAsync = valueNode.children.some((c) => c.type === NodeType.ASYNC)
const returnTypeNode = valueNode.childForFieldName(FieldName.RETURN_TYPE)
ast.functions.push({
name: nameNode?.text ?? "",
@@ -340,6 +341,7 @@ export class ASTParser {
params,
isAsync,
isExported,
returnType: returnTypeNode?.text?.replace(/^:\s*/, ""),
})
if (isExported) {
@@ -552,10 +554,14 @@ export class ASTParser {
return
}
const valueNode = node.childForFieldName(FieldName.VALUE)
const definition = valueNode?.text
ast.typeAliases.push({
name: nameNode.text,
line: node.startPosition.row + 1,
isExported,
definition,
})
}

View File

@@ -11,6 +11,13 @@ export interface ProjectStructure {
directories: string[]
}
/**
* Options for building initial context.
*/
export interface BuildContextOptions {
includeSignatures?: boolean
}
/**
* System prompt for the ipuaro AI agent.
*/
@@ -116,12 +123,14 @@ export function buildInitialContext(
structure: ProjectStructure,
asts: Map<string, FileAST>,
metas?: Map<string, FileMeta>,
options?: BuildContextOptions,
): string {
const sections: string[] = []
const includeSignatures = options?.includeSignatures ?? true
sections.push(formatProjectHeader(structure))
sections.push(formatDirectoryTree(structure))
sections.push(formatFileOverview(asts, metas))
sections.push(formatFileOverview(asts, metas, includeSignatures))
return sections.join("\n\n")
}
@@ -157,7 +166,11 @@ function formatDirectoryTree(structure: ProjectStructure): string {
/**
* Format file overview with AST summaries.
*/
function formatFileOverview(asts: Map<string, FileAST>, metas?: Map<string, FileMeta>): string {
function formatFileOverview(
asts: Map<string, FileAST>,
metas?: Map<string, FileMeta>,
includeSignatures = true,
): string {
const lines: string[] = ["## Files", ""]
const sortedPaths = [...asts.keys()].sort()
@@ -168,16 +181,133 @@ function formatFileOverview(asts: Map<string, FileAST>, metas?: Map<string, File
}
const meta = metas?.get(path)
lines.push(formatFileSummary(path, ast, meta))
lines.push(formatFileSummary(path, ast, meta, includeSignatures))
}
return lines.join("\n")
}
/**
* Format a single file's AST summary.
* Format a function signature.
*/
function formatFileSummary(path: string, ast: FileAST, meta?: FileMeta): string {
function formatFunctionSignature(fn: FileAST["functions"][0]): string {
const asyncPrefix = fn.isAsync ? "async " : ""
const params = fn.params
.map((p) => {
const optional = p.optional ? "?" : ""
const type = p.type ? `: ${p.type}` : ""
return `${p.name}${optional}${type}`
})
.join(", ")
const returnType = fn.returnType ? `: ${fn.returnType}` : ""
return `${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}`
}
/**
* 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)}...`
}
/**
* Format a single file's AST summary.
* When includeSignatures is true, shows full function signatures.
* When false, shows compact format with just names.
*/
function formatFileSummary(
path: string,
ast: FileAST,
meta?: FileMeta,
includeSignatures = true,
): string {
const flags = formatFileFlags(meta)
if (!includeSignatures) {
return formatFileSummaryCompact(path, ast, flags)
}
const lines: string[] = []
lines.push(`### ${path}${flags}`)
if (ast.functions.length > 0) {
for (const fn of ast.functions) {
lines.push(`- ${formatFunctionSignature(fn)}`)
}
}
if (ast.classes.length > 0) {
for (const cls of ast.classes) {
const ext = cls.extends ? ` extends ${cls.extends}` : ""
const impl = cls.implements.length > 0 ? ` implements ${cls.implements.join(", ")}` : ""
lines.push(`- class ${cls.name}${ext}${impl}`)
}
}
if (ast.interfaces.length > 0) {
for (const iface of ast.interfaces) {
lines.push(`- ${formatInterfaceSignature(iface)}`)
}
}
if (ast.typeAliases.length > 0) {
for (const type of ast.typeAliases) {
lines.push(`- ${formatTypeAliasSignature(type)}`)
}
}
if (lines.length === 1) {
return `- ${path}${flags}`
}
return lines.join("\n")
}
/**
* Format file summary in compact mode (just names, no signatures).
*/
function formatFileSummaryCompact(path: string, ast: FileAST, flags: string): string {
const parts: string[] = []
if (ast.functions.length > 0) {
@@ -201,8 +331,6 @@ function formatFileSummary(path: string, ast: FileAST, meta?: FileMeta): string
}
const summary = parts.length > 0 ? ` [${parts.join(" | ")}]` : ""
const flags = formatFileFlags(meta)
return `- ${path}${summary}${flags}`
}

View File

@@ -114,6 +114,7 @@ export const ContextConfigSchema = z.object({
maxContextUsage: 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"),
includeSignatures: z.boolean().default(true),
})
/**

View File

@@ -224,6 +224,62 @@ describe("ASTParser", () => {
const ast = parser.parse(code, "ts")
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", () => {

View File

@@ -108,13 +108,23 @@ describe("prompts", () => {
expect(context).toContain("tests/")
})
it("should include file overview with AST summaries", () => {
it("should include file overview with AST summaries (signatures format)", () => {
const context = buildInitialContext(structure, asts)
expect(context).toContain("## Files")
expect(context).toContain("src/index.ts")
expect(context).toContain("### src/index.ts")
expect(context).toContain("- main()")
expect(context).toContain("### src/utils.ts")
expect(context).toContain("- class Helper")
})
it("should use compact format when includeSignatures is false", () => {
const context = buildInitialContext(structure, asts, undefined, {
includeSignatures: false,
})
expect(context).toContain("## Files")
expect(context).toContain("fn: main")
expect(context).toContain("src/utils.ts")
expect(context).toContain("class: Helper")
})
@@ -506,7 +516,16 @@ describe("prompts", () => {
exports: [],
functions: [],
classes: [],
interfaces: [{ name: "IFoo", lineStart: 1, lineEnd: 5, isExported: true }],
interfaces: [
{
name: "IFoo",
lineStart: 1,
lineEnd: 5,
properties: [],
extends: [],
isExported: true,
},
],
typeAliases: [],
parseError: false,
},
@@ -515,6 +534,44 @@ describe("prompts", () => {
const context = buildInitialContext(structure, asts)
expect(context).toContain("- interface IFoo")
})
it("should handle file with only interfaces (compact format)", () => {
const structure: ProjectStructure = {
name: "test",
rootPath: "/test",
files: ["types.ts"],
directories: [],
}
const asts = new Map<string, FileAST>([
[
"types.ts",
{
imports: [],
exports: [],
functions: [],
classes: [],
interfaces: [
{
name: "IFoo",
lineStart: 1,
lineEnd: 5,
properties: [],
extends: [],
isExported: true,
},
],
typeAliases: [],
parseError: false,
},
],
])
const context = buildInitialContext(structure, asts, undefined, {
includeSignatures: false,
})
expect(context).toContain("interface: IFoo")
})
@@ -534,9 +591,7 @@ describe("prompts", () => {
functions: [],
classes: [],
interfaces: [],
typeAliases: [
{ name: "MyType", lineStart: 1, lineEnd: 1, isExported: true },
],
typeAliases: [{ name: "MyType", line: 1, isExported: true }],
parseError: false,
},
],
@@ -544,6 +599,35 @@ describe("prompts", () => {
const context = buildInitialContext(structure, asts)
expect(context).toContain("- type MyType")
})
it("should handle file with only type aliases (compact format)", () => {
const structure: ProjectStructure = {
name: "test",
rootPath: "/test",
files: ["types.ts"],
directories: [],
}
const asts = new Map<string, FileAST>([
[
"types.ts",
{
imports: [],
exports: [],
functions: [],
classes: [],
interfaces: [],
typeAliases: [{ name: "MyType", line: 1, isExported: true }],
parseError: false,
},
],
])
const context = buildInitialContext(structure, asts, undefined, {
includeSignatures: false,
})
expect(context).toContain("type: MyType")
})
@@ -686,6 +770,22 @@ describe("prompts", () => {
expect(context).toContain("exists.ts")
expect(context).not.toContain("missing.ts")
})
it("should skip undefined AST entries", () => {
const structure: ProjectStructure = {
name: "test",
rootPath: "/test",
files: ["file.ts"],
directories: [],
}
const asts = new Map<string, FileAST>()
asts.set("file.ts", undefined as unknown as FileAST)
const context = buildInitialContext(structure, asts)
expect(context).toContain("## Files")
expect(context).not.toContain("file.ts")
})
})
describe("truncateContext", () => {
@@ -714,4 +814,684 @@ describe("prompts", () => {
expect(result).toContain("truncated")
})
})
describe("function signatures with types", () => {
it("should format function with typed parameters", () => {
const structure: ProjectStructure = {
name: "test",
rootPath: "/test",
files: ["user.ts"],
directories: [],
}
const asts = new Map<string, FileAST>([
[
"user.ts",
{
imports: [],
exports: [],
functions: [
{
name: "getUser",
lineStart: 1,
lineEnd: 5,
params: [
{
name: "id",
type: "string",
optional: false,
hasDefault: false,
},
],
isAsync: false,
isExported: true,
},
],
classes: [],
interfaces: [],
typeAliases: [],
parseError: false,
},
],
])
const context = buildInitialContext(structure, asts)
expect(context).toContain("- getUser(id: string)")
})
it("should format async function with return type", () => {
const structure: ProjectStructure = {
name: "test",
rootPath: "/test",
files: ["user.ts"],
directories: [],
}
const asts = new Map<string, FileAST>([
[
"user.ts",
{
imports: [],
exports: [],
functions: [
{
name: "getUser",
lineStart: 1,
lineEnd: 5,
params: [
{
name: "id",
type: "string",
optional: false,
hasDefault: false,
},
],
isAsync: true,
isExported: true,
returnType: "Promise<User>",
},
],
classes: [],
interfaces: [],
typeAliases: [],
parseError: false,
},
],
])
const context = buildInitialContext(structure, asts)
expect(context).toContain("- async getUser(id: string): Promise<User>")
})
it("should format function with optional parameters", () => {
const structure: ProjectStructure = {
name: "test",
rootPath: "/test",
files: ["utils.ts"],
directories: [],
}
const asts = new Map<string, FileAST>([
[
"utils.ts",
{
imports: [],
exports: [],
functions: [
{
name: "format",
lineStart: 1,
lineEnd: 5,
params: [
{
name: "value",
type: "string",
optional: false,
hasDefault: false,
},
{
name: "options",
type: "FormatOptions",
optional: true,
hasDefault: false,
},
],
isAsync: false,
isExported: true,
returnType: "string",
},
],
classes: [],
interfaces: [],
typeAliases: [],
parseError: false,
},
],
])
const context = buildInitialContext(structure, asts)
expect(context).toContain("- format(value: string, options?: FormatOptions): string")
})
it("should format function with multiple typed parameters", () => {
const structure: ProjectStructure = {
name: "test",
rootPath: "/test",
files: ["api.ts"],
directories: [],
}
const asts = new Map<string, FileAST>([
[
"api.ts",
{
imports: [],
exports: [],
functions: [
{
name: "createUser",
lineStart: 1,
lineEnd: 10,
params: [
{
name: "name",
type: "string",
optional: false,
hasDefault: false,
},
{
name: "email",
type: "string",
optional: false,
hasDefault: false,
},
{
name: "age",
type: "number",
optional: true,
hasDefault: false,
},
],
isAsync: true,
isExported: true,
returnType: "Promise<User>",
},
],
classes: [],
interfaces: [],
typeAliases: [],
parseError: false,
},
],
])
const context = buildInitialContext(structure, asts)
expect(context).toContain(
"- async createUser(name: string, email: string, age?: number): Promise<User>",
)
})
it("should format function without types (JavaScript style)", () => {
const structure: ProjectStructure = {
name: "test",
rootPath: "/test",
files: ["legacy.js"],
directories: [],
}
const asts = new Map<string, FileAST>([
[
"legacy.js",
{
imports: [],
exports: [],
functions: [
{
name: "doSomething",
lineStart: 1,
lineEnd: 5,
params: [
{ name: "x", optional: false, hasDefault: false },
{ name: "y", optional: false, hasDefault: false },
],
isAsync: false,
isExported: true,
},
],
classes: [],
interfaces: [],
typeAliases: [],
parseError: false,
},
],
])
const context = buildInitialContext(structure, asts)
expect(context).toContain("- doSomething(x, y)")
})
it("should format interface with extends", () => {
const structure: ProjectStructure = {
name: "test",
rootPath: "/test",
files: ["types.ts"],
directories: [],
}
const asts = new Map<string, FileAST>([
[
"types.ts",
{
imports: [],
exports: [],
functions: [],
classes: [],
interfaces: [
{
name: "AdminUser",
lineStart: 1,
lineEnd: 5,
properties: [],
extends: ["User", "Admin"],
isExported: true,
},
],
typeAliases: [],
parseError: false,
},
],
])
const context = buildInitialContext(structure, asts)
expect(context).toContain("- interface AdminUser extends User, Admin")
})
})
describe("interface fields (0.24.2)", () => {
it("should format interface with fields", () => {
const structure: ProjectStructure = {
name: "test",
rootPath: "/test",
files: ["user.ts"],
directories: [],
}
const asts = new Map<string, FileAST>([
[
"user.ts",
{
imports: [],
exports: [],
functions: [],
classes: [],
interfaces: [
{
name: "User",
lineStart: 1,
lineEnd: 5,
properties: [
{
name: "id",
line: 2,
type: "string",
visibility: "public",
isStatic: false,
isReadonly: false,
},
{
name: "name",
line: 3,
type: "string",
visibility: "public",
isStatic: false,
isReadonly: false,
},
{
name: "email",
line: 4,
type: "string",
visibility: "public",
isStatic: false,
isReadonly: false,
},
],
extends: [],
isExported: true,
},
],
typeAliases: [],
parseError: false,
},
],
])
const context = buildInitialContext(structure, asts)
expect(context).toContain("interface User { id: string, name: string, email: string }")
})
it("should format interface with extends and fields", () => {
const structure: ProjectStructure = {
name: "test",
rootPath: "/test",
files: ["types.ts"],
directories: [],
}
const asts = new Map<string, FileAST>([
[
"types.ts",
{
imports: [],
exports: [],
functions: [],
classes: [],
interfaces: [
{
name: "AdminUser",
lineStart: 1,
lineEnd: 5,
properties: [
{
name: "role",
line: 2,
type: "string",
visibility: "public",
isStatic: false,
isReadonly: false,
},
],
extends: ["User"],
isExported: true,
},
],
typeAliases: [],
parseError: false,
},
],
])
const context = buildInitialContext(structure, asts)
expect(context).toContain(
"interface AdminUser extends User { role: string }",
)
})
it("should format interface with readonly fields", () => {
const structure: ProjectStructure = {
name: "test",
rootPath: "/test",
files: ["types.ts"],
directories: [],
}
const asts = new Map<string, FileAST>([
[
"types.ts",
{
imports: [],
exports: [],
functions: [],
classes: [],
interfaces: [
{
name: "Config",
lineStart: 1,
lineEnd: 3,
properties: [
{
name: "version",
line: 2,
type: "string",
visibility: "public",
isStatic: false,
isReadonly: true,
},
],
extends: [],
isExported: true,
},
],
typeAliases: [],
parseError: false,
},
],
])
const context = buildInitialContext(structure, asts)
expect(context).toContain("interface Config { readonly version: string }")
})
it("should format interface with no type annotation", () => {
const structure: ProjectStructure = {
name: "test",
rootPath: "/test",
files: ["types.ts"],
directories: [],
}
const asts = new Map<string, FileAST>([
[
"types.ts",
{
imports: [],
exports: [],
functions: [],
classes: [],
interfaces: [
{
name: "Loose",
lineStart: 1,
lineEnd: 3,
properties: [
{
name: "data",
line: 2,
visibility: "public",
isStatic: false,
isReadonly: false,
},
],
extends: [],
isExported: true,
},
],
typeAliases: [],
parseError: false,
},
],
])
const context = buildInitialContext(structure, asts)
expect(context).toContain("interface Loose { data }")
})
})
describe("type alias definitions (0.24.2)", () => {
it("should format type alias with definition", () => {
const structure: ProjectStructure = {
name: "test",
rootPath: "/test",
files: ["types.ts"],
directories: [],
}
const asts = new Map<string, FileAST>([
[
"types.ts",
{
imports: [],
exports: [],
functions: [],
classes: [],
interfaces: [],
typeAliases: [
{
name: "UserId",
line: 1,
isExported: true,
definition: "string",
},
],
parseError: false,
},
],
])
const context = buildInitialContext(structure, asts)
expect(context).toContain("- type UserId = string")
})
it("should format union type alias", () => {
const structure: ProjectStructure = {
name: "test",
rootPath: "/test",
files: ["types.ts"],
directories: [],
}
const asts = new Map<string, FileAST>([
[
"types.ts",
{
imports: [],
exports: [],
functions: [],
classes: [],
interfaces: [],
typeAliases: [
{
name: "Status",
line: 1,
isExported: true,
definition: '"pending" | "active" | "done"',
},
],
parseError: false,
},
],
])
const context = buildInitialContext(structure, asts)
expect(context).toContain('- type Status = "pending" | "active" | "done"')
})
it("should format intersection type alias", () => {
const structure: ProjectStructure = {
name: "test",
rootPath: "/test",
files: ["types.ts"],
directories: [],
}
const asts = new Map<string, FileAST>([
[
"types.ts",
{
imports: [],
exports: [],
functions: [],
classes: [],
interfaces: [],
typeAliases: [
{
name: "AdminUser",
line: 1,
isExported: true,
definition: "User & Admin",
},
],
parseError: false,
},
],
])
const context = buildInitialContext(structure, asts)
expect(context).toContain("- type AdminUser = User & Admin")
})
it("should truncate long type definitions", () => {
const structure: ProjectStructure = {
name: "test",
rootPath: "/test",
files: ["types.ts"],
directories: [],
}
const longDefinition =
"{ id: string, name: string, email: string, phone: string, address: string, city: string, country: string, zip: string }"
const asts = new Map<string, FileAST>([
[
"types.ts",
{
imports: [],
exports: [],
functions: [],
classes: [],
interfaces: [],
typeAliases: [
{
name: "BigType",
line: 1,
isExported: true,
definition: longDefinition,
},
],
parseError: false,
},
],
])
const context = buildInitialContext(structure, asts)
expect(context).toContain("- type BigType = { id: string")
expect(context).toContain("...")
})
it("should format type alias without definition (fallback)", () => {
const structure: ProjectStructure = {
name: "test",
rootPath: "/test",
files: ["types.ts"],
directories: [],
}
const asts = new Map<string, FileAST>([
[
"types.ts",
{
imports: [],
exports: [],
functions: [],
classes: [],
interfaces: [],
typeAliases: [
{
name: "Unknown",
line: 1,
isExported: true,
},
],
parseError: false,
},
],
])
const context = buildInitialContext(structure, asts)
expect(context).toContain("- type Unknown")
expect(context).not.toContain("- type Unknown =")
})
it("should format function type alias", () => {
const structure: ProjectStructure = {
name: "test",
rootPath: "/test",
files: ["types.ts"],
directories: [],
}
const asts = new Map<string, FileAST>([
[
"types.ts",
{
imports: [],
exports: [],
functions: [],
classes: [],
interfaces: [],
typeAliases: [
{
name: "Handler",
line: 1,
isExported: true,
definition: "(event: Event) => void",
},
],
parseError: false,
},
],
])
const context = buildInitialContext(structure, asts)
expect(context).toContain("- type Handler = (event: Event) => void")
})
})
})

View File

@@ -15,6 +15,7 @@ describe("ContextConfigSchema", () => {
maxContextUsage: 0.8,
autoCompressAt: 0.8,
compressionMethod: "llm-summary",
includeSignatures: true,
})
})
@@ -26,6 +27,7 @@ describe("ContextConfigSchema", () => {
maxContextUsage: 0.8,
autoCompressAt: 0.8,
compressionMethod: "llm-summary",
includeSignatures: true,
})
})
})
@@ -162,6 +164,7 @@ describe("ContextConfigSchema", () => {
maxContextUsage: 0.8,
autoCompressAt: 0.8,
compressionMethod: "llm-summary",
includeSignatures: true,
})
})
@@ -175,6 +178,7 @@ describe("ContextConfigSchema", () => {
maxContextUsage: 0.8,
autoCompressAt: 0.9,
compressionMethod: "llm-summary",
includeSignatures: true,
})
})
@@ -189,6 +193,7 @@ describe("ContextConfigSchema", () => {
maxContextUsage: 0.7,
autoCompressAt: 0.8,
compressionMethod: "truncate",
includeSignatures: true,
})
})
})
@@ -200,6 +205,7 @@ describe("ContextConfigSchema", () => {
maxContextUsage: 0.9,
autoCompressAt: 0.85,
compressionMethod: "truncate" as const,
includeSignatures: false,
}
const result = ContextConfigSchema.parse(config)
@@ -212,10 +218,36 @@ describe("ContextConfigSchema", () => {
maxContextUsage: 0.8,
autoCompressAt: 0.8,
compressionMethod: "llm-summary" as const,
includeSignatures: true,
}
const result = ContextConfigSchema.parse(config)
expect(result).toEqual(config)
})
})
describe("includeSignatures", () => {
it("should accept true", () => {
const result = ContextConfigSchema.parse({ includeSignatures: true })
expect(result.includeSignatures).toBe(true)
})
it("should accept false", () => {
const result = ContextConfigSchema.parse({ includeSignatures: false })
expect(result.includeSignatures).toBe(false)
})
it("should default to true", () => {
const result = ContextConfigSchema.parse({})
expect(result.includeSignatures).toBe(true)
})
it("should reject non-boolean", () => {
expect(() => ContextConfigSchema.parse({ includeSignatures: "true" })).toThrow()
})
it("should reject number", () => {
expect(() => ContextConfigSchema.parse({ includeSignatures: 1 })).toThrow()
})
})
})