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.
This commit is contained in:
imfozilbek
2025-12-05 13:14:51 +05:00
parent 806c9281b0
commit 5a22cd5c9b
7 changed files with 575 additions and 10 deletions

View File

@@ -1782,7 +1782,7 @@ export interface ScanResult {
## Version 0.24.0 - Rich Initial Context 📋 ## Version 0.24.0 - Rich Initial Context 📋
**Priority:** HIGH **Priority:** HIGH
**Status:** In Progress (2/4 complete) **Status:** In Progress (3/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. 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.
@@ -1836,7 +1836,7 @@ Enhance initial context for LLM: add function signatures, interface field types,
**Why:** LLM knows data structure, won't invent fields. **Why:** LLM knows data structure, won't invent fields.
### 0.24.3 - Enum Value Definitions ### 0.24.3 - Enum Value Definitions ⭐ ✅
**Problem:** LLM only sees `type: Status` **Problem:** LLM only sees `type: Status`
**Solution:** Show values: `Status { Active=1, Inactive=0, Pending=2 }` **Solution:** Show values: `Status { Active=1, Inactive=0, Pending=2 }`
@@ -1852,9 +1852,9 @@ Enhance initial context for LLM: add function signatures, interface field types,
``` ```
**Changes:** **Changes:**
- [ ] Add `EnumInfo` to FileAST with members and values - [x] Add `EnumInfo` to FileAST with members and values
- [ ] Update `ASTParser.ts` to extract enum members - [x] Update `ASTParser.ts` to extract enum members
- [ ] Update `formatFileSummary()` to output enum values - [x] Update `formatFileSummary()` to output enum values
**Why:** LLM knows valid enum values. **Why:** LLM knows valid enum values.
@@ -2077,9 +2077,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.25.0 **Current Version:** 0.25.0
**Next Milestones:** v0.24.0 (Rich Context - 2/4 complete), v0.25.0 (Graph Metrics) **Next Milestones:** v0.24.0 (Rich Context - 3/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. > **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

@@ -133,6 +133,28 @@ export interface TypeAliasInfo {
definition?: 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 {
/** Import statements */ /** Import statements */
imports: ImportInfo[] imports: ImportInfo[]
@@ -146,6 +168,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 */
@@ -160,6 +184,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
} }
} }
@@ -275,6 +281,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
@@ -565,6 +575,75 @@ export class ASTParser {
}) })
} }
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)

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

View File

@@ -240,6 +240,37 @@ function formatTypeAliasSignature(type: FileAST["typeAliases"][0]): string {
return `type ${type.name} = ${definition}` 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. * Truncate long type definitions for display.
*/ */
@@ -297,6 +328,12 @@ function formatFileSummary(
} }
} }
if (ast.enums && ast.enums.length > 0) {
for (const enumInfo of ast.enums) {
lines.push(`- ${formatEnumSignature(enumInfo)}`)
}
}
if (lines.length === 1) { if (lines.length === 1) {
return `- ${path}${flags}` return `- ${path}${flags}`
} }
@@ -330,6 +367,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}`
} }

View File

@@ -562,4 +562,138 @@ 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)
})
})
}) })

View File

@@ -1191,9 +1191,7 @@ describe("prompts", () => {
const context = buildInitialContext(structure, asts) const context = buildInitialContext(structure, asts)
expect(context).toContain( expect(context).toContain("interface AdminUser extends User { role: string }")
"interface AdminUser extends User { role: string }",
)
}) })
it("should format interface with readonly fields", () => { it("should format interface with readonly fields", () => {
@@ -1494,4 +1492,285 @@ describe("prompts", () => {
expect(context).toContain("- type Handler = (event: Event) => void") expect(context).toContain("- type Handler = (event: Event) => void")
}) })
}) })
describe("enum definitions (0.24.3)", () => {
it("should format enum with numeric values", () => {
const structure: ProjectStructure = {
name: "test",
rootPath: "/test",
files: ["enums.ts"],
directories: [],
}
const asts = new Map<string, FileAST>([
[
"enums.ts",
{
imports: [],
exports: [],
functions: [],
classes: [],
interfaces: [],
typeAliases: [],
enums: [
{
name: "Status",
lineStart: 1,
lineEnd: 5,
members: [
{ name: "Active", value: 1 },
{ name: "Inactive", value: 0 },
{ name: "Pending", value: 2 },
],
isExported: true,
isConst: false,
},
],
parseError: false,
},
],
])
const context = buildInitialContext(structure, asts)
expect(context).toContain("- enum Status { Active=1, Inactive=0, Pending=2 }")
})
it("should format enum with string values", () => {
const structure: ProjectStructure = {
name: "test",
rootPath: "/test",
files: ["enums.ts"],
directories: [],
}
const asts = new Map<string, FileAST>([
[
"enums.ts",
{
imports: [],
exports: [],
functions: [],
classes: [],
interfaces: [],
typeAliases: [],
enums: [
{
name: "Role",
lineStart: 1,
lineEnd: 5,
members: [
{ name: "Admin", value: "admin" },
{ name: "User", value: "user" },
],
isExported: true,
isConst: false,
},
],
parseError: false,
},
],
])
const context = buildInitialContext(structure, asts)
expect(context).toContain('- enum Role { Admin="admin", User="user" }')
})
it("should format const enum", () => {
const structure: ProjectStructure = {
name: "test",
rootPath: "/test",
files: ["enums.ts"],
directories: [],
}
const asts = new Map<string, FileAST>([
[
"enums.ts",
{
imports: [],
exports: [],
functions: [],
classes: [],
interfaces: [],
typeAliases: [],
enums: [
{
name: "HttpStatus",
lineStart: 1,
lineEnd: 5,
members: [
{ name: "OK", value: 200 },
{ name: "NotFound", value: 404 },
],
isExported: true,
isConst: true,
},
],
parseError: false,
},
],
])
const context = buildInitialContext(structure, asts)
expect(context).toContain("- const enum HttpStatus { OK=200, NotFound=404 }")
})
it("should format enum without explicit values", () => {
const structure: ProjectStructure = {
name: "test",
rootPath: "/test",
files: ["enums.ts"],
directories: [],
}
const asts = new Map<string, FileAST>([
[
"enums.ts",
{
imports: [],
exports: [],
functions: [],
classes: [],
interfaces: [],
typeAliases: [],
enums: [
{
name: "Direction",
lineStart: 1,
lineEnd: 5,
members: [
{ name: "Up", value: undefined },
{ name: "Down", value: undefined },
],
isExported: true,
isConst: false,
},
],
parseError: false,
},
],
])
const context = buildInitialContext(structure, asts)
expect(context).toContain("- enum Direction { Up, Down }")
})
it("should format empty enum", () => {
const structure: ProjectStructure = {
name: "test",
rootPath: "/test",
files: ["enums.ts"],
directories: [],
}
const asts = new Map<string, FileAST>([
[
"enums.ts",
{
imports: [],
exports: [],
functions: [],
classes: [],
interfaces: [],
typeAliases: [],
enums: [
{
name: "Empty",
lineStart: 1,
lineEnd: 1,
members: [],
isExported: true,
isConst: false,
},
],
parseError: false,
},
],
])
const context = buildInitialContext(structure, asts)
expect(context).toContain("- enum Empty")
})
it("should include enum in compact format", () => {
const structure: ProjectStructure = {
name: "test",
rootPath: "/test",
files: ["enums.ts"],
directories: [],
}
const asts = new Map<string, FileAST>([
[
"enums.ts",
{
imports: [],
exports: [],
functions: [],
classes: [],
interfaces: [],
typeAliases: [],
enums: [
{
name: "Status",
lineStart: 1,
lineEnd: 5,
members: [{ name: "Active", value: 1 }],
isExported: true,
isConst: false,
},
],
parseError: false,
},
],
])
const context = buildInitialContext(structure, asts, undefined, {
includeSignatures: false,
})
expect(context).toContain("enum: Status")
})
it("should truncate long enum definitions", () => {
const structure: ProjectStructure = {
name: "test",
rootPath: "/test",
files: ["enums.ts"],
directories: [],
}
const asts = new Map<string, FileAST>([
[
"enums.ts",
{
imports: [],
exports: [],
functions: [],
classes: [],
interfaces: [],
typeAliases: [],
enums: [
{
name: "VeryLongEnumName",
lineStart: 1,
lineEnd: 20,
members: [
{ name: "FirstVeryLongMemberName", value: "first_value" },
{ name: "SecondVeryLongMemberName", value: "second_value" },
{ name: "ThirdVeryLongMemberName", value: "third_value" },
],
isExported: true,
isConst: false,
},
],
parseError: false,
},
],
])
const context = buildInitialContext(structure, asts)
expect(context).toContain("...")
expect(context).toContain("- enum VeryLongEnumName")
})
})
}) })