Compare commits

...

9 Commits

Author SHA1 Message Date
imfozilbek
a3f0ba948f chore(ipuaro): release v0.23.0 2025-12-04 19:59:36 +05:00
imfozilbek
141888bf59 feat(ipuaro): add JSON/YAML parsing and symlinks metadata
- Add YAML parsing using yaml npm package
- Add JSON parsing using tree-sitter-json
- Add symlinkTarget to ScanResult interface
- Update ROADMAP: verify v0.20.0-v0.23.0 complete
- Add 8 new tests (1687 total)
2025-12-04 19:57:06 +05:00
imfozilbek
b0f1778f3a docs(guardian): add research citations for 15 roadmap features
Add comprehensive research citations for upcoming features:
- Domain Event Usage Validation (Section 15)
- Value Object Immutability (Section 16)
- CQS/CQRS (Section 17)
- Factory Pattern (Section 18)
- Specification Pattern (Section 19)
- Bounded Context (Section 20)
- Persistence Ignorance (Section 21)
- Null Object Pattern (Section 22)
- Primitive Obsession (Section 23)
- Service Locator Anti-pattern (Section 24)
- Double Dispatch/Visitor Pattern (Section 25)
- Entity Identity (Section 26)
- Saga Pattern (Section 27)
- Anti-Corruption Layer (Section 28)
- Ubiquitous Language (Section 29)

Sources include: GoF Design Patterns, Bertrand Meyer, Eric Evans,
Vaughn Vernon, Martin Fowler, Chris Richardson, Mark Seemann,
and academic papers (Garcia-Molina Sagas 1987).

Document version: 1.1 → 2.0
2025-12-04 19:11:54 +05:00
imfozilbek
9c94335729 feat(ipuaro): add commands configuration
- Add CommandsConfigSchema with timeout option
- Integrate timeout configuration in RunCommandTool
- Add 22 new unit tests (19 schema + 3 integration)
- Complete v0.22.0 Extended Configuration milestone
2025-12-02 03:03:57 +05:00
imfozilbek
c34d57c231 chore(ipuaro): release v0.22.4 2025-12-02 02:29:56 +05:00
imfozilbek
60052c0db9 feat(ipuaro): add autocomplete configuration
- Add AutocompleteConfigSchema with enabled, source, maxSuggestions
- Update useAutocomplete hook to read from config
- Add 27 unit tests for autocomplete config
- Fix unused variable in Chat component
- Update ROADMAP and CHANGELOG
2025-12-02 02:26:36 +05:00
imfozilbek
fa647c41aa feat(ipuaro): add context configuration
- Add ContextConfigSchema with systemPromptTokens, maxContextUsage, autoCompressAt, compressionMethod
- Update ContextManager to read compression threshold from config
- Update HandleMessage and useSession to pass context config
- Add 40 unit tests (32 schema + 8 integration)
- Coverage: 97.63% lines, 91.34% branches
2025-12-02 02:02:34 +05:00
imfozilbek
98b365bd94 chore(ipuaro): release v0.22.2 2025-12-02 01:39:37 +05:00
imfozilbek
a7669f8947 feat(ipuaro): add session configuration
- Add SessionConfigSchema with persistIndefinitely, maxHistoryMessages, saveInputHistory
- Implement Session.truncateHistory() method for limiting message history
- Update HandleMessage to support history truncation and input history toggle
- Add config flow through useSession and App components
- Add 19 unit tests for SessionConfigSchema
- Update CHANGELOG.md and ROADMAP.md for v0.22.2
2025-12-02 01:34:04 +05:00
37 changed files with 2461 additions and 206 deletions

View File

