From 12197a96247cc31c16430cf47ada1d8d6b8b8970 Mon Sep 17 00:00:00 2001 From: imfozilbek Date: Thu, 4 Dec 2025 22:49:03 +0500 Subject: [PATCH] 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 --- packages/ipuaro/CHANGELOG.md | 66 +++ packages/ipuaro/ROADMAP.md | 250 +++++------ .../src/domain/value-objects/FileAST.ts | 2 + .../src/infrastructure/indexer/ASTParser.ts | 4 + .../ipuaro/src/infrastructure/llm/prompts.ts | 54 ++- .../infrastructure/indexer/ASTParser.test.ts | 56 +++ .../unit/infrastructure/llm/prompts.test.ts | 408 ++++++++++++++++++ 7 files changed, 711 insertions(+), 129 deletions(-) diff --git a/packages/ipuaro/CHANGELOG.md b/packages/ipuaro/CHANGELOG.md index ad2efe2..6385ead 100644 --- a/packages/ipuaro/CHANGELOG.md +++ b/packages/ipuaro/CHANGELOG.md @@ -5,6 +5,72 @@ 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 diff --git a/packages/ipuaro/ROADMAP.md b/packages/ipuaro/ROADMAP.md index 79c2d8c..ac72a10 100644 --- a/packages/ipuaro/ROADMAP.md +++ b/packages/ipuaro/ROADMAP.md @@ -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 } -// СТАНЕТ: +// AFTER: interface ILLMClient { chat(messages: ChatMessage[]): Promise - // 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] Добавить поддержку `` для content -- [x] Валидация: tool name должен быть из известного списка -- [x] Улучшить сообщения об ошибках парсинга +**Changes:** +- [x] Add `` 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) --- @@ -1782,101 +1782,101 @@ export interface ScanResult { ## Version 0.24.0 - Rich Initial Context 📋 **Priority:** HIGH -**Status:** In Progress (1/4 complete) +**Status:** In Progress (2/4 complete) -Улучшение 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 ⭐ ✅ -**Проблема:** Сейчас LLM видит только имена функций: `fn: getUser, createUser` -**Решение:** Показать полные сигнатуры: `async getUser(id: string): Promise` +**Problem:** Currently LLM only sees function names: `fn: getUser, createUser` +**Solution:** Show full signatures: `async getUser(id: string): Promise` ```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 // - async createUser(data: UserDTO): Promise // - validateEmail(email: string): boolean ``` -**Изменения:** -- [x] Расширить `FunctionInfo` в FileAST для хранения типов параметров и return type (already existed) -- [x] Обновить `ASTParser.ts` для извлечения типов параметров и return types (arrow functions fixed) -- [x] Обновить `formatFileSummary()` в prompts.ts для вывода сигнатур -- [x] Добавить опцию `includeSignatures: boolean` в config +**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 -**Зачем:** 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` -**Решение:** Показать поля: `User { id: string, name: string, email: string }` +**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 ``` -**Изменения:** -- [ ] Расширить `InterfaceInfo` в FileAST для хранения полей с типами -- [ ] Обновить `ASTParser.ts` для извлечения полей интерфейсов -- [ ] Обновить `formatFileSummary()` для вывода полей -- [ ] Обработка type aliases с их определениями +**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 -**Зачем:** LLM знает структуру данных, не придумывает поля. +**Why:** LLM knows data structure, won't invent fields. ### 0.24.3 - Enum Value Definitions -**Проблема:** LLM видит только `type: Status` -**Решение:** Показать значения: `Status { Active=1, Inactive=0, Pending=2 }` +**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" } ``` -**Изменения:** -- [ ] Добавить `EnumInfo` в FileAST с members и values -- [ ] Обновить `ASTParser.ts` для извлечения enum members -- [ ] Обновить `formatFileSummary()` для вывода enum values +**Changes:** +- [ ] Add `EnumInfo` to FileAST with members and values +- [ ] Update `ASTParser.ts` to extract enum members +- [ ] Update `formatFileSummary()` to output enum values -**Зачем:** LLM знает допустимые значения enum. +**Why:** LLM knows valid enum values. ### 0.24.4 - Decorator Extraction -**Проблема:** LLM не видит декораторы (важно для NestJS, Angular) -**Решение:** Показать декораторы в контексте +**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 // - @Post() @Body() async createUser(data: UserDTO): Promise ``` -**Изменения:** -- [ ] Добавить `decorators: string[]` в FunctionInfo и ClassInfo -- [ ] Обновить `ASTParser.ts` для извлечения декораторов -- [ ] Обновить контекст для отображения декораторов +**Changes:** +- [ ] Add `decorators: string[]` to FunctionInfo and ClassInfo +- [ ] Update `ASTParser.ts` to extract decorators +- [ ] Update context to display decorators -**Зачем:** LLM понимает роутинг, DI, guards в NestJS/Angular. +**Why:** LLM understands routing, DI, guards in NestJS/Angular. **Tests:** - [ ] Unit tests for enhanced ASTParser @@ -1890,15 +1890,15 @@ export interface ScanResult { **Priority:** MEDIUM **Status:** Planned -Добавление графовых метрик в initial context: граф зависимостей, circular dependencies, impact score. +Add graph metrics to initial context: dependency graph, circular dependencies, impact score. ### 0.25.1 - Inline Dependency Graph -**Проблема:** LLM не видит связи между файлами без tool calls -**Решение:** Показать граф зависимостей в контексте +**Problem:** LLM doesn't see file relationships without tool calls +**Solution:** Show dependency graph in context ```typescript -// Добавить в initial context: +// Add to initial context: // ## Dependency Graph // src/services/user.ts: → types/user, utils/validation ← controllers/user, api/routes @@ -1906,41 +1906,41 @@ export interface ScanResult { // src/utils/validation.ts: ← services/user, services/auth, controllers/* ``` -**Изменения:** -- [ ] Добавить `formatDependencyGraph()` в prompts.ts -- [ ] Использовать данные из `FileMeta.dependencies` и `FileMeta.dependents` -- [ ] Группировать по hub files (много connections) -- [ ] Добавить опцию `includeDepsGraph: boolean` в config +**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 -**Зачем:** LLM видит архитектуру без tool call. +**Why:** LLM sees architecture without tool call. ### 0.25.2 - Circular Dependencies in Context -**Проблема:** Circular deps вычисляются, но не показываются в контексте -**Решение:** Показать циклы сразу +**Problem:** Circular deps are computed but not shown in context +**Solution:** Show cycles immediately ```typescript -// Добавить в initial context: +// Add to initial context: // ## ⚠️ Circular Dependencies // - services/user → services/auth → services/user // - utils/a → utils/b → utils/c → utils/a ``` -**Изменения:** -- [ ] Добавить `formatCircularDeps()` в prompts.ts -- [ ] Получать circular deps из IndexBuilder -- [ ] Хранить в Redis как отдельный ключ или в meta +**Changes:** +- [ ] Add `formatCircularDeps()` to prompts.ts +- [ ] Get circular deps from IndexBuilder +- [ ] Store in Redis as separate key or in meta -**Зачем:** LLM сразу видит проблемы архитектуры. +**Why:** LLM immediately sees architecture problems. ### 0.25.3 - Impact Score -**Проблема:** LLM не знает какие файлы критичные -**Решение:** Показать impact score (% кодовой базы, который зависит от файла) +**Problem:** LLM doesn't know which files are critical +**Solution:** Show impact score (% of codebase that depends on file) ```typescript -// Добавить в initial context: +// Add to initial context: // ## High Impact Files // | File | Impact | Dependents | @@ -1950,32 +1950,32 @@ export interface ScanResult { // | src/services/user.ts | 34% | 6 files | ``` -**Изменения:** -- [ ] Добавить `impactScore: number` в FileMeta (0-100) -- [ ] Вычислять в MetaAnalyzer: (transitiveDepByCount / totalFiles) * 100 -- [ ] Добавить `formatHighImpactFiles()` в prompts.ts -- [ ] Показывать top-10 high impact 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 -**Зачем:** LLM понимает какие файлы критичные для изменений. +**Why:** LLM understands which files are critical for changes. ### 0.25.4 - Transitive Dependencies Count -**Проблема:** Сейчас считаем только прямые зависимости -**Решение:** Добавить транзитивные зависимости в meta +**Problem:** Currently only counting direct dependencies +**Solution:** Add transitive dependencies to meta ```typescript // FileMeta additions: interface FileMeta { // existing... - transitiveDepCount: number; // сколько файлов зависит от этого (транзитивно) - transitiveDepByCount: number; // от скольких файлов зависит этот (транзитивно) + transitiveDepCount: number; // how many files depend on this (transitively) + transitiveDepByCount: number; // how many files this depends on (transitively) } ``` -**Изменения:** -- [ ] Добавить `computeTransitiveDeps()` в MetaAnalyzer -- [ ] Использовать DFS с memoization для эффективности -- [ ] Сохранять в FileMeta +**Changes:** +- [ ] Add `computeTransitiveDeps()` to MetaAnalyzer +- [ ] Use DFS with memoization for efficiency +- [ ] Store in FileMeta **Tests:** - [ ] Unit tests for graph metrics computation @@ -2079,7 +2079,7 @@ sessions:list # List **Last Updated:** 2025-12-04 **Target Version:** 1.0.0 -**Current Version:** 0.24.0 -**Next Milestones:** v0.24.0 (Rich Context - 1/4 complete), v0.25.0 (Graph Metrics) +**Current Version:** 0.25.0 +**Next Milestones:** v0.24.0 (Rich Context - 2/4 complete), v0.25.0 (Graph Metrics) > **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. \ No newline at end of file diff --git a/packages/ipuaro/src/domain/value-objects/FileAST.ts b/packages/ipuaro/src/domain/value-objects/FileAST.ts index 5d4f5a7..46f1d4c 100644 --- a/packages/ipuaro/src/domain/value-objects/FileAST.ts +++ b/packages/ipuaro/src/domain/value-objects/FileAST.ts @@ -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 { diff --git a/packages/ipuaro/src/infrastructure/indexer/ASTParser.ts b/packages/ipuaro/src/infrastructure/indexer/ASTParser.ts index 66759e8..6cc8377 100644 --- a/packages/ipuaro/src/infrastructure/indexer/ASTParser.ts +++ b/packages/ipuaro/src/infrastructure/indexer/ASTParser.ts @@ -554,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, }) } diff --git a/packages/ipuaro/src/infrastructure/llm/prompts.ts b/packages/ipuaro/src/infrastructure/llm/prompts.ts index f07564b..5e3decb 100644 --- a/packages/ipuaro/src/infrastructure/llm/prompts.ts +++ b/packages/ipuaro/src/infrastructure/llm/prompts.ts @@ -203,6 +203,54 @@ function formatFunctionSignature(fn: FileAST["functions"][0]): string { 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. @@ -239,15 +287,13 @@ function formatFileSummary( if (ast.interfaces.length > 0) { for (const iface of ast.interfaces) { - const extList = iface.extends ?? [] - const ext = extList.length > 0 ? ` extends ${extList.join(", ")}` : "" - lines.push(`- interface ${iface.name}${ext}`) + lines.push(`- ${formatInterfaceSignature(iface)}`) } } if (ast.typeAliases.length > 0) { for (const type of ast.typeAliases) { - lines.push(`- type ${type.name}`) + lines.push(`- ${formatTypeAliasSignature(type)}`) } } diff --git a/packages/ipuaro/tests/unit/infrastructure/indexer/ASTParser.test.ts b/packages/ipuaro/tests/unit/infrastructure/indexer/ASTParser.test.ts index 9d6c0cc..0f470de 100644 --- a/packages/ipuaro/tests/unit/infrastructure/indexer/ASTParser.test.ts +++ b/packages/ipuaro/tests/unit/infrastructure/indexer/ASTParser.test.ts @@ -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 = { 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", () => { diff --git a/packages/ipuaro/tests/unit/infrastructure/llm/prompts.test.ts b/packages/ipuaro/tests/unit/infrastructure/llm/prompts.test.ts index 44e5b55..9113d48 100644 --- a/packages/ipuaro/tests/unit/infrastructure/llm/prompts.test.ts +++ b/packages/ipuaro/tests/unit/infrastructure/llm/prompts.test.ts @@ -1086,4 +1086,412 @@ describe("prompts", () => { 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([ + [ + "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([ + [ + "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([ + [ + "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([ + [ + "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([ + [ + "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([ + [ + "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([ + [ + "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([ + [ + "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([ + [ + "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([ + [ + "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") + }) + }) })