feat(ipuaro): implement v0.1.0 foundation

- Project setup with tsup, vitest, ESM support
- Domain entities: Session, Project
- Value objects: FileData, FileAST, FileMeta, ChatMessage, ToolCall, ToolResult, UndoEntry
- Service interfaces: IStorage, ILLMClient, ITool, IIndexer, IToolRegistry
- Shared: Config (zod), IpuaroError, utils (hash, tokens), Result type
- CLI with placeholder commands (start, init, index)
- 91 unit tests with 100% coverage
- Fix package scope @puaros -> @samiyev in CLAUDE.md
This commit is contained in:
imfozilbek
2025-11-29 23:08:38 +05:00
parent 7f6180df37
commit 130a8c4f24
62 changed files with 4629 additions and 6 deletions

View File

@@ -0,0 +1,80 @@
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"
import {
createUserMessage,
createAssistantMessage,
createToolMessage,
createSystemMessage,
} from "../../../../src/domain/value-objects/ChatMessage.js"
describe("ChatMessage", () => {
beforeEach(() => {
vi.useFakeTimers()
vi.setSystemTime(new Date("2025-01-01T00:00:00Z"))
})
afterEach(() => {
vi.useRealTimers()
})
describe("createUserMessage", () => {
it("should create user message", () => {
const msg = createUserMessage("Hello")
expect(msg.role).toBe("user")
expect(msg.content).toBe("Hello")
expect(msg.timestamp).toBe(Date.now())
})
})
describe("createAssistantMessage", () => {
it("should create assistant message without tool calls", () => {
const msg = createAssistantMessage("Response")
expect(msg.role).toBe("assistant")
expect(msg.content).toBe("Response")
expect(msg.toolCalls).toBeUndefined()
})
it("should create assistant message with tool calls", () => {
const toolCalls = [
{ id: "1", name: "get_lines", params: {}, timestamp: Date.now() },
]
const stats = { tokens: 100, timeMs: 500, toolCalls: 1 }
const msg = createAssistantMessage("Response", toolCalls, stats)
expect(msg.toolCalls).toEqual(toolCalls)
expect(msg.stats).toEqual(stats)
})
})
describe("createToolMessage", () => {
it("should create tool message with results", () => {
const results = [
{ callId: "1", success: true, data: "data", executionTimeMs: 10 },
]
const msg = createToolMessage(results)
expect(msg.role).toBe("tool")
expect(msg.toolResults).toEqual(results)
expect(msg.content).toContain("[1] Success")
})
it("should format error results", () => {
const results = [
{ callId: "2", success: false, error: "Not found", executionTimeMs: 5 },
]
const msg = createToolMessage(results)
expect(msg.content).toContain("[2] Error: Not found")
})
})
describe("createSystemMessage", () => {
it("should create system message", () => {
const msg = createSystemMessage("System prompt")
expect(msg.role).toBe("system")
expect(msg.content).toBe("System prompt")
})
})
})

View File

@@ -0,0 +1,19 @@
import { describe, it, expect } from "vitest"
import { createEmptyFileAST } from "../../../../src/domain/value-objects/FileAST.js"
describe("FileAST", () => {
describe("createEmptyFileAST", () => {
it("should create empty AST with all arrays empty", () => {
const ast = createEmptyFileAST()
expect(ast.imports).toEqual([])
expect(ast.exports).toEqual([])
expect(ast.functions).toEqual([])
expect(ast.classes).toEqual([])
expect(ast.interfaces).toEqual([])
expect(ast.typeAliases).toEqual([])
expect(ast.parseError).toBe(false)
expect(ast.parseErrorMessage).toBeUndefined()
})
})
})

View File

@@ -0,0 +1,39 @@
import { describe, it, expect } from "vitest"
import {
createFileData,
isFileDataEqual,
} from "../../../../src/domain/value-objects/FileData.js"
describe("FileData", () => {
describe("createFileData", () => {
it("should create FileData with all fields", () => {
const lines = ["line1", "line2"]
const hash = "abc123"
const size = 100
const lastModified = Date.now()
const result = createFileData(lines, hash, size, lastModified)
expect(result.lines).toEqual(lines)
expect(result.hash).toBe(hash)
expect(result.size).toBe(size)
expect(result.lastModified).toBe(lastModified)
})
})
describe("isFileDataEqual", () => {
it("should return true for equal hashes", () => {
const a = createFileData(["a"], "hash1", 1, 1)
const b = createFileData(["b"], "hash1", 2, 2)
expect(isFileDataEqual(a, b)).toBe(true)
})
it("should return false for different hashes", () => {
const a = createFileData(["a"], "hash1", 1, 1)
const b = createFileData(["a"], "hash2", 1, 1)
expect(isFileDataEqual(a, b)).toBe(false)
})
})
})

View File

@@ -0,0 +1,48 @@
import { describe, it, expect } from "vitest"
import {
createFileMeta,
isHubFile,
} from "../../../../src/domain/value-objects/FileMeta.js"
describe("FileMeta", () => {
describe("createFileMeta", () => {
it("should create FileMeta with defaults", () => {
const meta = createFileMeta()
expect(meta.complexity.loc).toBe(0)
expect(meta.complexity.nesting).toBe(0)
expect(meta.complexity.cyclomaticComplexity).toBe(1)
expect(meta.complexity.score).toBe(0)
expect(meta.dependencies).toEqual([])
expect(meta.dependents).toEqual([])
expect(meta.isHub).toBe(false)
expect(meta.isEntryPoint).toBe(false)
expect(meta.fileType).toBe("unknown")
})
it("should merge partial values", () => {
const meta = createFileMeta({
isHub: true,
fileType: "source",
dependencies: ["dep1.ts"],
})
expect(meta.isHub).toBe(true)
expect(meta.fileType).toBe("source")
expect(meta.dependencies).toEqual(["dep1.ts"])
expect(meta.dependents).toEqual([])
})
})
describe("isHubFile", () => {
it("should return true for >5 dependents", () => {
expect(isHubFile(6)).toBe(true)
expect(isHubFile(10)).toBe(true)
})
it("should return false for <=5 dependents", () => {
expect(isHubFile(5)).toBe(false)
expect(isHubFile(0)).toBe(false)
})
})
})

View File

@@ -0,0 +1,31 @@
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"
import { createToolCall } from "../../../../src/domain/value-objects/ToolCall.js"
describe("ToolCall", () => {
beforeEach(() => {
vi.useFakeTimers()
vi.setSystemTime(new Date("2025-01-01T00:00:00Z"))
})
afterEach(() => {
vi.useRealTimers()
})
describe("createToolCall", () => {
it("should create tool call with all fields", () => {
const params = { path: "test.ts", line: 10 }
const call = createToolCall("call-1", "get_lines", params)
expect(call.id).toBe("call-1")
expect(call.name).toBe("get_lines")
expect(call.params).toEqual(params)
expect(call.timestamp).toBe(Date.now())
})
it("should handle empty params", () => {
const call = createToolCall("call-2", "git_status", {})
expect(call.params).toEqual({})
})
})
})

View File

@@ -0,0 +1,32 @@
import { describe, it, expect } from "vitest"
import {
createSuccessResult,
createErrorResult,
} from "../../../../src/domain/value-objects/ToolResult.js"
describe("ToolResult", () => {
describe("createSuccessResult", () => {
it("should create success result", () => {
const data = { lines: ["line1", "line2"] }
const result = createSuccessResult("call-1", data, 50)
expect(result.callId).toBe("call-1")
expect(result.success).toBe(true)
expect(result.data).toEqual(data)
expect(result.executionTimeMs).toBe(50)
expect(result.error).toBeUndefined()
})
})
describe("createErrorResult", () => {
it("should create error result", () => {
const result = createErrorResult("call-2", "File not found", 10)
expect(result.callId).toBe("call-2")
expect(result.success).toBe(false)
expect(result.error).toBe("File not found")
expect(result.executionTimeMs).toBe(10)
expect(result.data).toBeUndefined()
})
})
})

View File

@@ -0,0 +1,87 @@
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"
import {
createUndoEntry,
canUndo,
} from "../../../../src/domain/value-objects/UndoEntry.js"
describe("UndoEntry", () => {
beforeEach(() => {
vi.useFakeTimers()
vi.setSystemTime(new Date("2025-01-01T00:00:00Z"))
})
afterEach(() => {
vi.useRealTimers()
})
describe("createUndoEntry", () => {
it("should create undo entry with all fields", () => {
const entry = createUndoEntry(
"undo-1",
"test.ts",
["old line"],
["new line"],
"Edit line 1"
)
expect(entry.id).toBe("undo-1")
expect(entry.filePath).toBe("test.ts")
expect(entry.previousContent).toEqual(["old line"])
expect(entry.newContent).toEqual(["new line"])
expect(entry.description).toBe("Edit line 1")
expect(entry.timestamp).toBe(Date.now())
expect(entry.toolCallId).toBeUndefined()
})
it("should create undo entry with toolCallId", () => {
const entry = createUndoEntry(
"undo-2",
"test.ts",
[],
[],
"Create file",
"tool-123"
)
expect(entry.toolCallId).toBe("tool-123")
})
})
describe("canUndo", () => {
it("should return true when current content matches newContent", () => {
const entry = createUndoEntry(
"undo-1",
"test.ts",
["old"],
["new"],
"Edit"
)
expect(canUndo(entry, ["new"])).toBe(true)
})
it("should return false when content differs", () => {
const entry = createUndoEntry(
"undo-1",
"test.ts",
["old"],
["new"],
"Edit"
)
expect(canUndo(entry, ["modified"])).toBe(false)
})
it("should return false when length differs", () => {
const entry = createUndoEntry(
"undo-1",
"test.ts",
["old"],
["new"],
"Edit"
)
expect(canUndo(entry, ["new", "extra"])).toBe(false)
})
})
})