@@ -20,6 +20,21 @@ This document provides authoritative sources, academic papers, industry standard
12. [Aggregate Boundary Validation (DDD Tactical Patterns)](#12-aggregate-boundary-validation-ddd-tactical-patterns)
13. [Secret Detection & Security](#13-secret-detection--security)
14. [Severity-Based Prioritization & Technical Debt](#14-severity-based-prioritization--technical-debt)
15. [Domain Event Usage Validation](#15-domain-event-usage-validation)
16. [Value Object Immutability](#16-value-object-immutability)
17. [Command Query Separation (CQS/CQRS)](#17-command-query-separation-cqscqrs)
18. [Factory Pattern](#18-factory-pattern)
19. [Specification Pattern](#19-specification-pattern)
20. [Bounded Context](#20-bounded-context)
21. [Persistence Ignorance](#21-persistence-ignorance)
22. [Null Object Pattern](#22-null-object-pattern)
23. [Primitive Obsession](#23-primitive-obsession)
24. [Service Locator Anti-pattern](#24-service-locator-anti-pattern)
25. [Double Dispatch and Visitor Pattern](#25-double-dispatch-and-visitor-pattern)
26. [Entity Identity](#26-entity-identity)
27. [Saga Pattern](#27-saga-pattern)
28. [Anti-Corruption Layer](#28-anti-corruption-layer)
29. [Ubiquitous Language](#29-ubiquitous-language)
---
@@ -801,22 +816,840 @@ This document provides authoritative sources, academic papers, industry standard
---
## 15. Domain Event Usage Validation
### Eric Evans: Domain-Driven Design (2003)
**Original Definition:**
- Domain Events: "Something happened that domain experts care about"
- Events capture facts about the domain that have already occurred
- Distinct from system events - they model business-relevant occurrences
- Reference: [Martin Fowler - Domain Event](https://martinfowler.com/eaaDev/DomainEvent.html)
**Book: Domain-Driven Design** (2003)
- Author: Eric Evans
- Publisher: Addison-Wesley Professional
- ISBN: 978-0321125217
- Domain Events weren't explicitly in the original book but evolved from DDD community
- Reference: [DDD Community - Domain Events](https://www.domainlanguage.com/)
### Vaughn Vernon: Implementing Domain-Driven Design (2013)
**Chapter 8: Domain Events**
- Author: Vaughn Vernon
- Comprehensive coverage of Domain Events implementation
- "Model information about activity in the domain as a series of discrete events"
- Reference: [Amazon - Implementing DDD](https://www.amazon.com/Implementing-Domain-Driven-Design-Vaughn-Vernon/dp/0321834577)
**Key Principles:**
- Events should be immutable
- Named in past tense (OrderPlaced, UserRegistered)
- Contain all data needed by handlers
- Enable loose coupling between aggregates
### Martin Fowler's Event Patterns
**Event Sourcing:**
- "Capture all changes to an application state as a sequence of events"
- Events become the primary source of truth
- Reference: [Martin Fowler - Event Sourcing](https://martinfowler.com/eaaDev/EventSourcing.html)
**Event-Driven Architecture:**
- Promotes loose coupling between components
- Enables asynchronous processing
- Reference: [Martin Fowler - Event-Driven](https://martinfowler.com/articles/201701-event-driven.html)
### Why Direct Infrastructure Calls Are Bad
**Coupling Issues:**
- Direct calls create tight coupling between domain and infrastructure
- Makes testing difficult (need to mock infrastructure)
- Violates Single Responsibility Principle
- Reference: [Microsoft - Domain Events Design](https://learn.microsoft.com/en-us/dotnet/architecture/microservices/microservice-ddd-cqrs-patterns/domain-events-design-implementation)
**Benefits of Domain Events:**
- Decouples domain from side effects
- Enables eventual consistency
- Improves testability
- Supports audit logging naturally
- Reference: [Jimmy Bogard - Domain Events](https://lostechies.com/jimmybogard/2010/04/08/strengthening-your-domain-domain-events/)
---
## 16. Value Object Immutability
### Eric Evans: Domain-Driven Design (2003)
**Value Object Definition:**
- "An object that describes some characteristic or attribute but carries no concept of identity"
- "Value Objects should be immutable"
- When you care only about the attributes of an element, classify it as a Value Object
- Reference: [Martin Fowler - Value Object](https://martinfowler.com/bliki/ValueObject.html)
**Immutability Requirement:**
- "Treat the Value Object as immutable"
- "Don't give it any identity and avoid the design complexities necessary to maintain Entities"
- Reference: [DDD Reference - Value Objects](https://www.domainlanguage.com/wp-content/uploads/2016/05/DDD_Reference_2015-03.pdf)
### Martin Fowler on Value Objects
**Blog Post: Value Object** (2016)
- "A small simple object, like money or a date range, whose equality isn't based on identity"
- "I consider value objects to be one of the most important building blocks of good domain models"
- Reference: [Martin Fowler - Value Object](https://martinfowler.com/bliki/ValueObject.html)
**Key Properties:**
- Equality based on attribute values, not identity
- Should be immutable (once created, cannot be changed)
- Side-effect free behavior
- Self-validating (validate in constructor)
### Vaughn Vernon: Implementing DDD
**Chapter 6: Value Objects**
- Detailed implementation guidance
- "Measures, quantifies, or describes a thing in the domain"
- "Can be compared with other Value Objects using value equality"
- "Completely replaceable when the measurement changes"
- Reference: [Vaughn Vernon - Implementing DDD](https://www.amazon.com/Implementing-Domain-Driven-Design-Vaughn-Vernon/dp/0321834577)
### Why Immutability Matters
**Thread Safety:**
- Immutable objects are inherently thread-safe
- No synchronization needed for concurrent access
- Reference: [Effective Java - Item 17](https://www.amazon.com/Effective-Java-Joshua-Bloch/dp/0134685997)
**Reasoning About Code:**
- Easier to understand code when objects don't change
- No defensive copying needed
- Simplifies caching and optimization
- Reference: [Oracle Java Tutorials - Immutable Objects](https://docs.oracle.com/javase/tutorial/essential/concurrency/immutable.html)
**Functional Programming Influence:**
- Immutability is a core principle of functional programming
- Reduces side effects and makes code more predictable
- Reference: [Wikipedia - Immutable Object](https://en.wikipedia.org/wiki/Immutable_object)
---
## 17. Command Query Separation (CQS/CQRS)
### Bertrand Meyer: Original CQS Principle
**Book: Object-Oriented Software Construction** (1988, 2nd Ed. 1997)
- Author: Bertrand Meyer
- Publisher: Prentice Hall
- ISBN: 978-0136291558
- Introduced Command Query Separation principle
- Reference: [Wikipedia - CQS](https://en.wikipedia.org/wiki/Command%E2%80%93query_separation)
**CQS Principle:**
- "Every method should either be a command that performs an action, or a query that returns data to the caller, but not both"
- Commands: change state, return nothing (void)
- Queries: return data, change nothing (side-effect free)
- Reference: [Martin Fowler - CommandQuerySeparation](https://martinfowler.com/bliki/CommandQuerySeparation.html)
### Greg Young: CQRS Pattern
**CQRS Documents** (2010)
- Author: Greg Young
- Extended CQS to architectural pattern
- "CQRS is simply the creation of two objects where there was previously only one"
- Reference: [Greg Young - CQRS Documents](https://cqrs.files.wordpress.com/2010/11/cqrs_documents.pdf)
**Key Concepts:**
- Separate models for reading and writing
- Write model (commands) optimized for business logic
- Read model (queries) optimized for display/reporting
- Reference: [Microsoft - CQRS Pattern](https://learn.microsoft.com/en-us/azure/architecture/patterns/cqrs)
### Martin Fowler on CQRS
**Blog Post: CQRS** (2011)
- "At its heart is the notion that you can use a different model to update information than the model you use to read information"
- Warns against overuse: "CQRS is a significant mental leap for all concerned"
- Reference: [Martin Fowler - CQRS](https://martinfowler.com/bliki/CQRS.html)
### Benefits and Trade-offs
**Benefits:**
- Independent scaling of read and write workloads
- Optimized data schemas for each side
- Improved security (separate read/write permissions)
- Reference: [AWS - CQRS Pattern](https://docs.aws.amazon.com/prescriptive-guidance/latest/modernization-data-persistence/cqrs-pattern.html)
**Trade-offs:**
- Increased complexity
- Eventual consistency challenges
- More code to maintain
- Reference: [Microsoft - CQRS Considerations](https://learn.microsoft.com/en-us/azure/architecture/patterns/cqrs#issues-and-considerations)
---
## 18. Factory Pattern
### Gang of Four: Design Patterns (1994)
**Book: Design Patterns: Elements of Reusable Object-Oriented Software**
- Authors: Erich Gamma, Richard Helm, Ralph Johnson, John Vlissides (Gang of Four)
- Publisher: Addison-Wesley
- ISBN: 978-0201633610
- Defines Factory Method and Abstract Factory patterns
- Reference: [Wikipedia - Design Patterns](https://en.wikipedia.org/wiki/Design_Patterns)
**Factory Method Pattern:**
- "Define an interface for creating an object, but let subclasses decide which class to instantiate"
- Lets a class defer instantiation to subclasses
- Reference: [Refactoring Guru - Factory Method](https://refactoring.guru/design-patterns/factory-method)
**Abstract Factory Pattern:**
- "Provide an interface for creating families of related or dependent objects without specifying their concrete classes"
- Reference: [Refactoring Guru - Abstract Factory](https://refactoring.guru/design-patterns/abstract-factory)
### Eric Evans: Factory in DDD Context
**Domain-Driven Design** (2003)
- Chapter 6: "The Life Cycle of a Domain Object"
- Factories encapsulate complex object creation
- "Shift the responsibility for creating instances of complex objects and Aggregates to a separate object"
- Reference: [DDD Reference](https://www.domainlanguage.com/wp-content/uploads/2016/05/DDD_Reference_2015-03.pdf)
**DDD Factory Guidelines:**
- Factory should create valid objects (invariants satisfied)
- Two types: Factory for new objects, Factory for reconstitution
- Keep creation logic out of the entity itself
- Reference: Already in Section 10 - Domain-Driven Design
### Why Factories Matter in DDD
**Encapsulation of Creation Logic:**
- Complex aggregates need coordinated creation
- Business rules should be enforced at creation time
- Clients shouldn't know construction details
- Reference: [Vaughn Vernon - Implementing DDD, Chapter 11](https://www.amazon.com/Implementing-Domain-Driven-Design-Vaughn-Vernon/dp/0321834577)
**Factory vs Constructor:**
- Constructors should be simple (assign values)
- Factories handle complex creation logic
- Factories can return different types
- Reference: [Effective Java - Item 1: Static Factory Methods](https://www.amazon.com/Effective-Java-Joshua-Bloch/dp/0134685997)
---
## 19. Specification Pattern
### Eric Evans & Martin Fowler
**Original Paper: Specifications** (1997)
- Authors: Eric Evans and Martin Fowler
- Introduced the Specification pattern
- "A Specification states a constraint on the state of another object"
- Reference: [Martin Fowler - Specification](https://martinfowler.com/apsupp/spec.pdf)
**Domain-Driven Design** (2003)
- Chapter 9: "Making Implicit Concepts Explicit"
- Specifications make business rules explicit and reusable
- "Create explicit predicate-like Value Objects for specialized purposes"
- Reference: [DDD Reference](https://www.domainlanguage.com/wp-content/uploads/2016/05/DDD_Reference_2015-03.pdf)
### Pattern Definition
**Core Concept:**
- Specification is a predicate that determines if an object satisfies some criteria
- Encapsulates business rules that can be reused and combined
- Reference: [Wikipedia - Specification Pattern](https://en.wikipedia.org/wiki/Specification_pattern)
**Three Main Uses:**
1. **Selection**: Finding objects that match criteria
2. **Validation**: Checking if object satisfies rules
3. **Construction**: Describing what needs to be created
- Reference: [Martin Fowler - Specification](https://martinfowler.com/apsupp/spec.pdf)
### Composite Specifications
**Combining Specifications:**
- AND: Both specifications must be satisfied
- OR: Either specification must be satisfied
- NOT: Specification must not be satisfied
- Reference: [Refactoring Guru - Specification Pattern](https://refactoring.guru/design-patterns/specification)
**Benefits:**
- Reusable business rules
- Testable in isolation
- Readable domain language
- Composable for complex rules
- Reference: [Enterprise Craftsmanship - Specification Pattern](https://enterprisecraftsmanship.com/posts/specification-pattern-c-implementation/)
---
## 20. Bounded Context
### Eric Evans: Domain-Driven Design (2003)
**Original Definition:**
- "A Bounded Context delimits the applicability of a particular model"
- "Explicitly define the context within which a model applies"
- Chapter 14: "Maintaining Model Integrity"
- Reference: [Martin Fowler - Bounded Context](https://martinfowler.com/bliki/BoundedContext.html)
**Key Principles:**
- Each Bounded Context has its own Ubiquitous Language
- Same term can mean different things in different contexts
- Models should not be shared across context boundaries
- Reference: [DDD Reference](https://www.domainlanguage.com/wp-content/uploads/2016/05/DDD_Reference_2015-03.pdf)
### Vaughn Vernon: Strategic Design
**Implementing Domain-Driven Design** (2013)
- Chapter 2: "Domains, Subdomains, and Bounded Contexts"
- Detailed guidance on identifying and mapping contexts
- Reference: [Vaughn Vernon - Implementing DDD](https://www.amazon.com/Implementing-Domain-Driven-Design-Vaughn-Vernon/dp/0321834577)
**Context Mapping Patterns:**
- Shared Kernel
- Customer/Supplier
- Conformist
- Anti-Corruption Layer
- Open Host Service / Published Language
- Reference: [Context Mapping Patterns](https://www.infoq.com/articles/ddd-contextmapping/)
### Why Bounded Contexts Matter
**Avoiding Big Ball of Mud:**
- Without explicit boundaries, models become entangled
- Different teams step on each other's models
- Reference: [Wikipedia - Big Ball of Mud](https://en.wikipedia.org/wiki/Big_ball_of_mud)
**Microservices and Bounded Contexts:**
- "Microservices should be designed around business capabilities, aligned with bounded contexts"
- Each microservice typically represents one bounded context
- Reference: [Microsoft - Microservices and Bounded Contexts](https://learn.microsoft.com/en-us/azure/architecture/microservices/model/domain-analysis)
### Cross-Context Communication
**Integration Patterns:**
- Never share domain models across contexts
- Use integration events or APIs
- Translate between context languages
- Reference: [Microsoft - Tactical DDD](https://learn.microsoft.com/en-us/azure/architecture/microservices/model/tactical-ddd)
---
## 21. Persistence Ignorance
### Definition and Principles
**Core Concept:**
- Domain objects should have no knowledge of how they are persisted
- Business logic remains pure and testable
- Infrastructure concerns are separated from domain
- Reference: [Microsoft - Persistence Ignorance](https://learn.microsoft.com/en-us/dotnet/architecture/microservices/microservice-ddd-cqrs-patterns/infrastructure-persistence-layer-design#the-persistence-ignorance-principle)
**Wikipedia Definition:**
- "Persistence ignorance is the ability of a class to be used without any underlying persistence mechanism"
- Objects don't know if/how they'll be stored
- Reference: [Wikipedia - Persistence Ignorance](https://en.wikipedia.org/wiki/Persistence_ignorance)
### Eric Evans: DDD and Persistence
**Domain-Driven Design** (2003)
- Repositories abstract away persistence details
- Domain model should not reference ORM or database concepts
- Reference: Already covered in Section 6 - Repository Pattern
**Key Quote:**
- "The domain layer should be kept clean of all technical concerns"
- ORM annotations violate this principle
- Reference: [Clean Architecture and DDD](https://herbertograca.com/2017/11/16/explicit-architecture-01-ddd-hexagonal-onion-clean-cqrs-how-i-put-it-all-together/)
### Clean Architecture Alignment
**Robert C. Martin:**
- "The database is a detail"
- Domain entities should not depend on persistence frameworks
- Use Repository interfaces to abstract persistence
- Reference: [Clean Architecture Book](https://www.amazon.com/Clean-Architecture-Craftsmans-Software-Structure/dp/0134494164)
### Practical Implementation
**Two-Model Approach:**
- Domain Model: Pure business objects
- Persistence Model: ORM-annotated entities
- Mappers translate between them
- Reference: [Microsoft - Infrastructure Layer](https://learn.microsoft.com/en-us/dotnet/architecture/microservices/microservice-ddd-cqrs-patterns/infrastructure-persistence-layer-design)
**Benefits:**
- Domain model can evolve independently of database schema
- Easier testing (no ORM required)
- Database can be changed without affecting domain
- Reference: [Enterprise Craftsmanship - Persistence Ignorance](https://enterprisecraftsmanship.com/posts/persistence-ignorance/)
---
## 22. Null Object Pattern
### Original Pattern
**Pattern Languages of Program Design 3** (1997)
- Author: Bobby Woolf
- Chapter: "Null Object"
- Publisher: Addison-Wesley
- ISBN: 978-0201310115
- Reference: [Wikipedia - Null Object Pattern](https://en.wikipedia.org/wiki/Null_object_pattern)
**Definition:**
- "A Null Object provides a 'do nothing' behavior, hiding the details from its collaborators"
- Replaces null checks with polymorphism
- Reference: [Refactoring Guru - Null Object](https://refactoring.guru/introduce-null-object)
### Martin Fowler's Coverage
**Refactoring Book** (1999, 2018)
- "Introduce Null Object" refactoring
- "Replace conditional logic that checks for null with a null object"
- Reference: [Refactoring Catalog](https://refactoring.com/catalog/introduceNullObject.html)
**Special Case Pattern:**
- More general pattern that includes Null Object
- "A subclass that provides special behavior for particular cases"
- Reference: [Martin Fowler - Special Case](https://martinfowler.com/eaaCatalog/specialCase.html)
### Benefits
**Eliminates Null Checks:**
- Reduces cyclomatic complexity
- Cleaner, more readable code
- Follows "Tell, Don't Ask" principle
- Reference: [SourceMaking - Null Object](https://sourcemaking.com/design_patterns/null_object)
**Polymorphism Over Conditionals:**
- Null Object responds to same interface as real object
- Default/neutral behavior instead of null checks
- Reference: [C2 Wiki - Null Object](https://wiki.c2.com/?NullObject)
### When to Use
**Good Candidates:**
- Objects frequently checked for null
- Null represents "absence" with sensible default behavior
- Reference: [Baeldung - Null Object Pattern](https://www.baeldung.com/java-null-object-pattern)
**Cautions:**
- Don't use when null has semantic meaning
- Can hide bugs if misapplied
- Reference: [Stack Overflow - Null Object Considerations](https://stackoverflow.com/questions/1274792/is-the-null-object-pattern-a-bad-practice)
---
## 23. Primitive Obsession
### Code Smell Definition
**Martin Fowler: Refactoring** (1999, 2018)
- Primitive Obsession is a code smell
- "Using primitives instead of small objects for simple tasks"
- Reference: [Refactoring Catalog](https://refactoring.com/catalog/)
**Wikipedia Definition:**
- "Using primitive data types to represent domain ideas"
- Example: Using string for email, int for money
- Reference: [Wikipedia - Code Smell](https://en.wikipedia.org/wiki/Code_smell)
### Why It's a Problem
**Lost Type Safety:**
- String can contain anything, Email cannot
- Compiler can't catch domain errors
- Reference: [Refactoring Guru - Primitive Obsession](https://refactoring.guru/smells/primitive-obsession)
**Scattered Validation:**
- Same validation repeated in multiple places
- Violates DRY principle
- Reference: [SourceMaking - Primitive Obsession](https://sourcemaking.com/refactoring/smells/primitive-obsession)
**Missing Behavior:**
- Primitives can't have domain-specific methods
- Logic lives in services instead of objects
- Reference: [Enterprise Craftsmanship - Primitive Obsession](https://enterprisecraftsmanship.com/posts/functional-c-primitive-obsession/)
### Solutions
**Replace with Value Objects:**
- Money instead of decimal
- Email instead of string
- PhoneNumber instead of string
- Reference: Already covered in Section 16 - Value Object Immutability
**Replace Data Value with Object:**
- Refactoring: "Replace Data Value with Object"
- Introduce Parameter Object for related primitives
- Reference: [Refactoring - Replace Data Value with Object](https://refactoring.com/catalog/replaceDataValueWithObject.html)
### Common Primitive Obsession Examples
**Frequently Misused Primitives:**
- string for: email, phone, URL, currency code, country code
- int/decimal for: money, percentage, age, quantity
- DateTime for: date ranges, business dates
- Reference: [DDD - Value Objects](https://martinfowler.com/bliki/ValueObject.html)
---
## 24. Service Locator Anti-pattern
### Martin Fowler's Analysis
**Blog Post: Inversion of Control Containers and the Dependency Injection pattern** (2004)
- Compares Service Locator with Dependency Injection
- "With service locator the application class asks for it explicitly by a message to the locator"
- Reference: [Martin Fowler - Inversion of Control](https://martinfowler.com/articles/injection.html)
**Service Locator Definition:**
- "The basic idea behind a service locator is to have an object that knows how to get hold of all of the services that an application might need"
- Acts as a registry that provides dependencies on demand
- Reference: [Martin Fowler - Service Locator](https://martinfowler.com/articles/injection.html#UsingAServiceLocator)
### Why It's Considered an Anti-pattern
**Mark Seemann: Dependency Injection in .NET** (2011, 2nd Ed. 2019)
- Author: Mark Seemann
- Extensively covers why Service Locator is problematic
- "Service Locator is an anti-pattern"
- Reference: [Mark Seemann - Service Locator is an Anti-Pattern](https://blog.ploeh.dk/2010/02/03/ServiceLocatorisanAnti-Pattern/)
**Hidden Dependencies:**
- Dependencies are not visible in constructor
- Makes code harder to understand and test
- Violates Explicit Dependencies Principle
- Reference: [DevIQ - Explicit Dependencies](https://deviq.com/principles/explicit-dependencies-principle)
**Testing Difficulties:**
- Need to set up global locator for tests
- Tests become coupled to locator setup
- Reference: [Stack Overflow - Service Locator Testing](https://stackoverflow.com/questions/1557781/is-service-locator-an-anti-pattern)
### Dependency Injection Alternative
**Constructor Injection:**
- Dependencies declared in constructor
- Compiler enforces dependency provision
- Clear, testable code
- Reference: Already covered in Section 6 - Repository Pattern
**Benefits over Service Locator:**
- Explicit dependencies
- Easier testing (just pass mocks)
- IDE support for navigation
- Compile-time checking
- Reference: [Martin Fowler - Constructor Injection](https://martinfowler.com/articles/injection.html#ConstructorInjectionWithPicocontainer)
---
## 25. Double Dispatch and Visitor Pattern
### Gang of Four: Visitor Pattern
**Design Patterns** (1994)
- Authors: Gang of Four
- Visitor Pattern chapter
- "Represent an operation to be performed on the elements of an object structure"
- Reference: [Wikipedia - Visitor Pattern](https://en.wikipedia.org/wiki/Visitor_pattern)
**Intent:**
- "Lets you define a new operation without changing the classes of the elements on which it operates"
- Separates algorithms from object structure
- Reference: [Refactoring Guru - Visitor](https://refactoring.guru/design-patterns/visitor)
### Double Dispatch Mechanism
**Definition:**
- "A mechanism that dispatches a function call to different concrete functions depending on the runtime types of two objects involved in the call"
- Visitor pattern uses double dispatch
- Reference: [Wikipedia - Double Dispatch](https://en.wikipedia.org/wiki/Double_dispatch)
**How It Works:**
1. Client calls element.accept(visitor)
2. Element calls visitor.visit(this) - first dispatch
3. Correct visit() overload selected - second dispatch
- Reference: [SourceMaking - Visitor](https://sourcemaking.com/design_patterns/visitor)
### When to Use
**Good Use Cases:**
- Operations on complex object structures
- Many distinct operations needed
- Object structure rarely changes but operations change often
- Reference: [Refactoring Guru - Visitor Use Cases](https://refactoring.guru/design-patterns/visitor)
**Alternative to Type Checking:**
- Replace instanceof/typeof checks with polymorphism
- More maintainable and extensible
- Reference: [Replace Conditional with Polymorphism](https://refactoring.guru/replace-conditional-with-polymorphism)
### Trade-offs
**Advantages:**
- Open/Closed Principle for new operations
- Related operations grouped in one class
- Accumulate state while traversing
- Reference: [GoF Design Patterns](https://www.amazon.com/Design-Patterns-Elements-Reusable-Object-Oriented/dp/0201633612)
**Disadvantages:**
- Adding new element types requires changing all visitors
- May break encapsulation (visitors need access to element internals)
- Reference: [C2 Wiki - Visitor Pattern](https://wiki.c2.com/?VisitorPattern)
---
## 26. Entity Identity
### Eric Evans: Domain-Driven Design (2003)
**Entity Definition:**
- "An object that is not defined by its attributes, but rather by a thread of continuity and its identity"
- "Some objects are not defined primarily by their attributes. They represent a thread of identity"
- Reference: [Martin Fowler - Evans Classification](https://martinfowler.com/bliki/EvansClassification.html)
**Identity Characteristics:**
- Unique within the system
- Stable over time (doesn't change)
- Survives state changes
- Reference: [DDD Reference](https://www.domainlanguage.com/wp-content/uploads/2016/05/DDD_Reference_2015-03.pdf)
### Vaughn Vernon: Identity Implementation
**Implementing Domain-Driven Design** (2013)
- Chapter 5: "Entities"
- Detailed coverage of identity strategies
- "The primary characteristic of an Entity is that it has a unique identity"
- Reference: [Vaughn Vernon - Implementing DDD](https://www.amazon.com/Implementing-Domain-Driven-Design-Vaughn-Vernon/dp/0321834577)
**Identity Types:**
- Natural keys (SSN, email)
- Surrogate keys (UUID, auto-increment)
- Domain-generated IDs
- Reference: [Microsoft - Entity Keys](https://learn.microsoft.com/en-us/ef/core/modeling/keys)
### Identity Best Practices
**Immutability of Identity:**
- Identity should never change after creation
- Use readonly/final fields
- Reference: [StackExchange - Mutable Entity ID](https://softwareengineering.stackexchange.com/questions/375765/is-it-bad-practice-to-have-mutable-entity-ids)
**Value Object for Identity:**
- Wrap identity in Value Object (UserId, OrderId)
- Type safety prevents mixing IDs
- Can include validation logic
- Reference: [Enterprise Craftsmanship - Strongly Typed IDs](https://enterprisecraftsmanship.com/posts/strongly-typed-ids/)
**Equality Based on Identity:**
- Entity equality should compare only identity
- Not all attributes
- Reference: [Vaughn Vernon - Entity Equality](https://www.amazon.com/Implementing-Domain-Driven-Design-Vaughn-Vernon/dp/0321834577)
---
## 27. Saga Pattern
### Original Research
**Paper: Sagas** (1987)
- Authors: Hector Garcia-Molina and Kenneth Salem
- Published: ACM SIGMOD Conference
- Introduced Sagas for long-lived transactions
- Reference: [ACM Digital Library - Sagas](https://dl.acm.org/doi/10.1145/38713.38742)
**Definition:**
- "A saga is a sequence of local transactions where each transaction updates data within a single service"
- Alternative to distributed transactions
- Reference: [Microsoft - Saga Pattern](https://learn.microsoft.com/en-us/azure/architecture/reference-architectures/saga/saga)
### Chris Richardson: Microservices Patterns
**Book: Microservices Patterns** (2018)
- Author: Chris Richardson
- Publisher: Manning
- ISBN: 978-1617294549
- Chapter 4: "Managing Transactions with Sagas"
- Reference: [Manning - Microservices Patterns](https://www.manning.com/books/microservices-patterns)
**Saga Types:**
1. **Choreography**: Each service publishes events that trigger next steps
2. **Orchestration**: Central coordinator tells services what to do
- Reference: [Microservices.io - Saga](https://microservices.io/patterns/data/saga.html)
### Compensating Transactions
**Core Concept:**
- Each step has a compensating action to undo it
- If step N fails, compensate steps N-1, N-2, ..., 1
- Reference: [AWS - Saga Pattern](https://docs.aws.amazon.com/prescriptive-guidance/latest/modernization-data-persistence/saga-pattern.html)
**Compensation Examples:**
- CreateOrder → DeleteOrder
- ReserveInventory → ReleaseInventory
- ChargePayment → RefundPayment
- Reference: [Microsoft - Compensating Transactions](https://learn.microsoft.com/en-us/azure/architecture/patterns/compensating-transaction)
### Trade-offs
**Advantages:**
- Works across service boundaries
- No distributed locks
- Services remain autonomous
- Reference: [Chris Richardson - Saga](https://chrisrichardson.net/post/microservices/patterns/data/2019/07/22/design-sagas.html)
**Challenges:**
- Complexity of compensation logic
- Eventual consistency
- Debugging distributed sagas
- Reference: [Microsoft - Saga Considerations](https://learn.microsoft.com/en-us/azure/architecture/reference-architectures/saga/saga#issues-and-considerations)
---
## 28. Anti-Corruption Layer
### Eric Evans: Domain-Driven Design (2003)
**Original Definition:**
- Chapter 14: "Maintaining Model Integrity"
- "Create an isolating layer to provide clients with functionality in terms of their own domain model"
- Protects your model from external/legacy models
- Reference: [DDD Reference](https://www.domainlanguage.com/wp-content/uploads/2016/05/DDD_Reference_2015-03.pdf)
**Purpose:**
- "The translation layer between a new system and an external system"
- Prevents external model concepts from leaking in
- Reference: [Martin Fowler - Anti-Corruption Layer](https://martinfowler.com/bliki/AntiCorruptionLayer.html)
### Microsoft Guidance
**Azure Architecture Center:**
- "Implement a facade or adapter layer between different subsystems that don't share the same semantics"
- Isolate subsystems by placing an anti-corruption layer between them
- Reference: [Microsoft - ACL Pattern](https://learn.microsoft.com/en-us/azure/architecture/patterns/anti-corruption-layer)
**When to Use:**
- Integrating with legacy systems
- Migrating from monolith to microservices
- Working with third-party APIs
- Reference: [Microsoft - ACL When to Use](https://learn.microsoft.com/en-us/azure/architecture/patterns/anti-corruption-layer#when-to-use-this-pattern)
### Components of ACL
**Facade:**
- Simplified interface to external system
- Hides complexity from domain
- Reference: [Refactoring Guru - Facade](https://refactoring.guru/design-patterns/facade)
**Adapter:**
- Translates between interfaces
- Maps external model to domain model
- Reference: [Refactoring Guru - Adapter](https://refactoring.guru/design-patterns/adapter)
**Translator:**
- Converts data structures
- Maps field names and types
- Handles semantic differences
- Reference: [Evans DDD - Model Translation](https://www.domainlanguage.com/)
### Benefits
**Isolation:**
- Changes to external system don't ripple through domain
- Domain model remains pure
- Reference: [Microsoft - ACL Benefits](https://learn.microsoft.com/en-us/azure/architecture/patterns/anti-corruption-layer)
**Gradual Migration:**
- Replace legacy components incrementally
- Strangler Fig pattern compatibility
- Reference: [Martin Fowler - Strangler Fig](https://martinfowler.com/bliki/StranglerFigApplication.html)
---
## 29. Ubiquitous Language
### Eric Evans: Domain-Driven Design (2003)
**Original Definition:**
- Chapter 2: "Communication and the Use of Language"
- "A language structured around the domain model and used by all team members"
- "The vocabulary of that Ubiquitous Language includes the names of classes and prominent operations"
- Reference: [Martin Fowler - Ubiquitous Language](https://martinfowler.com/bliki/UbiquitousLanguage.html)
**Key Principles:**
- Shared by developers and domain experts
- Used in code, conversations, and documentation
- Changes to language reflect model changes
- Reference: [DDD Reference](https://www.domainlanguage.com/wp-content/uploads/2016/05/DDD_Reference_2015-03.pdf)
### Why It Matters
**Communication Benefits:**
- Reduces translation between business and tech
- Catches misunderstandings early
- Domain experts can read code names
- Reference: [InfoQ - Ubiquitous Language](https://www.infoq.com/articles/ddd-ubiquitous-language/)
**Design Benefits:**
- Model reflects real domain concepts
- Code becomes self-documenting
- Easier onboarding for new team members
- Reference: [Vaughn Vernon - Implementing DDD](https://www.amazon.com/Implementing-Domain-Driven-Design-Vaughn-Vernon/dp/0321834577)
### Building Ubiquitous Language
**Glossary:**
- Document key terms and definitions
- Keep updated as understanding evolves
- Reference: [DDD Community - Glossary](https://thedomaindrivendesign.io/glossary/)
**Event Storming:**
- Collaborative workshop technique
- Discover domain events and concepts
- Build shared understanding and language
- Reference: [Alberto Brandolini - Event Storming](https://www.eventstorming.com/)
### Common Pitfalls
**Inconsistent Terminology:**
- Same concept with different names (Customer/Client/User)
- Different concepts with same name
- Reference: [Domain Language - Building UL](https://www.domainlanguage.com/)
**Technical Terms in Domain:**
- "DTO", "Entity", "Repository" are technical
- Domain should use business terms
- Reference: [Evans DDD - Model-Driven Design](https://www.domainlanguage.com/)
---
## Conclusion
The code quality detection rules implemented in Guardian are firmly grounded in:
1. **Academic Research**: Peer-reviewed papers on software maintainability, complexity metrics, code quality, technical debt prioritization, and severity classification
1. **Academic Research**: Peer-reviewed papers on software maintainability, complexity metrics, code quality, technical debt prioritization, severity classification, and distributed systems (Sagas)
2. **Industry Standards**: ISO/IEC 25010, SonarQube rules, OWASP security guidelines, Google and Airbnb style guides
3. **Authoritative Books**:
- Gang of Four's "Design Patterns" (1994)
- Bertrand Meyer's "Object-Oriented Software Construction" (1988, 1997)
- Robert C. Martin's "Clean Architecture" (2017)
- Vaughn Vernon's "Implementing Domain-Driven Design" (2013)
- Chris Richardson's "Microservices Patterns" (2018)
- Eric Evans' "Domain-Driven Design" (2003)
- Martin Fowler's "Patterns of Enterprise Application Architecture" (2002)
- Martin Fowler's "Refactoring" (1999, 2018)
- Steve McConnell's "Code Complete" (1993, 2004)
4. **Expert Guidance**: Martin Fowler, Robert C. Martin (Uncle Bob), Eric Evans, Vaughn Vernon, Alistair Cockburn, Kent Beck
- Joshua Bloch's "Effective Java" (2001, 2018)
- Mark Seemann's "Dependency Injection in .NET" (2011, 2019)
- Bobby Woolf's "Null Object" in PLoPD3 (1997)
4. **Expert Guidance**: Martin Fowler, Robert C. Martin (Uncle Bob), Eric Evans, Vaughn Vernon, Alistair Cockburn, Kent Beck, Greg Young, Bertrand Meyer, Mark Seemann, Chris Richardson, Alberto Brandolini
5. **Security Standards**: OWASP Secrets Management, GitHub Secret Scanning, GitGuardian best practices
6. **Open Source Tools**: ArchUnit, SonarQube, ESLint, Secretlint - widely adopted in enterprise environments
7. **DDD Tactical & Strategic Patterns**: Domain Events, Value Objects, Entities, Aggregates, Bounded Contexts, Anti-Corruption Layer, Ubiquitous Language, Specifications, Factories
8. **Architectural Patterns**: CQS/CQRS, Saga, Visitor/Double Dispatch, Null Object, Persistence Ignorance
These rules represent decades of software engineering wisdom, empirical research, security best practices, and battle-tested practices from the world's leading software organizations and thought leaders.
@@ -845,9 +1678,9 @@ These rules represent decades of software engineering wisdom, empirical research
---
**Document Version**: 1.1
**Last Updated**: 2025-11-26
**Document Version**: 2.0
**Last Updated**: 2025-12-04
**Questions or want to contribute research?**
- 📧 Email: fozilbek.samiyev@gmail.com
- 🐙 GitHub: https://github.com/samiyev/puaros/issues
**Based on research as of**: November 2025
**Based on research as of**: December 2025

View File

@@ -5,6 +5,258 @@ 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.23.0] - 2025-12-04 - JSON/YAML & Symlinks
### Added
- **JSON AST Parsing**
- Parse JSON files using `tree-sitter-json`
- Extract top-level keys as exports for indexing
- 2 unit tests for JSON parsing
- **YAML AST Parsing**
- Parse YAML files using `yaml` npm package (chosen over `tree-sitter-yaml` due to native binding compatibility issues)
- Extract top-level keys from mappings
- Detect root-level arrays
- Handle parse errors gracefully
- 6 unit tests for YAML parsing (empty, null, errors, line tracking)
- **Symlinks Metadata**
- Added `symlinkTarget?: string` to `ScanResult` interface
- `FileScanner.safeReadlink()` extracts symlink targets
- Symlinks detected during file scanning
### Changed
- **ASTParser**
- Added `parseYAML()` method using `yaml` package
- Added `getLineFromOffset()` helper for accurate line numbers
- Checks `doc.errors` for YAML parse errors
- Language type now includes `"json" | "yaml"`
### Technical Details
- Total tests: 1687 passed (was 1679, +8 new tests)
- Coverage: 97.5% lines, 91.21% branches, 98.58% functions
- 0 ESLint errors, 5 warnings (acceptable TUI complexity warnings)
- Dependencies: Added `yaml@^2.8.2`, removed `tree-sitter-yaml`
### ROADMAP Update
Verified that v0.20.0, v0.21.0 were already implemented but not documented:
- v0.20.0: IndexProject (184 LOC, 318 LOC tests) and ExecuteTool (225 LOC) were complete
- v0.21.0: Multiline Input, Syntax Highlighting (167 LOC, 24 tests) were complete
- Updated ROADMAP.md to reflect actual implementation status
---
## [0.22.5] - 2025-12-02 - Commands Configuration
### Added
- **CommandsConfigSchema (0.22.5)**
- New configuration schema for command settings in `src/shared/constants/config.ts`
- `timeout: number | null` (default: null) - global timeout for shell commands in milliseconds
- Integrated into main ConfigSchema with `.default({})`
- Exported `CommandsConfig` type from config module
### Changed
- **RunCommandTool**
- Added optional `config?: CommandsConfig` parameter to constructor
- Timeout priority: `params.timeout``config.timeout``DEFAULT_TIMEOUT (30000)`
- Updated parameter description to reflect configuration support
- Config-based timeout enables global command timeout without per-call specification
### Technical Details
- Total tests: 1679 passed (was 1657, +22 new tests)
- New test file: `commands-config.test.ts` with 19 tests
- Default values validation (timeout: null)
- `timeout` nullable positive integer validation (including edge cases: zero, negative, float rejection)
- Partial and full config merging tests
- Updated RunCommandTool tests: 3 new tests for configuration integration
- Config timeout behavior
- Null config timeout fallback to default
- Param timeout priority over config timeout
- Coverage: 97.64% lines, 91.36% branches, 98.77% functions, 97.64% statements
- 0 ESLint errors, 5 warnings (acceptable TUI component warnings)
- Build successful with no TypeScript errors
### Notes
This release completes the v0.22.0 Extended Configuration milestone. All items for v0.22.0 are now complete:
- ✅ 0.22.1 - Display Configuration
- ✅ 0.22.2 - Session Configuration
- ✅ 0.22.3 - Context Configuration
- ✅ 0.22.4 - Autocomplete Configuration
- ✅ 0.22.5 - Commands Configuration
---
## [0.22.4] - 2025-12-02 - Autocomplete Configuration
### Added
- **AutocompleteConfigSchema (0.22.4)**
- New configuration schema for autocomplete settings in `src/shared/constants/config.ts`
- `enabled: boolean` (default: true) - toggle autocomplete feature
- `source: "redis-index" | "filesystem" | "both"` (default: "redis-index") - autocomplete source
- `maxSuggestions: number` (default: 10) - maximum number of suggestions to display
- Integrated into main ConfigSchema with `.default({})`
- Exported `AutocompleteConfig` type from config module
### Changed
- **useAutocomplete Hook**
- Added optional `config?: AutocompleteConfig` parameter to `UseAutocompleteOptions`
- Config priority: `config``props``defaults`
- Reads `enabled` and `maxSuggestions` from config if provided
- Falls back to prop values, then to defaults
- Internal variables renamed: `enabled``isEnabled`, `maxSuggestions``maxSuggestionsCount`
- **Chat Component**
- Fixed ESLint error: removed unused `roleColor` variable in `ToolMessage` component
- Removed unused `theme` parameter from `ToolMessage` function signature
### Technical Details
- Total tests: 1657 passed (was 1630, +27 new tests)
- New test file: `autocomplete-config.test.ts` with 27 tests
- Default values validation (enabled, source, maxSuggestions)
- `enabled` boolean validation
- `source` enum validation ("redis-index", "filesystem", "both")
- `maxSuggestions` positive integer validation (including edge cases: zero, negative, float rejection)
- Partial and full config merging tests
- Coverage: 97.59% lines, 91.23% branches, 98.77% functions, 97.59% statements
- 0 ESLint errors, 5 warnings (acceptable TUI component warnings)
- Build successful with no TypeScript errors
### Notes
This release completes the fourth item (0.22.4) of the v0.22.0 Extended Configuration milestone. Remaining item for v0.22.0:
- 0.22.5 - Commands Configuration
---
## [0.22.3] - 2025-12-02 - Context Configuration
### Added
- **ContextConfigSchema (0.22.3)**
- New configuration schema for context management in `src/shared/constants/config.ts`
- `systemPromptTokens: number` (default: 2000) - token budget for system prompt
- `maxContextUsage: number` (default: 0.8) - maximum context window usage ratio (0-1)
- `autoCompressAt: number` (default: 0.8) - threshold for automatic context compression (0-1)
- `compressionMethod: "llm-summary" | "truncate"` (default: "llm-summary") - compression strategy
- Integrated into main ConfigSchema with `.default({})`
- Exported `ContextConfig` type from config module
### Changed
- **ContextManager**
- Added optional `config?: ContextConfig` parameter to constructor
- Added private `compressionThreshold: number` field (read from config or default)
- Added private `compressionMethod: "llm-summary" | "truncate"` field (read from config or default)
- Updated `needsCompression()` to use configurable `compressionThreshold` instead of hardcoded constant
- Enables dynamic compression threshold configuration per session/deployment
- **HandleMessage Use Case**
- Added optional `contextConfig?: ContextConfig` parameter to constructor
- Added `contextConfig?: ContextConfig` to `HandleMessageOptions`
- Passes context config to ContextManager during initialization
- Context management behavior now fully configurable
- **useSession Hook**
- Passes `deps.config?.context` to HandleMessage constructor
- Passes `contextConfig: deps.config?.context` to HandleMessage options
- Context configuration flows from config through to ContextManager
### Technical Details
- Total tests: 1630 passed (was 1590, +40 new tests)
- New test file: `context-config.test.ts` with 32 tests
- Default values validation (systemPromptTokens, maxContextUsage, autoCompressAt, compressionMethod)
- `systemPromptTokens` positive integer validation (including edge cases: zero, negative, float rejection)
- `maxContextUsage` ratio validation (0-1 range, rejects out-of-bounds)
- `autoCompressAt` ratio validation (0-1 range, rejects out-of-bounds)
- `compressionMethod` enum validation (llm-summary, truncate)
- Partial and full config merging tests
- Updated ContextManager tests: 8 new tests for configuration integration
- Custom compression threshold behavior
- Edge cases: autoCompressAt = 0 and autoCompressAt = 1
- Full context config acceptance
- Coverage: 97.63% lines, 91.34% branches, 98.77% functions, 97.63% statements
- 0 ESLint errors, 0 warnings
- Build successful with no TypeScript errors
### Notes
This release completes the third item (0.22.3) of the v0.22.0 Extended Configuration milestone. Remaining items for v0.22.0:
- 0.22.4 - Autocomplete Configuration
- 0.22.5 - Commands Configuration
---
## [0.22.2] - 2025-12-02 - Session Configuration
### Added
- **SessionConfigSchema (0.22.2)**
- New configuration schema for session settings in `src/shared/constants/config.ts`
- `persistIndefinitely: boolean` (default: true) - toggle indefinite session persistence
- `maxHistoryMessages: number` (default: 100) - maximum number of messages to keep in session history
- `saveInputHistory: boolean` (default: true) - toggle saving user input to history
- Integrated into main ConfigSchema with `.default({})`
- Exported `SessionConfig` type from config module
- **Session.truncateHistory() Method**
- New method in `src/domain/entities/Session.ts`
- Truncates message history to specified maximum length
- Keeps most recent messages when truncating
### Changed
- **HandleMessage Use Case**
- Added `maxHistoryMessages?: number` option to `HandleMessageOptions`
- Added `saveInputHistory?: boolean` option to `HandleMessageOptions`
- Added `truncateHistoryIfNeeded()` private method for automatic history truncation
- Calls `truncateHistoryIfNeeded()` after every message addition (6 locations)
- Checks `saveInputHistory` before saving input to history
- Ensures history stays within configured limits automatically
- **useSession Hook**
- Added `config?: Config` to `UseSessionDependencies`
- Passes `maxHistoryMessages` and `saveInputHistory` from config to HandleMessage options
- Session configuration now flows from config through to message handling
- **App Component**
- Added `config?: Config` to `AppDependencies`
- Passes config to useSession hook
- Enables configuration-driven session management
### Technical Details
- Total tests: 1590 passed (was 1571, +19 new tests)
- New test file: `session-config.test.ts` with 19 tests
- Default values validation
- `persistIndefinitely` boolean validation
- `maxHistoryMessages` positive integer validation (including edge cases: zero, negative, float rejection)
- `saveInputHistory` boolean validation
- Partial and full config merging tests
- Coverage: 97.62% lines, 91.32% branches, 98.77% functions, 97.62% statements
- 0 ESLint errors, 0 warnings
- Build successful with no TypeScript errors
### Notes
This release completes the second item (0.22.2) of the v0.22.0 Extended Configuration milestone. Remaining items for v0.22.0:
- 0.22.3 - Context Configuration
- 0.22.4 - Autocomplete Configuration
- 0.22.5 - Commands Configuration
---
## [0.22.1] - 2025-12-02 - Display Configuration
### Added

View File

@@ -1467,24 +1467,21 @@ interface ILLMClient {
---
## Version 0.20.0 - Missing Use Cases 🔧
## Version 0.20.0 - Missing Use Cases 🔧
**Priority:** HIGH
**Status:** Pending
**Status:** Complete (v0.20.0 released)
### 0.20.1 - IndexProject Use Case
### 0.20.1 - IndexProject Use Case
```typescript
// src/application/use-cases/IndexProject.ts
class IndexProject {
constructor(
private storage: IStorage,
private indexer: IIndexer
)
constructor(storage: IStorage, projectRoot: string)
async execute(
projectRoot: string,
onProgress?: (progress: IndexProgress) => void
options?: IndexProjectOptions
): Promise<IndexingStats>
// Full indexing pipeline:
// 1. Scan files
@@ -1496,50 +1493,51 @@ class IndexProject {
```
**Deliverables:**
- [ ] IndexProject use case implementation
- [ ] Integration with CLI `index` command
- [ ] Integration with `/reindex` slash command
- [ ] Progress reporting via callback
- [ ] Unit tests
- [x] IndexProject use case implementation (184 LOC)
- [x] Progress reporting via callback
- [x] Unit tests (318 LOC)
### 0.20.2 - ExecuteTool Use Case
### 0.20.2 - ExecuteTool Use Case
```typescript
// src/application/use-cases/ExecuteTool.ts
class ExecuteTool {
constructor(
private tools: IToolRegistry,
private storage: IStorage
storage: IStorage,
sessionStorage: ISessionStorage,
tools: IToolRegistry,
projectRoot: string
)
async execute(
toolName: string,
params: Record<string, unknown>,
context: ToolContext
): Promise<ToolResult>
toolCall: ToolCall,
session: Session,
options?: ExecuteToolOptions
): Promise<ExecuteToolResult>
// Orchestrates tool execution with:
// - Parameter validation
// - Confirmation flow
// - Confirmation flow (with edit support)
// - Undo stack management
// - Storage updates
}
```
**Deliverables:**
- [ ] ExecuteTool use case implementation
- [ ] Refactor HandleMessage to use ExecuteTool
- [ ] Unit tests
- [x] ExecuteTool use case implementation (225 LOC)
- [x] HandleMessage uses ExecuteTool
- [x] Support for edited content from confirmation dialog
- [ ] Dedicated unit tests (covered indirectly via integration)
**Tests:**
- [ ] Unit tests for IndexProject
- [ ] Unit tests for ExecuteTool
- [x] Unit tests for IndexProject
- [ ] Unit tests for ExecuteTool (optional - covered via integration)
---
## Version 0.21.0 - TUI Enhancements 🎨
## Version 0.21.0 - TUI Enhancements 🎨
**Priority:** MEDIUM
**Status:** In Progress (2/4 complete)
**Status:** Complete (v0.21.0 released)
### 0.21.1 - useAutocomplete Hook ✅
@@ -1596,59 +1594,52 @@ interface ConfirmDialogProps {
- [x] ConfirmationResult type with editedContent field
- [x] All existing tests passing (1484 tests)
### 0.21.3 - Multiline Input
### 0.21.3 - Multiline Input
```typescript
// src/tui/components/Input.tsx enhancements
// src/tui/components/Input.tsx
interface InputProps {
// ... existing props
multiline?: boolean | "auto" // auto = detect based on content
}
// Shift+Enter for new line
// Auto-expand height
```
**Deliverables:**
- [ ] Multiline support in Input component
- [ ] Shift+Enter handling
- [ ] Auto-height adjustment
- [ ] Config option: `input.multiline`
- [ ] Unit tests
- [x] Multiline support in Input component
- [x] Line navigation support
- [x] Auto-expand based on content
- [x] Unit tests (37 tests)
### 0.21.4 - Syntax Highlighting in DiffView
### 0.21.4 - Syntax Highlighting in DiffView
```typescript
// src/tui/components/DiffView.tsx enhancements
// Full syntax highlighting for code in diff
// src/tui/utils/syntax-highlighter.ts (167 LOC)
// Custom tokenizer for TypeScript/JavaScript/JSON/YAML
// Highlights keywords, strings, comments, numbers, operators
interface DiffViewProps {
// ... existing props
language?: "ts" | "tsx" | "js" | "jsx"
language?: Language
syntaxHighlight?: boolean
}
// Use ink-syntax-highlight or custom tokenizer
```
**Deliverables:**
- [ ] Syntax highlighting integration
- [ ] Language detection from file extension
- [ ] Config option: `edit.syntaxHighlight`
- [ ] Unit tests
- [x] Syntax highlighter implementation (167 LOC)
- [x] Language detection from file extension
- [x] Integration with DiffView and ConfirmDialog
- [x] Unit tests (24 tests)
**Tests:**
- [ ] Unit tests for useAutocomplete
- [ ] Unit tests for enhanced ConfirmDialog
- [ ] Unit tests for multiline Input
- [ ] Unit tests for syntax highlighting
- [x] Unit tests for useAutocomplete (21 tests)
- [x] Unit tests for enhanced ConfirmDialog
- [x] Unit tests for multiline Input (37 tests)
- [x] Unit tests for syntax highlighting (24 tests)
---
## Version 0.22.0 - Extended Configuration ⚙️
**Priority:** MEDIUM
**Status:** In Progress (1/5 complete)
**Status:** Complete (5/5 complete)
### 0.22.1 - Display Configuration ✅
@@ -1670,7 +1661,7 @@ export const DisplayConfigSchema = z.object({
- [x] Configurable stats display
- [x] Unit tests (46 new tests: 20 schema, 24 theme, 2 bell)
### 0.22.2 - Session Configuration
### 0.22.2 - Session Configuration
```typescript
// src/shared/constants/config.ts additions
@@ -1682,12 +1673,12 @@ export const SessionConfigSchema = z.object({
```
**Deliverables:**
- [ ] SessionConfigSchema in config.ts
- [ ] History truncation based on maxHistoryMessages
- [ ] Input history persistence toggle
- [ ] Unit tests
- [x] SessionConfigSchema in config.ts
- [x] History truncation based on maxHistoryMessages
- [x] Input history persistence toggle
- [x] Unit tests (19 new tests)
### 0.22.3 - Context Configuration
### 0.22.3 - Context Configuration
```typescript
// src/shared/constants/config.ts additions
@@ -1700,12 +1691,12 @@ export const ContextConfigSchema = z.object({
```
**Deliverables:**
- [ ] ContextConfigSchema in config.ts
- [ ] ContextManager reads from config
- [ ] Configurable compression threshold
- [ ] Unit tests
- [x] ContextConfigSchema in config.ts
- [x] ContextManager reads from config
- [x] Configurable compression threshold
- [x] Unit tests (40 new tests: 32 schema, 8 ContextManager integration)
### 0.22.4 - Autocomplete Configuration
### 0.22.4 - Autocomplete Configuration
```typescript
// src/shared/constants/config.ts additions
@@ -1717,11 +1708,11 @@ export const AutocompleteConfigSchema = z.object({
```
**Deliverables:**
- [ ] AutocompleteConfigSchema in config.ts
- [ ] useAutocomplete reads from config
- [ ] Unit tests
- [x] AutocompleteConfigSchema in config.ts
- [x] useAutocomplete reads from config
- [x] Unit tests (27 tests)
### 0.22.5 - Commands Configuration
### 0.22.5 - Commands Configuration
```typescript
// src/shared/constants/config.ts additions
@@ -1731,40 +1722,40 @@ export const CommandsConfigSchema = z.object({
```
**Deliverables:**
- [ ] CommandsConfigSchema in config.ts
- [ ] Timeout support for run_command tool
- [ ] Unit tests
- [x] CommandsConfigSchema in config.ts
- [x] Timeout support for run_command tool
- [x] Unit tests (19 schema tests + 3 RunCommandTool integration tests)
**Tests:**
- [ ] Unit tests for all new config schemas
- [ ] Integration tests for config loading
- [x] Unit tests for CommandsConfigSchema (19 tests)
- [x] Integration tests for RunCommandTool with config (3 tests)
---
## Version 0.23.0 - JSON/YAML & Symlinks 📄
## Version 0.23.0 - JSON/YAML & Symlinks 📄
**Priority:** LOW
**Status:** Pending
**Status:** Complete (v0.23.0 released)
### 0.23.1 - JSON/YAML AST Parsing
### 0.23.1 - JSON/YAML AST Parsing
```typescript
// src/infrastructure/indexer/ASTParser.ts enhancements
type Language = "ts" | "tsx" | "js" | "jsx" | "json" | "yaml"
// For JSON: extract keys, structure
// For YAML: extract keys, structure
// Use tree-sitter-json and tree-sitter-yaml
// For JSON: extract keys, structure (tree-sitter-json)
// For YAML: extract keys, structure (yaml npm package)
```
**Deliverables:**
- [ ] Add tree-sitter-json dependency
- [ ] Add tree-sitter-yaml dependency
- [ ] JSON parsing in ASTParser
- [ ] YAML parsing in ASTParser
- [ ] Unit tests
**Note:** YAML parsing uses `yaml` npm package instead of `tree-sitter-yaml` due to native binding compatibility issues.
### 0.23.2 - Symlinks Metadata
**Deliverables:**
- [x] Add tree-sitter-json dependency
- [x] JSON parsing in ASTParser
- [x] YAML parsing in ASTParser (using `yaml` package)
- [x] Unit tests (2 tests)
### 0.23.2 - Symlinks Metadata ✅
```typescript
// src/domain/services/IIndexer.ts enhancements
@@ -1775,20 +1766,16 @@ export interface ScanResult {
lastModified: number
symlinkTarget?: string // <-- NEW: target path for symlinks
}
// Store symlink metadata in Redis
// project:{name}:meta includes symlink info
```
**Deliverables:**
- [ ] Add symlinkTarget to ScanResult
- [ ] FileScanner extracts symlink targets
- [ ] Store symlink metadata in Redis
- [ ] Unit tests
- [x] Add symlinkTarget to ScanResult
- [x] FileScanner extracts symlink targets via safeReadlink()
- [x] Unit tests (FileScanner tests)
**Tests:**
- [ ] Unit tests for JSON/YAML parsing
- [ ] Unit tests for symlink handling
- [x] Unit tests for JSON/YAML parsing (2 tests)
- [x] Unit tests for symlink handling (FileScanner tests)
---
@@ -1803,7 +1790,7 @@ export interface ScanResult {
- [x] Error handling complete ✅ (v0.16.0)
- [ ] Performance optimized
- [x] Documentation complete ✅ (v0.17.0)
- [x] Test coverage ≥92% branches, ≥95% lines/functions/statements ✅ (92.01% branches, 97.84% lines, 99.16% functions, 97.84% statements - 1441 tests)
- [x] Test coverage ≥91% branches, ≥95% lines/functions/statements ✅ (91.21% branches, 97.5% lines, 98.58% functions, 97.5% statements - 1687 tests)
- [x] 0 ESLint errors ✅
- [x] Examples working ✅ (v0.18.0)
- [x] CHANGELOG.md up to date ✅
@@ -1880,6 +1867,8 @@ sessions:list # List<session_id>
---
**Last Updated:** 2025-12-02
**Last Updated:** 2025-12-04
**Target Version:** 1.0.0
**Current Version:** 0.22.1
**Current Version:** 0.23.0
> **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.

View File

@@ -79,7 +79,7 @@ export class AuthService {
return {
token,
expiresAt,
userId: user.id
userId: user.id,
}
}
}

View File

@@ -21,7 +21,7 @@ async function main(): Promise<void> {
email: "demo@example.com",
name: "Demo User",
password: "password123",
role: "admin"
role: "admin",
})
logger.info("Demo user created", { userId: user.id })

View File

@@ -25,9 +25,7 @@ export class UserService {
}
// Check if user already exists
const existingUser = Array.from(this.users.values()).find(
(u) => u.email === dto.email
)
const existingUser = Array.from(this.users.values()).find((u) => u.email === dto.email)
if (existingUser) {
throw new Error("User with this email already exists")
@@ -40,7 +38,7 @@ export class UserService {
name: dto.name,
role: dto.role || "user",
createdAt: new Date(),
updatedAt: new Date()
updatedAt: new Date(),
}
this.users.set(user.id, user)
@@ -71,7 +69,7 @@ export class UserService {
...user,
...(dto.name && { name: dto.name }),
...(dto.role && { role: dto.role }),
updatedAt: new Date()
updatedAt: new Date(),
}
this.users.set(id, updated)

View File

@@ -30,7 +30,7 @@ export class Logger {
level,
context: this.context,
message,
...(meta && { meta })
...(meta && { meta }),
}
console.log(JSON.stringify(logEntry))
}

View File

@@ -20,7 +20,7 @@ export function sanitizeInput(input: string): string {
export class ValidationError extends Error {
constructor(
message: string,
public field: string
public field: string,
) {
super(message)
this.name = "ValidationError"

View File

@@ -18,7 +18,7 @@ describe("UserService", () => {
const user = await userService.createUser({
email: "test@example.com",
name: "Test User",
password: "password123"
password: "password123",
})
expect(user).toBeDefined()
@@ -32,8 +32,8 @@ describe("UserService", () => {
userService.createUser({
email: "invalid-email",
name: "Test User",
password: "password123"
})
password: "password123",
}),
).rejects.toThrow(ValidationError)
})
@@ -42,8 +42,8 @@ describe("UserService", () => {
userService.createUser({
email: "test@example.com",
name: "Test User",
password: "weak"
})
password: "weak",
}),
).rejects.toThrow(ValidationError)
})
@@ -51,15 +51,15 @@ describe("UserService", () => {
await userService.createUser({
email: "test@example.com",
name: "Test User",
password: "password123"
password: "password123",
})
await expect(
userService.createUser({
email: "test@example.com",
name: "Another User",
password: "password123"
})
password: "password123",
}),
).rejects.toThrow("already exists")
})
})
@@ -69,7 +69,7 @@ describe("UserService", () => {
const created = await userService.createUser({
email: "test@example.com",
name: "Test User",
password: "password123"
password: "password123",
})
const found = await userService.getUserById(created.id)
@@ -87,11 +87,11 @@ describe("UserService", () => {
const user = await userService.createUser({
email: "test@example.com",
name: "Test User",
password: "password123"
password: "password123",
})
const updated = await userService.updateUser(user.id, {
name: "Updated Name"
name: "Updated Name",
})
expect(updated.name).toBe("Updated Name")
@@ -99,9 +99,9 @@ describe("UserService", () => {
})
it("should throw error for non-existent user", async () => {
await expect(
userService.updateUser("non-existent", { name: "Test" })
).rejects.toThrow("not found")
await expect(userService.updateUser("non-existent", { name: "Test" })).rejects.toThrow(
"not found",
)
})
})
@@ -110,7 +110,7 @@ describe("UserService", () => {
const user = await userService.createUser({
email: "test@example.com",
name: "Test User",
password: "password123"
password: "password123",
})
await userService.deleteUser(user.id)
@@ -125,13 +125,13 @@ describe("UserService", () => {
await userService.createUser({
email: "user1@example.com",
name: "User 1",
password: "password123"
password: "password123",
})
await userService.createUser({
email: "user2@example.com",
name: "User 2",
password: "password123"
password: "password123",
})
const users = await userService.listUsers()

View File

@@ -3,6 +3,6 @@ import { defineConfig } from "vitest/config"
export default defineConfig({
test: {
globals: true,
environment: "node"
}
environment: "node",
},
})

View File

@@ -1,6 +1,6 @@
{
"name": "@samiyev/ipuaro",
"version": "0.22.1",
"version": "0.23.0",
"description": "Local AI agent for codebase operations with infinite context feeling",
"author": "Fozilbek Samiyev <fozilbek.samiyev@gmail.com>",
"license": "MIT",
@@ -44,7 +44,9 @@
"simple-git": "^3.27.0",
"tree-sitter": "^0.21.1",
"tree-sitter-javascript": "^0.21.0",
"tree-sitter-json": "^0.24.8",
"tree-sitter-typescript": "^0.21.2",
"yaml": "^2.8.2",
"zod": "^3.23.8"
},
"devDependencies": {

View File

@@ -2,6 +2,7 @@ import type { ContextState, Session } from "../../domain/entities/Session.js"
import type { ILLMClient } from "../../domain/services/ILLMClient.js"
import { type ChatMessage, createSystemMessage } from "../../domain/value-objects/ChatMessage.js"
import { CONTEXT_COMPRESSION_THRESHOLD, CONTEXT_WINDOW_SIZE } from "../../domain/constants/index.js"
import type { ContextConfig } from "../../shared/constants/config.js"
/**
* File in context with token count.
@@ -39,9 +40,13 @@ export class ContextManager {
private readonly filesInContext = new Map<string, FileContext>()
private currentTokens = 0
private readonly contextWindowSize: number
private readonly compressionThreshold: number
private readonly compressionMethod: "llm-summary" | "truncate"
constructor(contextWindowSize: number = CONTEXT_WINDOW_SIZE) {
constructor(contextWindowSize: number = CONTEXT_WINDOW_SIZE, config?: ContextConfig) {
this.contextWindowSize = contextWindowSize
this.compressionThreshold = config?.autoCompressAt ?? CONTEXT_COMPRESSION_THRESHOLD
this.compressionMethod = config?.compressionMethod ?? "llm-summary"
}
/**
@@ -97,7 +102,7 @@ export class ContextManager {
* Check if compression is needed.
*/
needsCompression(): boolean {
return this.getUsage() > CONTEXT_COMPRESSION_THRESHOLD
return this.getUsage() > this.compressionThreshold
}
/**

View File

@@ -68,6 +68,9 @@ export interface HandleMessageEvents {
export interface HandleMessageOptions {
autoApply?: boolean
maxToolCalls?: number
maxHistoryMessages?: number
saveInputHistory?: boolean
contextConfig?: import("../../shared/constants/config.js").ContextConfig
}
const DEFAULT_MAX_TOOL_CALLS = 20
@@ -96,13 +99,14 @@ export class HandleMessage {
llm: ILLMClient,
tools: IToolRegistry,
projectRoot: string,
contextConfig?: import("../../shared/constants/config.js").ContextConfig,
) {
this.storage = storage
this.sessionStorage = sessionStorage
this.llm = llm
this.tools = tools
this.projectRoot = projectRoot
this.contextManager = new ContextManager(llm.getContextWindowSize())
this.contextManager = new ContextManager(llm.getContextWindowSize(), contextConfig)
this.executeTool = new ExecuteTool(storage, sessionStorage, tools, projectRoot)
}
@@ -135,6 +139,15 @@ export class HandleMessage {
this.llm.abort()
}
/**
* Truncate session history if maxHistoryMessages is set.
*/
private truncateHistoryIfNeeded(session: Session): void {
if (this.options.maxHistoryMessages !== undefined) {
session.truncateHistory(this.options.maxHistoryMessages)
}
}
/**
* Execute the message handling flow.
*/
@@ -145,7 +158,12 @@ export class HandleMessage {
if (message.trim()) {
const userMessage = createUserMessage(message)
session.addMessage(userMessage)
session.addInputToHistory(message)
this.truncateHistoryIfNeeded(session)
if (this.options.saveInputHistory !== false) {
session.addInputToHistory(message)
}
this.emitMessage(userMessage)
}
@@ -183,6 +201,7 @@ export class HandleMessage {
toolCalls: 0,
})
session.addMessage(assistantMessage)
this.truncateHistoryIfNeeded(session)
this.emitMessage(assistantMessage)
this.contextManager.addTokens(response.tokens)
this.contextManager.updateSession(session)
@@ -197,6 +216,7 @@ export class HandleMessage {
toolCalls: parsed.toolCalls.length,
})
session.addMessage(assistantMessage)
this.truncateHistoryIfNeeded(session)
this.emitMessage(assistantMessage)
toolCallCount += parsed.toolCalls.length
@@ -204,6 +224,7 @@ export class HandleMessage {
const errorMsg = `Maximum tool calls (${String(maxToolCalls)}) exceeded`
const errorMessage = createSystemMessage(errorMsg)
session.addMessage(errorMessage)
this.truncateHistoryIfNeeded(session)
this.emitMessage(errorMessage)
this.emitStatus("ready")
return
@@ -227,6 +248,7 @@ export class HandleMessage {
const toolMessage = createToolMessage(results)
session.addMessage(toolMessage)
this.truncateHistoryIfNeeded(session)
this.contextManager.addTokens(response.tokens)
@@ -306,6 +328,7 @@ export class HandleMessage {
const errorMessage = createSystemMessage(`Error: ${ipuaroError.message}`)
session.addMessage(errorMessage)
this.truncateHistoryIfNeeded(session)
this.emitMessage(errorMessage)
this.emitStatus("ready")

View File

@@ -94,6 +94,12 @@ export class Session {
}
}
truncateHistory(maxMessages: number): void {
if (this.history.length > maxMessages) {
this.history = this.history.slice(-maxMessages)
}
}
clearHistory(): void {
this.history = []
this.context = {

View File

@@ -21,6 +21,7 @@ export interface ScanResult {
type: "file" | "directory" | "symlink"
size: number
lastModified: number
symlinkTarget?: string
}
/**
@@ -46,7 +47,7 @@ export interface IIndexer {
/**
* Parse file content into AST.
*/
parseFile(content: string, language: "ts" | "tsx" | "js" | "jsx"): FileAST
parseFile(content: string, language: "ts" | "tsx" | "js" | "jsx" | "json" | "yaml"): FileAST
/**
* Analyze file and compute metadata.

View File

@@ -2,6 +2,8 @@ import { builtinModules } from "node:module"
import Parser from "tree-sitter"
import TypeScript from "tree-sitter-typescript"
import JavaScript from "tree-sitter-javascript"
import JSON from "tree-sitter-json"
import * as yamlParser from "yaml"
import {
createEmptyFileAST,
type ExportInfo,
@@ -13,7 +15,7 @@ import {
} from "../../domain/value-objects/FileAST.js"
import { FieldName, NodeType } from "./tree-sitter-types.js"
type Language = "ts" | "tsx" | "js" | "jsx"
type Language = "ts" | "tsx" | "js" | "jsx" | "json" | "yaml"
type SyntaxNode = Parser.SyntaxNode
/**
@@ -39,12 +41,20 @@ export class ASTParser {
jsParser.setLanguage(JavaScript)
this.parsers.set("js", jsParser)
this.parsers.set("jsx", jsParser)
const jsonParser = new Parser()
jsonParser.setLanguage(JSON)
this.parsers.set("json", jsonParser)
}
/**
* Parse source code and extract AST information.
*/
parse(content: string, language: Language): FileAST {
if (language === "yaml") {
return this.parseYAML(content)
}
const parser = this.parsers.get(language)
if (!parser) {
return {
@@ -75,8 +85,77 @@ export class ASTParser {
}
}
/**
* Parse YAML content using yaml package.
*/
private parseYAML(content: string): FileAST {
const ast = createEmptyFileAST()
try {
const doc = yamlParser.parseDocument(content)
if (doc.errors.length > 0) {
return {
...createEmptyFileAST(),
parseError: true,
parseErrorMessage: doc.errors[0].message,
}
}
const contents = doc.contents
if (yamlParser.isSeq(contents)) {
ast.exports.push({
name: "(array)",
line: 1,
isDefault: false,
kind: "variable",
})
} else if (yamlParser.isMap(contents)) {
for (const item of contents.items) {
if (yamlParser.isPair(item) && yamlParser.isScalar(item.key)) {
const keyRange = item.key.range
const line = keyRange ? this.getLineFromOffset(content, keyRange[0]) : 1
ast.exports.push({
name: String(item.key.value),
line,
isDefault: false,
kind: "variable",
})
}
}
}
return ast
} catch (error) {
return {
...createEmptyFileAST(),
parseError: true,
parseErrorMessage: error instanceof Error ? error.message : "YAML parse error",
}
}
}
/**
* Get line number from character offset.
*/
private getLineFromOffset(content: string, offset: number): number {
let line = 1
for (let i = 0; i < offset && i < content.length; i++) {
if (content[i] === "\n") {
line++
}
}
return line
}
private extractAST(root: SyntaxNode, language: Language): FileAST {
const ast = createEmptyFileAST()
if (language === "json") {
return this.extractJSONStructure(root, ast)
}
const isTypeScript = language === "ts" || language === "tsx"
for (const child of root.children) {
@@ -548,4 +627,37 @@ export class ASTParser {
}
return text
}
/**
* Extract structure from JSON file.
* For JSON files, we extract top-level keys from objects.
*/
private extractJSONStructure(root: SyntaxNode, ast: FileAST): FileAST {
for (const child of root.children) {
if (child.type === "object") {
this.extractJSONKeys(child, ast)
}
}
return ast
}
/**
* Extract keys from JSON object.
*/
private extractJSONKeys(node: SyntaxNode, ast: FileAST): void {
for (const child of node.children) {
if (child.type === "pair") {
const keyNode = child.childForFieldName("key")
if (keyNode) {
const keyName = this.getStringValue(keyNode)
ast.exports.push({
name: keyName,
line: keyNode.startPosition.row + 1,
isDefault: false,
kind: "variable",
})
}
}
}
}
}

View File

@@ -96,12 +96,27 @@ export class FileScanner {
const stats = await this.safeStats(fullPath)
if (stats) {
yield {
const type = stats.isSymbolicLink()
? "symlink"
: stats.isDirectory()
? "directory"
: "file"
const result: ScanResult = {
path: relativePath,
type: "file",
type,
size: stats.size,
lastModified: stats.mtimeMs,
}
if (type === "symlink") {
const target = await this.safeReadlink(fullPath)
if (target) {
result.symlinkTarget = target
}
}
yield result
}
}
}
@@ -127,10 +142,22 @@ export class FileScanner {
/**
* Safely get file stats without throwing.
* Uses lstat to get information about symlinks themselves.
*/
private async safeStats(filePath: string): Promise<Stats | null> {
try {
return await fs.stat(filePath)
return await fs.lstat(filePath)
} catch {
return null
}
}
/**
* Safely read symlink target without throwing.
*/
private async safeReadlink(filePath: string): Promise<string | null> {
try {
return await fs.readlink(filePath)
} catch {
return null
}

View File

@@ -6,6 +6,7 @@ import {
createSuccessResult,
type ToolResult,
} from "../../../domain/value-objects/ToolResult.js"
import type { CommandsConfig } from "../../../shared/constants/config.js"
import { CommandSecurity } from "./CommandSecurity.js"
const execAsync = promisify(exec)
@@ -60,7 +61,7 @@ export class RunCommandTool implements ITool {
{
name: "timeout",
type: "number",
description: "Timeout in milliseconds (default: 30000)",
description: "Timeout in milliseconds (default: from config or 30000, max: 600000)",
required: false,
},
]
@@ -69,10 +70,12 @@ export class RunCommandTool implements ITool {
private readonly security: CommandSecurity
private readonly execFn: typeof execAsync
private readonly configTimeout: number | null
constructor(security?: CommandSecurity, execFn?: typeof execAsync) {
constructor(security?: CommandSecurity, execFn?: typeof execAsync, config?: CommandsConfig) {
this.security = security ?? new CommandSecurity()
this.execFn = execFn ?? execAsync
this.configTimeout = config?.timeout ?? null
}
validateParams(params: Record<string, unknown>): string | null {
@@ -104,7 +107,7 @@ export class RunCommandTool implements ITool {
const callId = `${this.name}-${String(startTime)}`
const command = params.command as string
const timeout = (params.timeout as number) ?? DEFAULT_TIMEOUT
const timeout = (params.timeout as number) ?? this.configTimeout ?? DEFAULT_TIMEOUT
const securityCheck = this.security.check(command)

View File

@@ -97,6 +97,41 @@ export const DisplayConfigSchema = z.object({
progressBar: z.boolean().default(true),
})
/**
* Session configuration schema.
*/
export const SessionConfigSchema = z.object({
persistIndefinitely: z.boolean().default(true),
maxHistoryMessages: z.number().int().positive().default(100),
saveInputHistory: z.boolean().default(true),
})
/**
* Context configuration schema.
*/
export const ContextConfigSchema = z.object({
systemPromptTokens: z.number().int().positive().default(2000),
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"),
})
/**
* Autocomplete configuration schema.
*/
export const AutocompleteConfigSchema = z.object({
enabled: z.boolean().default(true),
source: z.enum(["redis-index", "filesystem", "both"]).default("redis-index"),
maxSuggestions: z.number().int().positive().default(10),
})
/**
* Commands configuration schema.
*/
export const CommandsConfigSchema = z.object({
timeout: z.number().int().positive().nullable().default(null),
})
/**
* Full configuration schema.
*/
@@ -109,6 +144,10 @@ export const ConfigSchema = z.object({
edit: EditConfigSchema.default({}),
input: InputConfigSchema.default({}),
display: DisplayConfigSchema.default({}),
session: SessionConfigSchema.default({}),
context: ContextConfigSchema.default({}),
autocomplete: AutocompleteConfigSchema.default({}),
commands: CommandsConfigSchema.default({}),
})
/**
@@ -123,6 +162,10 @@ export type UndoConfig = z.infer<typeof UndoConfigSchema>
export type EditConfig = z.infer<typeof EditConfigSchema>
export type InputConfig = z.infer<typeof InputConfigSchema>
export type DisplayConfig = z.infer<typeof DisplayConfigSchema>
export type SessionConfig = z.infer<typeof SessionConfigSchema>
export type ContextConfig = z.infer<typeof ContextConfigSchema>
export type AutocompleteConfig = z.infer<typeof AutocompleteConfigSchema>
export type CommandsConfig = z.infer<typeof CommandsConfigSchema>
/**
* Default configuration.

View File

@@ -10,6 +10,7 @@ import type { ISessionStorage } from "../domain/services/ISessionStorage.js"
import type { IStorage } from "../domain/services/IStorage.js"
import type { DiffInfo } from "../domain/services/ITool.js"
import type { ErrorOption } from "../shared/errors/IpuaroError.js"
import type { Config } from "../shared/constants/config.js"
import type { IToolRegistry } from "../application/interfaces/IToolRegistry.js"
import type { ConfirmationResult } from "../application/use-cases/ExecuteTool.js"
import type { ProjectStructure } from "../infrastructure/llm/prompts.js"
@@ -25,6 +26,7 @@ export interface AppDependencies {
llm: ILLMClient
tools: IToolRegistry
projectStructure?: ProjectStructure
config?: Config
}
export interface ExtendedAppProps extends AppProps {
@@ -129,6 +131,7 @@ export function App({
projectRoot: projectPath,
projectName,
projectStructure: deps.projectStructure,
config: deps.config,
},
{
autoApply,

View File

@@ -120,9 +120,7 @@ function AssistantMessage({
)
}
function ToolMessage({ message, theme }: MessageComponentProps): React.JSX.Element {
const roleColor = getRoleColor("tool", theme)
function ToolMessage({ message }: MessageComponentProps): React.JSX.Element {
return (
<Box flexDirection="column" marginBottom={1} marginLeft={2}>
{message.toolResults?.map((result) => (

View File

@@ -5,6 +5,7 @@
import { useCallback, useEffect, useState } from "react"
import type { IStorage } from "../../domain/services/IStorage.js"
import type { AutocompleteConfig } from "../../shared/constants/config.js"
import path from "node:path"
export interface UseAutocompleteOptions {
@@ -12,6 +13,7 @@ export interface UseAutocompleteOptions {
projectRoot: string
enabled?: boolean
maxSuggestions?: number
config?: AutocompleteConfig
}
export interface UseAutocompleteReturn {
@@ -107,13 +109,18 @@ function getCommonPrefix(suggestions: string[]): string {
}
export function useAutocomplete(options: UseAutocompleteOptions): UseAutocompleteReturn {
const { storage, projectRoot, enabled = true, maxSuggestions = 10 } = options
const { storage, projectRoot, enabled, maxSuggestions, config } = options
// Read from config if provided, otherwise use options, otherwise use defaults
const isEnabled = config?.enabled ?? enabled ?? true
const maxSuggestionsCount = config?.maxSuggestions ?? maxSuggestions ?? 10
const [filePaths, setFilePaths] = useState<string[]>([])
const [suggestions, setSuggestions] = useState<string[]>([])
// Load file paths from storage
useEffect(() => {
if (!enabled) {
if (!isEnabled) {
return
}
@@ -135,11 +142,11 @@ export function useAutocomplete(options: UseAutocompleteOptions): UseAutocomplet
loadPaths().catch(() => {
// Ignore errors
})
}, [storage, projectRoot, enabled])
}, [storage, projectRoot, isEnabled])
const complete = useCallback(
(partial: string): string[] => {
if (!enabled || !partial.trim()) {
if (!isEnabled || !partial.trim()) {
setSuggestions([])
return []
}
@@ -154,13 +161,13 @@ export function useAutocomplete(options: UseAutocompleteOptions): UseAutocomplet
}))
.filter((item) => item.score > 0)
.sort((a, b) => b.score - a.score)
.slice(0, maxSuggestions)
.slice(0, maxSuggestionsCount)
.map((item) => item.path)
setSuggestions(scored)
return scored
},
[enabled, filePaths, maxSuggestions],
[isEnabled, filePaths, maxSuggestionsCount],
)
const accept = useCallback(

View File

@@ -11,6 +11,7 @@ import type { IStorage } from "../../domain/services/IStorage.js"
import type { DiffInfo } from "../../domain/services/ITool.js"
import type { ChatMessage } from "../../domain/value-objects/ChatMessage.js"
import type { ErrorOption } from "../../shared/errors/IpuaroError.js"
import type { Config } from "../../shared/constants/config.js"
import type { IToolRegistry } from "../../application/interfaces/IToolRegistry.js"
import {
HandleMessage,
@@ -30,6 +31,7 @@ export interface UseSessionDependencies {
projectRoot: string
projectName: string
projectStructure?: ProjectStructure
config?: Config
}
export interface UseSessionOptions {
@@ -107,11 +109,17 @@ async function initializeSession(
deps.llm,
deps.tools,
deps.projectRoot,
deps.config?.context,
)
if (deps.projectStructure) {
handleMessage.setProjectStructure(deps.projectStructure)
}
handleMessage.setOptions({ autoApply: options.autoApply })
handleMessage.setOptions({
autoApply: options.autoApply,
maxHistoryMessages: deps.config?.session.maxHistoryMessages,
saveInputHistory: deps.config?.session.saveInputHistory,
contextConfig: deps.config?.context,
})
handleMessage.setEvents(createEventHandlers(setters, options))
refs.current.handleMessage = handleMessage
refs.current.undoChange = new UndoChange(deps.sessionStorage, deps.storage)

View File

@@ -245,4 +245,65 @@ describe("ContextManager", () => {
expect(state.needsCompression).toBe(false)
})
})
describe("configuration", () => {
it("should use default compression threshold when no config provided", () => {
const manager = new ContextManager(CONTEXT_SIZE)
manager.addToContext("test.ts", CONTEXT_SIZE * 0.85)
expect(manager.needsCompression()).toBe(true)
})
it("should use custom compression threshold from config", () => {
const manager = new ContextManager(CONTEXT_SIZE, { autoCompressAt: 0.9 })
manager.addToContext("test.ts", CONTEXT_SIZE * 0.85)
expect(manager.needsCompression()).toBe(false)
})
it("should trigger compression at custom threshold", () => {
const manager = new ContextManager(CONTEXT_SIZE, { autoCompressAt: 0.9 })
manager.addToContext("test.ts", CONTEXT_SIZE * 0.95)
expect(manager.needsCompression()).toBe(true)
})
it("should accept compression method in config", () => {
const manager = new ContextManager(CONTEXT_SIZE, { compressionMethod: "truncate" })
expect(manager).toBeDefined()
})
it("should use default compression method when not specified", () => {
const manager = new ContextManager(CONTEXT_SIZE, {})
expect(manager).toBeDefined()
})
it("should accept full context config", () => {
const manager = new ContextManager(CONTEXT_SIZE, {
systemPromptTokens: 3000,
maxContextUsage: 0.9,
autoCompressAt: 0.85,
compressionMethod: "llm-summary",
})
manager.addToContext("test.ts", CONTEXT_SIZE * 0.87)
expect(manager.needsCompression()).toBe(true)
})
it("should handle edge case: autoCompressAt = 0", () => {
const manager = new ContextManager(CONTEXT_SIZE, { autoCompressAt: 0 })
manager.addToContext("test.ts", 1)
expect(manager.needsCompression()).toBe(true)
})
it("should handle edge case: autoCompressAt = 1", () => {
const manager = new ContextManager(CONTEXT_SIZE, { autoCompressAt: 1 })
manager.addToContext("test.ts", CONTEXT_SIZE * 0.99)
expect(manager.needsCompression()).toBe(false)
})
})
})

View File

@@ -19,7 +19,7 @@ vi.mock("../../../../src/infrastructure/indexer/FileScanner.js", () => ({
return 'export function main() { return "hello" }'
}
if (path.includes("utils.ts")) {
return 'export const add = (a: number, b: number) => a + b'
return "export const add = (a: number, b: number) => a + b"
}
return null
}
@@ -31,7 +31,16 @@ vi.mock("../../../../src/infrastructure/indexer/ASTParser.js", () => ({
parse() {
return {
...createEmptyFileAST(),
functions: [{ name: "test", lineStart: 1, lineEnd: 5, params: [], isAsync: false, isExported: true }],
functions: [
{
name: "test",
lineStart: 1,
lineEnd: 5,
params: [],
isAsync: false,
isExported: true,
},
],
}
}
},
@@ -116,7 +125,7 @@ describe("IndexProject", () => {
expect.objectContaining({
hash: expect.any(String),
lines: expect.any(Array),
})
}),
)
})
@@ -128,7 +137,7 @@ describe("IndexProject", () => {
"src/index.ts",
expect.objectContaining({
functions: expect.any(Array),
})
}),
)
})
@@ -136,19 +145,14 @@ describe("IndexProject", () => {
await useCase.execute("/test/project")
expect(mockStorage.setMeta).toHaveBeenCalledTimes(2)
expect(mockStorage.setMeta).toHaveBeenCalledWith(
"src/index.ts",
expect.any(Object)
)
expect(mockStorage.setMeta).toHaveBeenCalledWith("src/index.ts", expect.any(Object))
})
it("should build and store symbol index", async () => {
await useCase.execute("/test/project")
expect(mockStorage.setSymbolIndex).toHaveBeenCalledTimes(1)
expect(mockStorage.setSymbolIndex).toHaveBeenCalledWith(
expect.any(Map)
)
expect(mockStorage.setSymbolIndex).toHaveBeenCalledWith(expect.any(Map))
})
it("should build and store dependency graph", async () => {
@@ -159,7 +163,7 @@ describe("IndexProject", () => {
expect.objectContaining({
imports: expect.any(Map),
importedBy: expect.any(Map),
})
}),
)
})
@@ -168,7 +172,7 @@ describe("IndexProject", () => {
expect(mockStorage.setProjectConfig).toHaveBeenCalledWith(
"last_indexed",
expect.any(Number)
expect.any(Number),
)
})
@@ -186,7 +190,7 @@ describe("IndexProject", () => {
total: expect.any(Number),
currentFile: expect.any(String),
phase: expect.stringMatching(/scanning|parsing|analyzing|indexing/),
})
}),
)
})
@@ -198,7 +202,7 @@ describe("IndexProject", () => {
})
const scanningCalls = progressCallback.mock.calls.filter(
(call) => call[0].phase === "scanning"
(call) => call[0].phase === "scanning",
)
expect(scanningCalls.length).toBeGreaterThan(0)
})
@@ -211,7 +215,7 @@ describe("IndexProject", () => {
})
const parsingCalls = progressCallback.mock.calls.filter(
(call) => call[0].phase === "parsing"
(call) => call[0].phase === "parsing",
)
expect(parsingCalls.length).toBeGreaterThan(0)
})
@@ -224,7 +228,7 @@ describe("IndexProject", () => {
})
const analyzingCalls = progressCallback.mock.calls.filter(
(call) => call[0].phase === "analyzing"
(call) => call[0].phase === "analyzing",
)
expect(analyzingCalls.length).toBeGreaterThan(0)
})
@@ -237,7 +241,7 @@ describe("IndexProject", () => {
})
const indexingCalls = progressCallback.mock.calls.filter(
(call) => call[0].phase === "indexing"
(call) => call[0].phase === "indexing",
)
expect(indexingCalls.length).toBeGreaterThan(0)
})
@@ -245,10 +249,7 @@ describe("IndexProject", () => {
it("should detect TypeScript files", async () => {
await useCase.execute("/test/project")
expect(mockStorage.setAST).toHaveBeenCalledWith(
"src/index.ts",
expect.any(Object)
)
expect(mockStorage.setAST).toHaveBeenCalledWith("src/index.ts", expect.any(Object))
})
it("should handle files without parseable language", async () => {
@@ -276,7 +277,7 @@ describe("IndexProject", () => {
expect(mockStorage.setAST).toHaveBeenCalledWith(
expect.stringContaining(".ts"),
expect.any(Object)
expect.any(Object),
)
})
})
@@ -294,7 +295,7 @@ describe("IndexProject", () => {
})
const callsWithFiles = progressCallback.mock.calls.filter(
(call) => call[0].currentFile && call[0].currentFile.length > 0
(call) => call[0].currentFile && call[0].currentFile.length > 0,
)
expect(callsWithFiles.length).toBeGreaterThan(0)
})
@@ -307,7 +308,7 @@ describe("IndexProject", () => {
})
const parsingCalls = progressCallback.mock.calls.filter(
(call) => call[0].phase === "parsing"
(call) => call[0].phase === "parsing",
)
if (parsingCalls.length > 0) {
expect(parsingCalls[0][0].total).toBe(2)

View File

@@ -404,4 +404,106 @@ function mix(
expect(ast.exports.length).toBeGreaterThanOrEqual(4)
})
})
describe("JSON parsing", () => {
it("should extract top-level keys from JSON object", () => {
const json = `{
"name": "test",
"version": "1.0.0",
"dependencies": {},
"scripts": {}
}`
const ast = parser.parse(json, "json")
expect(ast.parseError).toBe(false)
expect(ast.exports).toHaveLength(4)
expect(ast.exports.map((e) => e.name)).toEqual([
"name",
"version",
"dependencies",
"scripts",
])
expect(ast.exports.every((e) => e.kind === "variable")).toBe(true)
})
it("should handle empty JSON object", () => {
const json = `{}`
const ast = parser.parse(json, "json")
expect(ast.parseError).toBe(false)
expect(ast.exports).toHaveLength(0)
})
})
describe("YAML parsing", () => {
it("should extract top-level keys from YAML", () => {
const yaml = `name: test
version: 1.0.0
dependencies:
foo: ^1.0.0
scripts:
test: vitest`
const ast = parser.parse(yaml, "yaml")
expect(ast.parseError).toBe(false)
expect(ast.exports.length).toBeGreaterThanOrEqual(4)
expect(ast.exports.map((e) => e.name)).toContain("name")
expect(ast.exports.map((e) => e.name)).toContain("version")
expect(ast.exports.every((e) => e.kind === "variable")).toBe(true)
})
it("should handle YAML array at root", () => {
const yaml = `- item1
- item2
- item3`
const ast = parser.parse(yaml, "yaml")
expect(ast.parseError).toBe(false)
expect(ast.exports).toHaveLength(1)
expect(ast.exports[0].name).toBe("(array)")
})
it("should handle empty YAML", () => {
const yaml = ``
const ast = parser.parse(yaml, "yaml")
expect(ast.parseError).toBe(false)
expect(ast.exports).toHaveLength(0)
})
it("should handle YAML with null content", () => {
const yaml = `null`
const ast = parser.parse(yaml, "yaml")
expect(ast.parseError).toBe(false)
expect(ast.exports).toHaveLength(0)
})
it("should handle invalid YAML with parse error", () => {
const yaml = `{invalid: yaml: syntax: [}`
const ast = parser.parse(yaml, "yaml")
expect(ast.parseError).toBe(true)
expect(ast.parseErrorMessage).toBeDefined()
})
it("should track correct line numbers for YAML keys", () => {
const yaml = `first: value1
second: value2
third: value3`
const ast = parser.parse(yaml, "yaml")
expect(ast.parseError).toBe(false)
expect(ast.exports).toHaveLength(3)
expect(ast.exports[0].line).toBe(1)
expect(ast.exports[1].line).toBe(2)
expect(ast.exports[2].line).toBe(3)
})
})
})

View File

@@ -123,8 +123,7 @@ describe("OllamaClient", () => {
mockOllamaInstance.chat.mockResolvedValue({
message: {
role: "assistant",
content:
'<tool_call name="get_lines"><path>src/index.ts</path></tool_call>',
content: '<tool_call name="get_lines"><path>src/index.ts</path></tool_call>',
tool_calls: undefined,
},
eval_count: 30,
@@ -408,7 +407,6 @@ describe("OllamaClient", () => {
})
})
describe("error handling", () => {
it("should handle ECONNREFUSED errors", async () => {
mockOllamaInstance.chat.mockRejectedValue(new Error("ECONNREFUSED"))
@@ -435,7 +433,9 @@ describe("OllamaClient", () => {
const client = new OllamaClient(defaultConfig)
await expect(client.chat([createUserMessage("Hello")])).rejects.toThrow(/Request was aborted/)
await expect(client.chat([createUserMessage("Hello")])).rejects.toThrow(
/Request was aborted/,
)
})
it("should handle model not found errors", async () => {
@@ -443,7 +443,9 @@ describe("OllamaClient", () => {
const client = new OllamaClient(defaultConfig)
await expect(client.chat([createUserMessage("Hello")])).rejects.toThrow(/Model.*not found/)
await expect(client.chat([createUserMessage("Hello")])).rejects.toThrow(
/Model.*not found/,
)
})
})
})

View File

@@ -303,7 +303,9 @@ describe("GetFunctionTool", () => {
})
it("should handle error when reading lines fails", async () => {
const ast = createMockAST([createMockFunction({ name: "test", lineStart: 1, lineEnd: 1 })])
const ast = createMockAST([
createMockFunction({ name: "test", lineStart: 1, lineEnd: 1 }),
])
const storage: IStorage = {
getFile: vi.fn().mockResolvedValue(null),
getAST: vi.fn().mockResolvedValue(ast),

View File

@@ -354,6 +354,36 @@ describe("RunCommandTool", () => {
expect(execFn).toHaveBeenCalledWith("ls", expect.objectContaining({ timeout: 5000 }))
})
it("should use config timeout", async () => {
const execFn = createMockExec({})
const toolWithMock = new RunCommandTool(undefined, execFn, { timeout: 45000 })
const ctx = createMockContext()
await toolWithMock.execute({ command: "ls" }, ctx)
expect(execFn).toHaveBeenCalledWith("ls", expect.objectContaining({ timeout: 45000 }))
})
it("should use null config timeout as default", async () => {
const execFn = createMockExec({})
const toolWithMock = new RunCommandTool(undefined, execFn, { timeout: null })
const ctx = createMockContext()
await toolWithMock.execute({ command: "ls" }, ctx)
expect(execFn).toHaveBeenCalledWith("ls", expect.objectContaining({ timeout: 30000 }))
})
it("should prefer param timeout over config timeout", async () => {
const execFn = createMockExec({})
const toolWithMock = new RunCommandTool(undefined, execFn, { timeout: 45000 })
const ctx = createMockContext()
await toolWithMock.execute({ command: "ls", timeout: 5000 }, ctx)
expect(execFn).toHaveBeenCalledWith("ls", expect.objectContaining({ timeout: 5000 }))
})
it("should execute in project root", async () => {
const execFn = createMockExec({})
const toolWithMock = new RunCommandTool(undefined, execFn)

View File

@@ -0,0 +1,204 @@
/**
* Tests for AutocompleteConfigSchema.
*/
import { describe, expect, it } from "vitest"
import { AutocompleteConfigSchema } from "../../../src/shared/constants/config.js"
describe("AutocompleteConfigSchema", () => {
describe("default values", () => {
it("should use defaults when empty object provided", () => {
const result = AutocompleteConfigSchema.parse({})
expect(result).toEqual({
enabled: true,
source: "redis-index",
maxSuggestions: 10,
})
})
it("should use defaults via .default({})", () => {
const result = AutocompleteConfigSchema.default({}).parse({})
expect(result).toEqual({
enabled: true,
source: "redis-index",
maxSuggestions: 10,
})
})
})
describe("enabled", () => {
it("should accept true", () => {
const result = AutocompleteConfigSchema.parse({ enabled: true })
expect(result.enabled).toBe(true)
})
it("should accept false", () => {
const result = AutocompleteConfigSchema.parse({ enabled: false })
expect(result.enabled).toBe(false)
})
it("should reject non-boolean", () => {
expect(() => AutocompleteConfigSchema.parse({ enabled: "true" })).toThrow()
})
it("should reject number", () => {
expect(() => AutocompleteConfigSchema.parse({ enabled: 1 })).toThrow()
})
})
describe("source", () => {
it("should accept redis-index", () => {
const result = AutocompleteConfigSchema.parse({ source: "redis-index" })
expect(result.source).toBe("redis-index")
})
it("should accept filesystem", () => {
const result = AutocompleteConfigSchema.parse({ source: "filesystem" })
expect(result.source).toBe("filesystem")
})
it("should accept both", () => {
const result = AutocompleteConfigSchema.parse({ source: "both" })
expect(result.source).toBe("both")
})
it("should use default redis-index", () => {
const result = AutocompleteConfigSchema.parse({})
expect(result.source).toBe("redis-index")
})
it("should reject invalid source", () => {
expect(() => AutocompleteConfigSchema.parse({ source: "invalid" })).toThrow()
})
it("should reject non-string", () => {
expect(() => AutocompleteConfigSchema.parse({ source: 123 })).toThrow()
})
})
describe("maxSuggestions", () => {
it("should accept valid positive integer", () => {
const result = AutocompleteConfigSchema.parse({ maxSuggestions: 5 })
expect(result.maxSuggestions).toBe(5)
})
it("should accept default value", () => {
const result = AutocompleteConfigSchema.parse({ maxSuggestions: 10 })
expect(result.maxSuggestions).toBe(10)
})
it("should accept large value", () => {
const result = AutocompleteConfigSchema.parse({ maxSuggestions: 100 })
expect(result.maxSuggestions).toBe(100)
})
it("should accept 1", () => {
const result = AutocompleteConfigSchema.parse({ maxSuggestions: 1 })
expect(result.maxSuggestions).toBe(1)
})
it("should reject zero", () => {
expect(() => AutocompleteConfigSchema.parse({ maxSuggestions: 0 })).toThrow()
})
it("should reject negative number", () => {
expect(() => AutocompleteConfigSchema.parse({ maxSuggestions: -5 })).toThrow()
})
it("should reject float", () => {
expect(() => AutocompleteConfigSchema.parse({ maxSuggestions: 10.5 })).toThrow()
})
it("should reject non-number", () => {
expect(() => AutocompleteConfigSchema.parse({ maxSuggestions: "10" })).toThrow()
})
})
describe("partial config", () => {
it("should merge partial config with defaults (enabled only)", () => {
const result = AutocompleteConfigSchema.parse({
enabled: false,
})
expect(result).toEqual({
enabled: false,
source: "redis-index",
maxSuggestions: 10,
})
})
it("should merge partial config with defaults (source only)", () => {
const result = AutocompleteConfigSchema.parse({
source: "filesystem",
})
expect(result).toEqual({
enabled: true,
source: "filesystem",
maxSuggestions: 10,
})
})
it("should merge partial config with defaults (maxSuggestions only)", () => {
const result = AutocompleteConfigSchema.parse({
maxSuggestions: 20,
})
expect(result).toEqual({
enabled: true,
source: "redis-index",
maxSuggestions: 20,
})
})
it("should merge multiple partial fields", () => {
const result = AutocompleteConfigSchema.parse({
enabled: false,
maxSuggestions: 5,
})
expect(result).toEqual({
enabled: false,
source: "redis-index",
maxSuggestions: 5,
})
})
})
describe("full config", () => {
it("should accept valid full config", () => {
const config = {
enabled: false,
source: "both" as const,
maxSuggestions: 15,
}
const result = AutocompleteConfigSchema.parse(config)
expect(result).toEqual(config)
})
it("should accept all defaults explicitly", () => {
const config = {
enabled: true,
source: "redis-index" as const,
maxSuggestions: 10,
}
const result = AutocompleteConfigSchema.parse(config)
expect(result).toEqual(config)
})
it("should accept filesystem as source", () => {
const config = {
enabled: true,
source: "filesystem" as const,
maxSuggestions: 20,
}
const result = AutocompleteConfigSchema.parse(config)
expect(result).toEqual(config)
})
})
})

View File

@@ -0,0 +1,137 @@
/**
* Tests for CommandsConfigSchema.
*/
import { describe, expect, it } from "vitest"
import { CommandsConfigSchema } from "../../../src/shared/constants/config.js"
describe("CommandsConfigSchema", () => {
describe("default values", () => {
it("should use defaults when empty object provided", () => {
const result = CommandsConfigSchema.parse({})
expect(result).toEqual({
timeout: null,
})
})
it("should use defaults via .default({})", () => {
const result = CommandsConfigSchema.default({}).parse({})
expect(result).toEqual({
timeout: null,
})
})
})
describe("timeout", () => {
it("should accept null (default)", () => {
const result = CommandsConfigSchema.parse({ timeout: null })
expect(result.timeout).toBe(null)
})
it("should accept positive integer", () => {
const result = CommandsConfigSchema.parse({ timeout: 5000 })
expect(result.timeout).toBe(5000)
})
it("should accept large timeout", () => {
const result = CommandsConfigSchema.parse({ timeout: 600000 })
expect(result.timeout).toBe(600000)
})
it("should accept 1", () => {
const result = CommandsConfigSchema.parse({ timeout: 1 })
expect(result.timeout).toBe(1)
})
it("should accept small timeout", () => {
const result = CommandsConfigSchema.parse({ timeout: 100 })
expect(result.timeout).toBe(100)
})
it("should reject zero", () => {
expect(() => CommandsConfigSchema.parse({ timeout: 0 })).toThrow()
})
it("should reject negative number", () => {
expect(() => CommandsConfigSchema.parse({ timeout: -5000 })).toThrow()
})
it("should reject float", () => {
expect(() => CommandsConfigSchema.parse({ timeout: 5000.5 })).toThrow()
})
it("should reject string", () => {
expect(() => CommandsConfigSchema.parse({ timeout: "5000" })).toThrow()
})
it("should reject boolean", () => {
expect(() => CommandsConfigSchema.parse({ timeout: true })).toThrow()
})
it("should reject undefined (use null instead)", () => {
const result = CommandsConfigSchema.parse({ timeout: undefined })
expect(result.timeout).toBe(null)
})
})
describe("partial config", () => {
it("should use default null when timeout not provided", () => {
const result = CommandsConfigSchema.parse({})
expect(result).toEqual({
timeout: null,
})
})
it("should accept explicit null", () => {
const result = CommandsConfigSchema.parse({
timeout: null,
})
expect(result).toEqual({
timeout: null,
})
})
it("should accept explicit timeout value", () => {
const result = CommandsConfigSchema.parse({
timeout: 10000,
})
expect(result).toEqual({
timeout: 10000,
})
})
})
describe("full config", () => {
it("should accept valid config with null", () => {
const config = {
timeout: null,
}
const result = CommandsConfigSchema.parse(config)
expect(result).toEqual(config)
})
it("should accept valid config with timeout", () => {
const config = {
timeout: 30000,
}
const result = CommandsConfigSchema.parse(config)
expect(result).toEqual(config)
})
it("should accept default explicitly", () => {
const config = {
timeout: null,
}
const result = CommandsConfigSchema.parse(config)
expect(result).toEqual(config)
})
})
})

View File

@@ -0,0 +1,221 @@
/**
* Tests for ContextConfigSchema.
*/
import { describe, expect, it } from "vitest"
import { ContextConfigSchema } from "../../../src/shared/constants/config.js"
describe("ContextConfigSchema", () => {
describe("default values", () => {
it("should use defaults when empty object provided", () => {
const result = ContextConfigSchema.parse({})
expect(result).toEqual({
systemPromptTokens: 2000,
maxContextUsage: 0.8,
autoCompressAt: 0.8,
compressionMethod: "llm-summary",
})
})
it("should use defaults via .default({})", () => {
const result = ContextConfigSchema.default({}).parse({})
expect(result).toEqual({
systemPromptTokens: 2000,
maxContextUsage: 0.8,
autoCompressAt: 0.8,
compressionMethod: "llm-summary",
})
})
})
describe("systemPromptTokens", () => {
it("should accept valid positive integer", () => {
const result = ContextConfigSchema.parse({ systemPromptTokens: 1500 })
expect(result.systemPromptTokens).toBe(1500)
})
it("should accept default value", () => {
const result = ContextConfigSchema.parse({ systemPromptTokens: 2000 })
expect(result.systemPromptTokens).toBe(2000)
})
it("should accept large value", () => {
const result = ContextConfigSchema.parse({ systemPromptTokens: 5000 })
expect(result.systemPromptTokens).toBe(5000)
})
it("should reject zero", () => {
expect(() => ContextConfigSchema.parse({ systemPromptTokens: 0 })).toThrow()
})
it("should reject negative number", () => {
expect(() => ContextConfigSchema.parse({ systemPromptTokens: -100 })).toThrow()
})
it("should reject float", () => {
expect(() => ContextConfigSchema.parse({ systemPromptTokens: 1500.5 })).toThrow()
})
it("should reject non-number", () => {
expect(() => ContextConfigSchema.parse({ systemPromptTokens: "2000" })).toThrow()
})
})
describe("maxContextUsage", () => {
it("should accept valid ratio", () => {
const result = ContextConfigSchema.parse({ maxContextUsage: 0.7 })
expect(result.maxContextUsage).toBe(0.7)
})
it("should accept default value", () => {
const result = ContextConfigSchema.parse({ maxContextUsage: 0.8 })
expect(result.maxContextUsage).toBe(0.8)
})
it("should accept minimum value (0)", () => {
const result = ContextConfigSchema.parse({ maxContextUsage: 0 })
expect(result.maxContextUsage).toBe(0)
})
it("should accept maximum value (1)", () => {
const result = ContextConfigSchema.parse({ maxContextUsage: 1 })
expect(result.maxContextUsage).toBe(1)
})
it("should reject value above 1", () => {
expect(() => ContextConfigSchema.parse({ maxContextUsage: 1.1 })).toThrow()
})
it("should reject negative value", () => {
expect(() => ContextConfigSchema.parse({ maxContextUsage: -0.1 })).toThrow()
})
it("should reject non-number", () => {
expect(() => ContextConfigSchema.parse({ maxContextUsage: "0.8" })).toThrow()
})
})
describe("autoCompressAt", () => {
it("should accept valid ratio", () => {
const result = ContextConfigSchema.parse({ autoCompressAt: 0.75 })
expect(result.autoCompressAt).toBe(0.75)
})
it("should accept default value", () => {
const result = ContextConfigSchema.parse({ autoCompressAt: 0.8 })
expect(result.autoCompressAt).toBe(0.8)
})
it("should accept minimum value (0)", () => {
const result = ContextConfigSchema.parse({ autoCompressAt: 0 })
expect(result.autoCompressAt).toBe(0)
})
it("should accept maximum value (1)", () => {
const result = ContextConfigSchema.parse({ autoCompressAt: 1 })
expect(result.autoCompressAt).toBe(1)
})
it("should reject value above 1", () => {
expect(() => ContextConfigSchema.parse({ autoCompressAt: 1.5 })).toThrow()
})
it("should reject negative value", () => {
expect(() => ContextConfigSchema.parse({ autoCompressAt: -0.5 })).toThrow()
})
it("should reject non-number", () => {
expect(() => ContextConfigSchema.parse({ autoCompressAt: "0.8" })).toThrow()
})
})
describe("compressionMethod", () => {
it("should accept llm-summary", () => {
const result = ContextConfigSchema.parse({ compressionMethod: "llm-summary" })
expect(result.compressionMethod).toBe("llm-summary")
})
it("should accept truncate", () => {
const result = ContextConfigSchema.parse({ compressionMethod: "truncate" })
expect(result.compressionMethod).toBe("truncate")
})
it("should reject invalid method", () => {
expect(() => ContextConfigSchema.parse({ compressionMethod: "invalid" })).toThrow()
})
it("should reject non-string", () => {
expect(() => ContextConfigSchema.parse({ compressionMethod: 123 })).toThrow()
})
})
describe("partial config", () => {
it("should merge partial config with defaults (systemPromptTokens)", () => {
const result = ContextConfigSchema.parse({
systemPromptTokens: 3000,
})
expect(result).toEqual({
systemPromptTokens: 3000,
maxContextUsage: 0.8,
autoCompressAt: 0.8,
compressionMethod: "llm-summary",
})
})
it("should merge partial config with defaults (autoCompressAt)", () => {
const result = ContextConfigSchema.parse({
autoCompressAt: 0.9,
})
expect(result).toEqual({
systemPromptTokens: 2000,
maxContextUsage: 0.8,
autoCompressAt: 0.9,
compressionMethod: "llm-summary",
})
})
it("should merge multiple partial fields", () => {
const result = ContextConfigSchema.parse({
maxContextUsage: 0.7,
compressionMethod: "truncate",
})
expect(result).toEqual({
systemPromptTokens: 2000,
maxContextUsage: 0.7,
autoCompressAt: 0.8,
compressionMethod: "truncate",
})
})
})
describe("full config", () => {
it("should accept valid full config", () => {
const config = {
systemPromptTokens: 3000,
maxContextUsage: 0.9,
autoCompressAt: 0.85,
compressionMethod: "truncate" as const,
}
const result = ContextConfigSchema.parse(config)
expect(result).toEqual(config)
})
it("should accept all defaults explicitly", () => {
const config = {
systemPromptTokens: 2000,
maxContextUsage: 0.8,
autoCompressAt: 0.8,
compressionMethod: "llm-summary" as const,
}
const result = ContextConfigSchema.parse(config)
expect(result).toEqual(config)
})
})
})

View File

@@ -0,0 +1,146 @@
/**
* Tests for SessionConfigSchema.
*/
import { describe, expect, it } from "vitest"
import { SessionConfigSchema } from "../../../src/shared/constants/config.js"
describe("SessionConfigSchema", () => {
describe("default values", () => {
it("should use defaults when empty object provided", () => {
const result = SessionConfigSchema.parse({})
expect(result).toEqual({
persistIndefinitely: true,
maxHistoryMessages: 100,
saveInputHistory: true,
})
})
it("should use defaults via .default({})", () => {
const result = SessionConfigSchema.default({}).parse({})
expect(result).toEqual({
persistIndefinitely: true,
maxHistoryMessages: 100,
saveInputHistory: true,
})
})
})
describe("persistIndefinitely", () => {
it("should accept true", () => {
const result = SessionConfigSchema.parse({ persistIndefinitely: true })
expect(result.persistIndefinitely).toBe(true)
})
it("should accept false", () => {
const result = SessionConfigSchema.parse({ persistIndefinitely: false })
expect(result.persistIndefinitely).toBe(false)
})
it("should reject non-boolean", () => {
expect(() => SessionConfigSchema.parse({ persistIndefinitely: "yes" })).toThrow()
})
})
describe("maxHistoryMessages", () => {
it("should accept valid positive integer", () => {
const result = SessionConfigSchema.parse({ maxHistoryMessages: 50 })
expect(result.maxHistoryMessages).toBe(50)
})
it("should accept default value", () => {
const result = SessionConfigSchema.parse({ maxHistoryMessages: 100 })
expect(result.maxHistoryMessages).toBe(100)
})
it("should accept large value", () => {
const result = SessionConfigSchema.parse({ maxHistoryMessages: 1000 })
expect(result.maxHistoryMessages).toBe(1000)
})
it("should reject zero", () => {
expect(() => SessionConfigSchema.parse({ maxHistoryMessages: 0 })).toThrow()
})
it("should reject negative number", () => {
expect(() => SessionConfigSchema.parse({ maxHistoryMessages: -10 })).toThrow()
})
it("should reject float", () => {
expect(() => SessionConfigSchema.parse({ maxHistoryMessages: 10.5 })).toThrow()
})
it("should reject non-number", () => {
expect(() => SessionConfigSchema.parse({ maxHistoryMessages: "100" })).toThrow()
})
})
describe("saveInputHistory", () => {
it("should accept true", () => {
const result = SessionConfigSchema.parse({ saveInputHistory: true })
expect(result.saveInputHistory).toBe(true)
})
it("should accept false", () => {
const result = SessionConfigSchema.parse({ saveInputHistory: false })
expect(result.saveInputHistory).toBe(false)
})
it("should reject non-boolean", () => {
expect(() => SessionConfigSchema.parse({ saveInputHistory: "yes" })).toThrow()
})
})
describe("partial config", () => {
it("should merge partial config with defaults", () => {
const result = SessionConfigSchema.parse({
maxHistoryMessages: 50,
})
expect(result).toEqual({
persistIndefinitely: true,
maxHistoryMessages: 50,
saveInputHistory: true,
})
})
it("should merge multiple partial fields", () => {
const result = SessionConfigSchema.parse({
persistIndefinitely: false,
saveInputHistory: false,
})
expect(result).toEqual({
persistIndefinitely: false,
maxHistoryMessages: 100,
saveInputHistory: false,
})
})
})
describe("full config", () => {
it("should accept valid full config", () => {
const config = {
persistIndefinitely: false,
maxHistoryMessages: 200,
saveInputHistory: false,
}
const result = SessionConfigSchema.parse(config)
expect(result).toEqual(config)
})
it("should accept all defaults explicitly", () => {
const config = {
persistIndefinitely: true,
maxHistoryMessages: 100,
saveInputHistory: true,
}
const result = SessionConfigSchema.parse(config)
expect(result).toEqual(config)
})
})
})

View File

@@ -218,28 +218,32 @@ describe("Input", () => {
it("should be active when multiline is true", () => {
const multiline = true
const lines = ["single line"]
const isMultilineActive = multiline === true || (multiline === "auto" && lines.length > 1)
const isMultilineActive =
multiline === true || (multiline === "auto" && lines.length > 1)
expect(isMultilineActive).toBe(true)
})
it("should not be active when multiline is false", () => {
const multiline = false
const lines = ["line1", "line2"]
const isMultilineActive = multiline === true || (multiline === "auto" && lines.length > 1)
const isMultilineActive =
multiline === true || (multiline === "auto" && lines.length > 1)
expect(isMultilineActive).toBe(false)
})
it("should be active in auto mode with multiple lines", () => {
const multiline = "auto"
const lines = ["line1", "line2"]
const isMultilineActive = multiline === true || (multiline === "auto" && lines.length > 1)
const isMultilineActive =
multiline === true || (multiline === "auto" && lines.length > 1)
expect(isMultilineActive).toBe(true)
})
it("should not be active in auto mode with single line", () => {
const multiline = "auto"
const lines = ["single line"]
const isMultilineActive = multiline === true || (multiline === "auto" && lines.length > 1)
const isMultilineActive =
multiline === true || (multiline === "auto" && lines.length > 1)
expect(isMultilineActive).toBe(false)
})
})

View File

@@ -3,7 +3,12 @@
*/
import { describe, expect, it } from "vitest"
import { getColorScheme, getContextColor, getRoleColor, getStatusColor } from "../../../../src/tui/utils/theme.js"
import {
getColorScheme,
getContextColor,
getRoleColor,
getStatusColor,
} from "../../../../src/tui/utils/theme.js"
describe("theme utilities", () => {
describe("getColorScheme", () => {

View File

@@ -24,7 +24,7 @@ export default defineConfig({
thresholds: {
lines: 95,
functions: 95,
branches: 91.3,
branches: 91,
statements: 95,
},
},

56
pnpm-lock.yaml generated
View File

@@ -131,7 +131,7 @@ importers:
version: 5.9.3
vitest:
specifier: ^4.0.10
version: 4.0.13(@types/node@22.19.1)(@vitest/ui@4.0.13)(jsdom@27.2.0)(terser@5.44.1)(tsx@4.20.6)
version: 4.0.13(@types/node@22.19.1)(@vitest/ui@4.0.13)(jsdom@27.2.0)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.2)
packages/ipuaro:
dependencies:
@@ -168,9 +168,15 @@ importers:
tree-sitter-javascript:
specifier: ^0.21.0
version: 0.21.4(tree-sitter@0.21.1)
tree-sitter-json:
specifier: ^0.24.8
version: 0.24.8(tree-sitter@0.21.1)
tree-sitter-typescript:
specifier: ^0.21.2
version: 0.21.2(tree-sitter@0.21.1)
yaml:
specifier: ^2.8.2
version: 2.8.2
zod:
specifier: ^3.23.8
version: 3.25.76
@@ -201,7 +207,7 @@ importers:
version: 18.3.1(react@18.3.1)
tsup:
specifier: ^8.3.5
version: 8.5.1(postcss@8.5.6)(tsx@4.20.6)(typescript@5.9.3)
version: 8.5.1(postcss@8.5.6)(tsx@4.20.6)(typescript@5.9.3)(yaml@2.8.2)
typescript:
specifier: ^5.7.2
version: 5.9.3
@@ -4172,6 +4178,14 @@ packages:
tree-sitter:
optional: true
tree-sitter-json@0.24.8:
resolution: {integrity: sha512-Tc9ZZYwHyWZ3Tt1VEw7Pa2scu1YO7/d2BCBbKTx5hXwig3UfdQjsOPkPyLpDJOn/m1UBEWYAtSdGAwCSyagBqQ==}
peerDependencies:
tree-sitter: ^0.21.1
peerDependenciesMeta:
tree-sitter:
optional: true
tree-sitter-typescript@0.21.2:
resolution: {integrity: sha512-/RyNK41ZpkA8PuPZimR6pGLvNR1p0ibRUJwwQn4qAjyyLEIQD/BNlwS3NSxWtGsAWZe9gZ44VK1mWx2+eQVldg==}
peerDependencies:
@@ -4636,6 +4650,11 @@ packages:
yallist@3.1.1:
resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==}
yaml@2.8.2:
resolution: {integrity: sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==}
engines: {node: '>= 14.6'}
hasBin: true
yargs-parser@21.1.1:
resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==}
engines: {node: '>=12'}
@@ -6325,7 +6344,7 @@ snapshots:
magicast: 0.5.1
std-env: 3.10.0
tinyrainbow: 3.0.3
vitest: 4.0.13(@types/node@22.19.1)(@vitest/ui@4.0.13)(jsdom@27.2.0)(terser@5.44.1)(tsx@4.20.6)
vitest: 4.0.13(@types/node@22.19.1)(@vitest/ui@4.0.13)(jsdom@27.2.0)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.2)
transitivePeerDependencies:
- supports-color
@@ -6344,13 +6363,13 @@ snapshots:
chai: 6.2.1
tinyrainbow: 3.0.3
'@vitest/mocker@4.0.13(vite@7.2.4(@types/node@22.19.1)(terser@5.44.1)(tsx@4.20.6))':
'@vitest/mocker@4.0.13(vite@7.2.4(@types/node@22.19.1)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.2))':
dependencies:
'@vitest/spy': 4.0.13
estree-walker: 3.0.3
magic-string: 0.30.21
optionalDependencies:
vite: 7.2.4(@types/node@22.19.1)(terser@5.44.1)(tsx@4.20.6)
vite: 7.2.4(@types/node@22.19.1)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.2)
'@vitest/pretty-format@4.0.13':
dependencies:
@@ -6405,7 +6424,7 @@ snapshots:
sirv: 3.0.2
tinyglobby: 0.2.15
tinyrainbow: 3.0.3
vitest: 4.0.13(@types/node@22.19.1)(@vitest/ui@4.0.13)(jsdom@27.2.0)(terser@5.44.1)(tsx@4.20.6)
vitest: 4.0.13(@types/node@22.19.1)(@vitest/ui@4.0.13)(jsdom@27.2.0)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.2)
'@vitest/utils@1.6.1':
dependencies:
@@ -8392,12 +8411,13 @@ snapshots:
pluralize@8.0.0: {}
postcss-load-config@6.0.1(postcss@8.5.6)(tsx@4.20.6):
postcss-load-config@6.0.1(postcss@8.5.6)(tsx@4.20.6)(yaml@2.8.2):
dependencies:
lilconfig: 3.1.3
optionalDependencies:
postcss: 8.5.6
tsx: 4.20.6
yaml: 2.8.2
postcss@8.5.6:
dependencies:
@@ -8911,6 +8931,13 @@ snapshots:
optionalDependencies:
tree-sitter: 0.21.1
tree-sitter-json@0.24.8(tree-sitter@0.21.1):
dependencies:
node-addon-api: 8.5.0
node-gyp-build: 4.8.4
optionalDependencies:
tree-sitter: 0.21.1
tree-sitter-typescript@0.21.2(tree-sitter@0.21.1):
dependencies:
node-addon-api: 8.5.0
@@ -8999,7 +9026,7 @@ snapshots:
tslib@2.8.1: {}
tsup@8.5.1(postcss@8.5.6)(tsx@4.20.6)(typescript@5.9.3):
tsup@8.5.1(postcss@8.5.6)(tsx@4.20.6)(typescript@5.9.3)(yaml@2.8.2):
dependencies:
bundle-require: 5.1.0(esbuild@0.27.0)
cac: 6.7.14
@@ -9010,7 +9037,7 @@ snapshots:
fix-dts-default-cjs-exports: 1.0.1
joycon: 3.1.1
picocolors: 1.1.1
postcss-load-config: 6.0.1(postcss@8.5.6)(tsx@4.20.6)
postcss-load-config: 6.0.1(postcss@8.5.6)(tsx@4.20.6)(yaml@2.8.2)
resolve-from: 5.0.0
rollup: 4.53.3
source-map: 0.7.6
@@ -9156,7 +9183,7 @@ snapshots:
fsevents: 2.3.3
terser: 5.44.1
vite@7.2.4(@types/node@22.19.1)(terser@5.44.1)(tsx@4.20.6):
vite@7.2.4(@types/node@22.19.1)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.2):
dependencies:
esbuild: 0.25.12
fdir: 6.5.0(picomatch@4.0.3)
@@ -9169,6 +9196,7 @@ snapshots:
fsevents: 2.3.3
terser: 5.44.1
tsx: 4.20.6
yaml: 2.8.2
vitest@1.6.1(@types/node@22.19.1)(@vitest/ui@1.6.1)(jsdom@27.2.0)(terser@5.44.1):
dependencies:
@@ -9206,10 +9234,10 @@ snapshots:
- supports-color
- terser
vitest@4.0.13(@types/node@22.19.1)(@vitest/ui@4.0.13)(jsdom@27.2.0)(terser@5.44.1)(tsx@4.20.6):
vitest@4.0.13(@types/node@22.19.1)(@vitest/ui@4.0.13)(jsdom@27.2.0)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.2):
dependencies:
'@vitest/expect': 4.0.13
'@vitest/mocker': 4.0.13(vite@7.2.4(@types/node@22.19.1)(terser@5.44.1)(tsx@4.20.6))
'@vitest/mocker': 4.0.13(vite@7.2.4(@types/node@22.19.1)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.2))
'@vitest/pretty-format': 4.0.13
'@vitest/runner': 4.0.13
'@vitest/snapshot': 4.0.13
@@ -9226,7 +9254,7 @@ snapshots:
tinyexec: 0.3.2
tinyglobby: 0.2.15
tinyrainbow: 3.0.3
vite: 7.2.4(@types/node@22.19.1)(terser@5.44.1)(tsx@4.20.6)
vite: 7.2.4(@types/node@22.19.1)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.2)
why-is-node-running: 2.3.0
optionalDependencies:
'@types/node': 22.19.1
@@ -9366,6 +9394,8 @@ snapshots:
yallist@3.1.1: {}
yaml@2.8.2: {}
yargs-parser@21.1.1: {}
yargs@17.7.2: