mirror of
https://github.com/samiyev/puaros.git
synced 2025-12-28 07:16:53 +05:00
Compare commits
10 Commits
ipuaro-v0.
...
ipuaro-v0.
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1489b69e69 | ||
|
|
2dcb22812c | ||
|
|
7d7c99fe4d | ||
|
|
a3f0ba948f | ||
|
|
141888bf59 | ||
|
|
b0f1778f3a | ||
|
|
9c94335729 | ||
|
|
c34d57c231 | ||
|
|
60052c0db9 | ||
|
|
fa647c41aa |
@@ -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)
|
12. [Aggregate Boundary Validation (DDD Tactical Patterns)](#12-aggregate-boundary-validation-ddd-tactical-patterns)
|
||||||
13. [Secret Detection & Security](#13-secret-detection--security)
|
13. [Secret Detection & Security](#13-secret-detection--security)
|
||||||
14. [Severity-Based Prioritization & Technical Debt](#14-severity-based-prioritization--technical-debt)
|
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
|
## Conclusion
|
||||||
|
|
||||||
The code quality detection rules implemented in Guardian are firmly grounded in:
|
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
|
2. **Industry Standards**: ISO/IEC 25010, SonarQube rules, OWASP security guidelines, Google and Airbnb style guides
|
||||||
3. **Authoritative Books**:
|
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)
|
- Robert C. Martin's "Clean Architecture" (2017)
|
||||||
- Vaughn Vernon's "Implementing Domain-Driven Design" (2013)
|
- Vaughn Vernon's "Implementing Domain-Driven Design" (2013)
|
||||||
|
- Chris Richardson's "Microservices Patterns" (2018)
|
||||||
- Eric Evans' "Domain-Driven Design" (2003)
|
- Eric Evans' "Domain-Driven Design" (2003)
|
||||||
- Martin Fowler's "Patterns of Enterprise Application Architecture" (2002)
|
- Martin Fowler's "Patterns of Enterprise Application Architecture" (2002)
|
||||||
- Martin Fowler's "Refactoring" (1999, 2018)
|
- Martin Fowler's "Refactoring" (1999, 2018)
|
||||||
- Steve McConnell's "Code Complete" (1993, 2004)
|
- 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
|
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
|
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.
|
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
|
**Document Version**: 2.0
|
||||||
**Last Updated**: 2025-11-26
|
**Last Updated**: 2025-12-04
|
||||||
**Questions or want to contribute research?**
|
**Questions or want to contribute research?**
|
||||||
- 📧 Email: fozilbek.samiyev@gmail.com
|
- 📧 Email: fozilbek.samiyev@gmail.com
|
||||||
- 🐙 GitHub: https://github.com/samiyev/puaros/issues
|
- 🐙 GitHub: https://github.com/samiyev/puaros/issues
|
||||||
**Based on research as of**: November 2025
|
**Based on research as of**: December 2025
|
||||||
|
|||||||
@@ -5,6 +5,270 @@ 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/),
|
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).
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
|
## [0.24.0] - 2025-12-04 - Rich Initial Context: Function Signatures
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- **Function Signatures in Context (0.24.1)**
|
||||||
|
- Full function signatures with parameter types and return types in initial context
|
||||||
|
- New format: `async getUser(id: string): Promise<User>` instead of `fn: getUser`
|
||||||
|
- Classes show inheritance: `class UserService extends BaseService implements IService`
|
||||||
|
- Interfaces show extends: `interface AdminUser extends User, Admin`
|
||||||
|
- Optional parameters marked with `?`: `format(value: string, options?: FormatOptions)`
|
||||||
|
|
||||||
|
- **BuildContextOptions Interface**
|
||||||
|
- New `includeSignatures?: boolean` option for `buildInitialContext()`
|
||||||
|
- Controls signature vs compact format (default: `true` for signatures)
|
||||||
|
|
||||||
|
- **Configuration**
|
||||||
|
- Added `includeSignatures: boolean` to `ContextConfigSchema` (default: `true`)
|
||||||
|
- Users can disable signatures to save tokens: `context.includeSignatures: false`
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- **ASTParser**
|
||||||
|
- Arrow functions now extract `returnType` in `extractLexicalDeclaration()`
|
||||||
|
- Return type format normalized (strips leading `: `)
|
||||||
|
|
||||||
|
- **prompts.ts**
|
||||||
|
- New `formatFunctionSignature()` helper function
|
||||||
|
- `formatFileSummary()` now shows full signatures by default
|
||||||
|
- Added `formatFileSummaryCompact()` for legacy format
|
||||||
|
- `formatFileOverview()` accepts `includeSignatures` parameter
|
||||||
|
- Defensive handling for missing interface `extends` array
|
||||||
|
|
||||||
|
### New Context Format (default)
|
||||||
|
|
||||||
|
```
|
||||||
|
### src/services/user.ts
|
||||||
|
- async getUser(id: string): Promise<User>
|
||||||
|
- async createUser(data: UserDTO): Promise<User>
|
||||||
|
- validateEmail(email: string): boolean
|
||||||
|
- class UserService extends BaseService
|
||||||
|
- interface IUserService extends IService
|
||||||
|
- type UserId
|
||||||
|
```
|
||||||
|
|
||||||
|
### Compact Format (includeSignatures: false)
|
||||||
|
|
||||||
|
```
|
||||||
|
- src/services/user.ts [fn: getUser, createUser | class: UserService | interface: IUserService | type: UserId]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Technical Details
|
||||||
|
|
||||||
|
- Total tests: 1702 passed (was 1687, +15 new tests)
|
||||||
|
- 8 new tests for function signature formatting
|
||||||
|
- 5 new tests for `includeSignatures` configuration
|
||||||
|
- 1 new test for compact format
|
||||||
|
- 1 new test for undefined AST entries
|
||||||
|
- Coverage: 97.54% lines, 91.14% branches, 98.59% functions
|
||||||
|
- 0 ESLint errors, 2 warnings (complexity in ASTParser and prompts)
|
||||||
|
- Build successful
|
||||||
|
|
||||||
|
### Notes
|
||||||
|
|
||||||
|
This is the first part of v0.24.0 Rich Initial Context milestone:
|
||||||
|
- ✅ 0.24.1 - Function Signatures with Types
|
||||||
|
- ⏳ 0.24.2 - Interface/Type Field Definitions
|
||||||
|
- ⏳ 0.24.3 - Enum Value Definitions
|
||||||
|
- ⏳ 0.24.4 - Decorator Extraction
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## [0.23.0] - 2025-12-04 - JSON/YAML & Symlinks
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- **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
|
## [0.22.2] - 2025-12-02 - Session Configuration
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|||||||
@@ -1467,24 +1467,21 @@ interface ILLMClient {
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Version 0.20.0 - Missing Use Cases 🔧
|
## Version 0.20.0 - Missing Use Cases 🔧 ✅
|
||||||
|
|
||||||
**Priority:** HIGH
|
**Priority:** HIGH
|
||||||
**Status:** Pending
|
**Status:** Complete (v0.20.0 released)
|
||||||
|
|
||||||
### 0.20.1 - IndexProject Use Case
|
### 0.20.1 - IndexProject Use Case ✅
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
// src/application/use-cases/IndexProject.ts
|
// src/application/use-cases/IndexProject.ts
|
||||||
class IndexProject {
|
class IndexProject {
|
||||||
constructor(
|
constructor(storage: IStorage, projectRoot: string)
|
||||||
private storage: IStorage,
|
|
||||||
private indexer: IIndexer
|
|
||||||
)
|
|
||||||
|
|
||||||
async execute(
|
async execute(
|
||||||
projectRoot: string,
|
projectRoot: string,
|
||||||
onProgress?: (progress: IndexProgress) => void
|
options?: IndexProjectOptions
|
||||||
): Promise<IndexingStats>
|
): Promise<IndexingStats>
|
||||||
// Full indexing pipeline:
|
// Full indexing pipeline:
|
||||||
// 1. Scan files
|
// 1. Scan files
|
||||||
@@ -1496,50 +1493,51 @@ class IndexProject {
|
|||||||
```
|
```
|
||||||
|
|
||||||
**Deliverables:**
|
**Deliverables:**
|
||||||
- [ ] IndexProject use case implementation
|
- [x] IndexProject use case implementation (184 LOC)
|
||||||
- [ ] Integration with CLI `index` command
|
- [x] Progress reporting via callback
|
||||||
- [ ] Integration with `/reindex` slash command
|
- [x] Unit tests (318 LOC)
|
||||||
- [ ] Progress reporting via callback
|
|
||||||
- [ ] Unit tests
|
|
||||||
|
|
||||||
### 0.20.2 - ExecuteTool Use Case
|
### 0.20.2 - ExecuteTool Use Case ✅
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
// src/application/use-cases/ExecuteTool.ts
|
// src/application/use-cases/ExecuteTool.ts
|
||||||
class ExecuteTool {
|
class ExecuteTool {
|
||||||
constructor(
|
constructor(
|
||||||
private tools: IToolRegistry,
|
storage: IStorage,
|
||||||
private storage: IStorage
|
sessionStorage: ISessionStorage,
|
||||||
|
tools: IToolRegistry,
|
||||||
|
projectRoot: string
|
||||||
)
|
)
|
||||||
|
|
||||||
async execute(
|
async execute(
|
||||||
toolName: string,
|
toolCall: ToolCall,
|
||||||
params: Record<string, unknown>,
|
session: Session,
|
||||||
context: ToolContext
|
options?: ExecuteToolOptions
|
||||||
): Promise<ToolResult>
|
): Promise<ExecuteToolResult>
|
||||||
// Orchestrates tool execution with:
|
// Orchestrates tool execution with:
|
||||||
// - Parameter validation
|
// - Parameter validation
|
||||||
// - Confirmation flow
|
// - Confirmation flow (with edit support)
|
||||||
// - Undo stack management
|
// - Undo stack management
|
||||||
// - Storage updates
|
// - Storage updates
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
**Deliverables:**
|
**Deliverables:**
|
||||||
- [ ] ExecuteTool use case implementation
|
- [x] ExecuteTool use case implementation (225 LOC)
|
||||||
- [ ] Refactor HandleMessage to use ExecuteTool
|
- [x] HandleMessage uses ExecuteTool
|
||||||
- [ ] Unit tests
|
- [x] Support for edited content from confirmation dialog
|
||||||
|
- [ ] Dedicated unit tests (covered indirectly via integration)
|
||||||
|
|
||||||
**Tests:**
|
**Tests:**
|
||||||
- [ ] Unit tests for IndexProject
|
- [x] Unit tests for IndexProject
|
||||||
- [ ] Unit tests for ExecuteTool
|
- [ ] Unit tests for ExecuteTool (optional - covered via integration)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Version 0.21.0 - TUI Enhancements 🎨
|
## Version 0.21.0 - TUI Enhancements 🎨 ✅
|
||||||
|
|
||||||
**Priority:** MEDIUM
|
**Priority:** MEDIUM
|
||||||
**Status:** In Progress (2/4 complete)
|
**Status:** Complete (v0.21.0 released)
|
||||||
|
|
||||||
### 0.21.1 - useAutocomplete Hook ✅
|
### 0.21.1 - useAutocomplete Hook ✅
|
||||||
|
|
||||||
@@ -1596,59 +1594,52 @@ interface ConfirmDialogProps {
|
|||||||
- [x] ConfirmationResult type with editedContent field
|
- [x] ConfirmationResult type with editedContent field
|
||||||
- [x] All existing tests passing (1484 tests)
|
- [x] All existing tests passing (1484 tests)
|
||||||
|
|
||||||
### 0.21.3 - Multiline Input
|
### 0.21.3 - Multiline Input ✅
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
// src/tui/components/Input.tsx enhancements
|
// src/tui/components/Input.tsx
|
||||||
interface InputProps {
|
interface InputProps {
|
||||||
// ... existing props
|
|
||||||
multiline?: boolean | "auto" // auto = detect based on content
|
multiline?: boolean | "auto" // auto = detect based on content
|
||||||
}
|
}
|
||||||
|
|
||||||
// Shift+Enter for new line
|
|
||||||
// Auto-expand height
|
|
||||||
```
|
```
|
||||||
|
|
||||||
**Deliverables:**
|
**Deliverables:**
|
||||||
- [ ] Multiline support in Input component
|
- [x] Multiline support in Input component
|
||||||
- [ ] Shift+Enter handling
|
- [x] Line navigation support
|
||||||
- [ ] Auto-height adjustment
|
- [x] Auto-expand based on content
|
||||||
- [ ] Config option: `input.multiline`
|
- [x] Unit tests (37 tests)
|
||||||
- [ ] Unit tests
|
|
||||||
|
|
||||||
### 0.21.4 - Syntax Highlighting in DiffView
|
### 0.21.4 - Syntax Highlighting in DiffView ✅
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
// src/tui/components/DiffView.tsx enhancements
|
// src/tui/utils/syntax-highlighter.ts (167 LOC)
|
||||||
// Full syntax highlighting for code in diff
|
// Custom tokenizer for TypeScript/JavaScript/JSON/YAML
|
||||||
|
// Highlights keywords, strings, comments, numbers, operators
|
||||||
|
|
||||||
interface DiffViewProps {
|
interface DiffViewProps {
|
||||||
// ... existing props
|
language?: Language
|
||||||
language?: "ts" | "tsx" | "js" | "jsx"
|
|
||||||
syntaxHighlight?: boolean
|
syntaxHighlight?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use ink-syntax-highlight or custom tokenizer
|
|
||||||
```
|
```
|
||||||
|
|
||||||
**Deliverables:**
|
**Deliverables:**
|
||||||
- [ ] Syntax highlighting integration
|
- [x] Syntax highlighter implementation (167 LOC)
|
||||||
- [ ] Language detection from file extension
|
- [x] Language detection from file extension
|
||||||
- [ ] Config option: `edit.syntaxHighlight`
|
- [x] Integration with DiffView and ConfirmDialog
|
||||||
- [ ] Unit tests
|
- [x] Unit tests (24 tests)
|
||||||
|
|
||||||
**Tests:**
|
**Tests:**
|
||||||
- [ ] Unit tests for useAutocomplete
|
- [x] Unit tests for useAutocomplete (21 tests)
|
||||||
- [ ] Unit tests for enhanced ConfirmDialog
|
- [x] Unit tests for enhanced ConfirmDialog
|
||||||
- [ ] Unit tests for multiline Input
|
- [x] Unit tests for multiline Input (37 tests)
|
||||||
- [ ] Unit tests for syntax highlighting
|
- [x] Unit tests for syntax highlighting (24 tests)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Version 0.22.0 - Extended Configuration ⚙️
|
## Version 0.22.0 - Extended Configuration ⚙️
|
||||||
|
|
||||||
**Priority:** MEDIUM
|
**Priority:** MEDIUM
|
||||||
**Status:** In Progress (2/5 complete)
|
**Status:** Complete (5/5 complete) ✅
|
||||||
|
|
||||||
### 0.22.1 - Display Configuration ✅
|
### 0.22.1 - Display Configuration ✅
|
||||||
|
|
||||||
@@ -1687,7 +1678,7 @@ export const SessionConfigSchema = z.object({
|
|||||||
- [x] Input history persistence toggle
|
- [x] Input history persistence toggle
|
||||||
- [x] Unit tests (19 new tests)
|
- [x] Unit tests (19 new tests)
|
||||||
|
|
||||||
### 0.22.3 - Context Configuration
|
### 0.22.3 - Context Configuration ✅
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
// src/shared/constants/config.ts additions
|
// src/shared/constants/config.ts additions
|
||||||
@@ -1700,12 +1691,12 @@ export const ContextConfigSchema = z.object({
|
|||||||
```
|
```
|
||||||
|
|
||||||
**Deliverables:**
|
**Deliverables:**
|
||||||
- [ ] ContextConfigSchema in config.ts
|
- [x] ContextConfigSchema in config.ts
|
||||||
- [ ] ContextManager reads from config
|
- [x] ContextManager reads from config
|
||||||
- [ ] Configurable compression threshold
|
- [x] Configurable compression threshold
|
||||||
- [ ] Unit tests
|
- [x] Unit tests (40 new tests: 32 schema, 8 ContextManager integration)
|
||||||
|
|
||||||
### 0.22.4 - Autocomplete Configuration
|
### 0.22.4 - Autocomplete Configuration ✅
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
// src/shared/constants/config.ts additions
|
// src/shared/constants/config.ts additions
|
||||||
@@ -1717,11 +1708,11 @@ export const AutocompleteConfigSchema = z.object({
|
|||||||
```
|
```
|
||||||
|
|
||||||
**Deliverables:**
|
**Deliverables:**
|
||||||
- [ ] AutocompleteConfigSchema in config.ts
|
- [x] AutocompleteConfigSchema in config.ts
|
||||||
- [ ] useAutocomplete reads from config
|
- [x] useAutocomplete reads from config
|
||||||
- [ ] Unit tests
|
- [x] Unit tests (27 tests)
|
||||||
|
|
||||||
### 0.22.5 - Commands Configuration
|
### 0.22.5 - Commands Configuration ✅
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
// src/shared/constants/config.ts additions
|
// src/shared/constants/config.ts additions
|
||||||
@@ -1731,40 +1722,40 @@ export const CommandsConfigSchema = z.object({
|
|||||||
```
|
```
|
||||||
|
|
||||||
**Deliverables:**
|
**Deliverables:**
|
||||||
- [ ] CommandsConfigSchema in config.ts
|
- [x] CommandsConfigSchema in config.ts
|
||||||
- [ ] Timeout support for run_command tool
|
- [x] Timeout support for run_command tool
|
||||||
- [ ] Unit tests
|
- [x] Unit tests (19 schema tests + 3 RunCommandTool integration tests)
|
||||||
|
|
||||||
**Tests:**
|
**Tests:**
|
||||||
- [ ] Unit tests for all new config schemas
|
- [x] Unit tests for CommandsConfigSchema (19 tests)
|
||||||
- [ ] Integration tests for config loading
|
- [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
|
**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
|
```typescript
|
||||||
// src/infrastructure/indexer/ASTParser.ts enhancements
|
// src/infrastructure/indexer/ASTParser.ts enhancements
|
||||||
type Language = "ts" | "tsx" | "js" | "jsx" | "json" | "yaml"
|
type Language = "ts" | "tsx" | "js" | "jsx" | "json" | "yaml"
|
||||||
|
|
||||||
// For JSON: extract keys, structure
|
// For JSON: extract keys, structure (tree-sitter-json)
|
||||||
// For YAML: extract keys, structure
|
// For YAML: extract keys, structure (yaml npm package)
|
||||||
// Use tree-sitter-json and tree-sitter-yaml
|
|
||||||
```
|
```
|
||||||
|
|
||||||
**Deliverables:**
|
**Note:** YAML parsing uses `yaml` npm package instead of `tree-sitter-yaml` due to native binding compatibility issues.
|
||||||
- [ ] Add tree-sitter-json dependency
|
|
||||||
- [ ] Add tree-sitter-yaml dependency
|
|
||||||
- [ ] JSON parsing in ASTParser
|
|
||||||
- [ ] YAML parsing in ASTParser
|
|
||||||
- [ ] Unit tests
|
|
||||||
|
|
||||||
### 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
|
```typescript
|
||||||
// src/domain/services/IIndexer.ts enhancements
|
// src/domain/services/IIndexer.ts enhancements
|
||||||
@@ -1775,20 +1766,221 @@ export interface ScanResult {
|
|||||||
lastModified: number
|
lastModified: number
|
||||||
symlinkTarget?: string // <-- NEW: target path for symlinks
|
symlinkTarget?: string // <-- NEW: target path for symlinks
|
||||||
}
|
}
|
||||||
|
|
||||||
// Store symlink metadata in Redis
|
|
||||||
// project:{name}:meta includes symlink info
|
|
||||||
```
|
```
|
||||||
|
|
||||||
**Deliverables:**
|
**Deliverables:**
|
||||||
- [ ] Add symlinkTarget to ScanResult
|
- [x] Add symlinkTarget to ScanResult
|
||||||
- [ ] FileScanner extracts symlink targets
|
- [x] FileScanner extracts symlink targets via safeReadlink()
|
||||||
- [ ] Store symlink metadata in Redis
|
- [x] Unit tests (FileScanner tests)
|
||||||
- [ ] Unit tests
|
|
||||||
|
|
||||||
**Tests:**
|
**Tests:**
|
||||||
- [ ] Unit tests for JSON/YAML parsing
|
- [x] Unit tests for JSON/YAML parsing (2 tests)
|
||||||
- [ ] Unit tests for symlink handling
|
- [x] Unit tests for symlink handling (FileScanner tests)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Version 0.24.0 - Rich Initial Context 📋
|
||||||
|
|
||||||
|
**Priority:** HIGH
|
||||||
|
**Status:** In Progress (1/4 complete)
|
||||||
|
|
||||||
|
Улучшение initial context для LLM: добавление сигнатур функций, типов интерфейсов и значений enum. Это позволит LLM отвечать на вопросы о типах и параметрах без tool calls.
|
||||||
|
|
||||||
|
### 0.24.1 - Function Signatures with Types ⭐ ✅
|
||||||
|
|
||||||
|
**Проблема:** Сейчас LLM видит только имена функций: `fn: getUser, createUser`
|
||||||
|
**Решение:** Показать полные сигнатуры: `async getUser(id: string): Promise<User>`
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// src/infrastructure/llm/prompts.ts changes
|
||||||
|
|
||||||
|
// БЫЛО:
|
||||||
|
// - src/services/user.ts [fn: getUser, createUser]
|
||||||
|
|
||||||
|
// СТАНЕТ:
|
||||||
|
// ### src/services/user.ts
|
||||||
|
// - async getUser(id: string): Promise<User>
|
||||||
|
// - async createUser(data: UserDTO): Promise<User>
|
||||||
|
// - validateEmail(email: string): boolean
|
||||||
|
```
|
||||||
|
|
||||||
|
**Изменения:**
|
||||||
|
- [x] Расширить `FunctionInfo` в FileAST для хранения типов параметров и return type (already existed)
|
||||||
|
- [x] Обновить `ASTParser.ts` для извлечения типов параметров и return types (arrow functions fixed)
|
||||||
|
- [x] Обновить `formatFileSummary()` в prompts.ts для вывода сигнатур
|
||||||
|
- [x] Добавить опцию `includeSignatures: boolean` в config
|
||||||
|
|
||||||
|
**Зачем:** LLM не будет галлюцинировать параметры и return types.
|
||||||
|
|
||||||
|
### 0.24.2 - Interface/Type Field Definitions ⭐
|
||||||
|
|
||||||
|
**Проблема:** LLM видит только `interface: User, UserDTO`
|
||||||
|
**Решение:** Показать поля: `User { id: string, name: string, email: string }`
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// БЫЛО:
|
||||||
|
// - src/types/user.ts [interface: User, UserDTO]
|
||||||
|
|
||||||
|
// СТАНЕТ:
|
||||||
|
// ### src/types/user.ts
|
||||||
|
// - interface User { id: string, name: string, email: string, createdAt: Date }
|
||||||
|
// - interface UserDTO { name: string, email: string }
|
||||||
|
// - type UserId = string
|
||||||
|
```
|
||||||
|
|
||||||
|
**Изменения:**
|
||||||
|
- [ ] Расширить `InterfaceInfo` в FileAST для хранения полей с типами
|
||||||
|
- [ ] Обновить `ASTParser.ts` для извлечения полей интерфейсов
|
||||||
|
- [ ] Обновить `formatFileSummary()` для вывода полей
|
||||||
|
- [ ] Обработка type aliases с их определениями
|
||||||
|
|
||||||
|
**Зачем:** LLM знает структуру данных, не придумывает поля.
|
||||||
|
|
||||||
|
### 0.24.3 - Enum Value Definitions
|
||||||
|
|
||||||
|
**Проблема:** LLM видит только `type: Status`
|
||||||
|
**Решение:** Показать значения: `Status { Active=1, Inactive=0, Pending=2 }`
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// БЫЛО:
|
||||||
|
// - src/types/enums.ts [type: Status, Role]
|
||||||
|
|
||||||
|
// СТАНЕТ:
|
||||||
|
// ### src/types/enums.ts
|
||||||
|
// - enum Status { Active=1, Inactive=0, Pending=2 }
|
||||||
|
// - enum Role { Admin="admin", User="user" }
|
||||||
|
```
|
||||||
|
|
||||||
|
**Изменения:**
|
||||||
|
- [ ] Добавить `EnumInfo` в FileAST с members и values
|
||||||
|
- [ ] Обновить `ASTParser.ts` для извлечения enum members
|
||||||
|
- [ ] Обновить `formatFileSummary()` для вывода enum values
|
||||||
|
|
||||||
|
**Зачем:** LLM знает допустимые значения enum.
|
||||||
|
|
||||||
|
### 0.24.4 - Decorator Extraction
|
||||||
|
|
||||||
|
**Проблема:** LLM не видит декораторы (важно для NestJS, Angular)
|
||||||
|
**Решение:** Показать декораторы в контексте
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// СТАНЕТ:
|
||||||
|
// ### src/controllers/user.controller.ts
|
||||||
|
// - @Controller('users') class UserController
|
||||||
|
// - @Get(':id') async getUser(id: string): Promise<User>
|
||||||
|
// - @Post() @Body() async createUser(data: UserDTO): Promise<User>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Изменения:**
|
||||||
|
- [ ] Добавить `decorators: string[]` в FunctionInfo и ClassInfo
|
||||||
|
- [ ] Обновить `ASTParser.ts` для извлечения декораторов
|
||||||
|
- [ ] Обновить контекст для отображения декораторов
|
||||||
|
|
||||||
|
**Зачем:** LLM понимает роутинг, DI, guards в NestJS/Angular.
|
||||||
|
|
||||||
|
**Tests:**
|
||||||
|
- [ ] Unit tests for enhanced ASTParser
|
||||||
|
- [ ] Unit tests for new context format
|
||||||
|
- [ ] Integration tests for full flow
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Version 0.25.0 - Graph Metrics in Context 📊
|
||||||
|
|
||||||
|
**Priority:** MEDIUM
|
||||||
|
**Status:** Planned
|
||||||
|
|
||||||
|
Добавление графовых метрик в initial context: граф зависимостей, circular dependencies, impact score.
|
||||||
|
|
||||||
|
### 0.25.1 - Inline Dependency Graph
|
||||||
|
|
||||||
|
**Проблема:** LLM не видит связи между файлами без tool calls
|
||||||
|
**Решение:** Показать граф зависимостей в контексте
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Добавить в initial context:
|
||||||
|
|
||||||
|
// ## Dependency Graph
|
||||||
|
// src/services/user.ts: → types/user, utils/validation ← controllers/user, api/routes
|
||||||
|
// src/services/auth.ts: → services/user, utils/jwt ← controllers/auth
|
||||||
|
// src/utils/validation.ts: ← services/user, services/auth, controllers/*
|
||||||
|
```
|
||||||
|
|
||||||
|
**Изменения:**
|
||||||
|
- [ ] Добавить `formatDependencyGraph()` в prompts.ts
|
||||||
|
- [ ] Использовать данные из `FileMeta.dependencies` и `FileMeta.dependents`
|
||||||
|
- [ ] Группировать по hub files (много connections)
|
||||||
|
- [ ] Добавить опцию `includeDepsGraph: boolean` в config
|
||||||
|
|
||||||
|
**Зачем:** LLM видит архитектуру без tool call.
|
||||||
|
|
||||||
|
### 0.25.2 - Circular Dependencies in Context
|
||||||
|
|
||||||
|
**Проблема:** Circular deps вычисляются, но не показываются в контексте
|
||||||
|
**Решение:** Показать циклы сразу
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Добавить в initial context:
|
||||||
|
|
||||||
|
// ## ⚠️ Circular Dependencies
|
||||||
|
// - services/user → services/auth → services/user
|
||||||
|
// - utils/a → utils/b → utils/c → utils/a
|
||||||
|
```
|
||||||
|
|
||||||
|
**Изменения:**
|
||||||
|
- [ ] Добавить `formatCircularDeps()` в prompts.ts
|
||||||
|
- [ ] Получать circular deps из IndexBuilder
|
||||||
|
- [ ] Хранить в Redis как отдельный ключ или в meta
|
||||||
|
|
||||||
|
**Зачем:** LLM сразу видит проблемы архитектуры.
|
||||||
|
|
||||||
|
### 0.25.3 - Impact Score
|
||||||
|
|
||||||
|
**Проблема:** LLM не знает какие файлы критичные
|
||||||
|
**Решение:** Показать impact score (% кодовой базы, который зависит от файла)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Добавить в initial context:
|
||||||
|
|
||||||
|
// ## High Impact Files
|
||||||
|
// | File | Impact | Dependents |
|
||||||
|
// |------|--------|------------|
|
||||||
|
// | src/utils/validation.ts | 67% | 12 files |
|
||||||
|
// | src/types/user.ts | 45% | 8 files |
|
||||||
|
// | src/services/user.ts | 34% | 6 files |
|
||||||
|
```
|
||||||
|
|
||||||
|
**Изменения:**
|
||||||
|
- [ ] Добавить `impactScore: number` в FileMeta (0-100)
|
||||||
|
- [ ] Вычислять в MetaAnalyzer: (transitiveDepByCount / totalFiles) * 100
|
||||||
|
- [ ] Добавить `formatHighImpactFiles()` в prompts.ts
|
||||||
|
- [ ] Показывать top-10 high impact files
|
||||||
|
|
||||||
|
**Зачем:** LLM понимает какие файлы критичные для изменений.
|
||||||
|
|
||||||
|
### 0.25.4 - Transitive Dependencies Count
|
||||||
|
|
||||||
|
**Проблема:** Сейчас считаем только прямые зависимости
|
||||||
|
**Решение:** Добавить транзитивные зависимости в meta
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// FileMeta additions:
|
||||||
|
interface FileMeta {
|
||||||
|
// existing...
|
||||||
|
transitiveDepCount: number; // сколько файлов зависит от этого (транзитивно)
|
||||||
|
transitiveDepByCount: number; // от скольких файлов зависит этот (транзитивно)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Изменения:**
|
||||||
|
- [ ] Добавить `computeTransitiveDeps()` в MetaAnalyzer
|
||||||
|
- [ ] Использовать DFS с memoization для эффективности
|
||||||
|
- [ ] Сохранять в FileMeta
|
||||||
|
|
||||||
|
**Tests:**
|
||||||
|
- [ ] Unit tests for graph metrics computation
|
||||||
|
- [ ] Unit tests for new context sections
|
||||||
|
- [ ] Performance tests for large codebases
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -1803,10 +1995,12 @@ export interface ScanResult {
|
|||||||
- [x] Error handling complete ✅ (v0.16.0)
|
- [x] Error handling complete ✅ (v0.16.0)
|
||||||
- [ ] Performance optimized
|
- [ ] Performance optimized
|
||||||
- [x] Documentation complete ✅ (v0.17.0)
|
- [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] 0 ESLint errors ✅
|
||||||
- [x] Examples working ✅ (v0.18.0)
|
- [x] Examples working ✅ (v0.18.0)
|
||||||
- [x] CHANGELOG.md up to date ✅
|
- [x] CHANGELOG.md up to date ✅
|
||||||
|
- [ ] Rich initial context (v0.24.0) — function signatures, interface fields, enum values
|
||||||
|
- [ ] Graph metrics in context (v0.25.0) — dependency graph, circular deps, impact score
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -1875,11 +2069,17 @@ sessions:list # List<session_id>
|
|||||||
| Component | Tokens | % |
|
| Component | Tokens | % |
|
||||||
|-----------|--------|---|
|
|-----------|--------|---|
|
||||||
| System prompt | ~2,000 | 1.5% |
|
| System prompt | ~2,000 | 1.5% |
|
||||||
| Structure + AST | ~10,000 | 8% |
|
| Structure + AST (v0.23) | ~10,000 | 8% |
|
||||||
| **Available** | ~116,000 | 90% |
|
| Signatures + Types (v0.24) | ~5,000 | 4% |
|
||||||
|
| Graph Metrics (v0.25) | ~3,000 | 2.5% |
|
||||||
|
| **Total Initial Context** | ~20,000 | 16% |
|
||||||
|
| **Available for Chat** | ~108,000 | 84% |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
**Last Updated:** 2025-12-02
|
**Last Updated:** 2025-12-04
|
||||||
**Target Version:** 1.0.0
|
**Target Version:** 1.0.0
|
||||||
**Current Version:** 0.22.1
|
**Current Version:** 0.24.0
|
||||||
|
**Next Milestones:** v0.24.0 (Rich Context - 1/4 complete), v0.25.0 (Graph Metrics)
|
||||||
|
|
||||||
|
> **Note:** v0.24.0 and v0.25.0 are required for 1.0.0 release. They enable LLM to answer questions about types, signatures, and architecture without tool calls.
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@samiyev/ipuaro",
|
"name": "@samiyev/ipuaro",
|
||||||
"version": "0.22.2",
|
"version": "0.24.0",
|
||||||
"description": "Local AI agent for codebase operations with infinite context feeling",
|
"description": "Local AI agent for codebase operations with infinite context feeling",
|
||||||
"author": "Fozilbek Samiyev <fozilbek.samiyev@gmail.com>",
|
"author": "Fozilbek Samiyev <fozilbek.samiyev@gmail.com>",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@@ -44,7 +44,9 @@
|
|||||||
"simple-git": "^3.27.0",
|
"simple-git": "^3.27.0",
|
||||||
"tree-sitter": "^0.21.1",
|
"tree-sitter": "^0.21.1",
|
||||||
"tree-sitter-javascript": "^0.21.0",
|
"tree-sitter-javascript": "^0.21.0",
|
||||||
|
"tree-sitter-json": "^0.24.8",
|
||||||
"tree-sitter-typescript": "^0.21.2",
|
"tree-sitter-typescript": "^0.21.2",
|
||||||
|
"yaml": "^2.8.2",
|
||||||
"zod": "^3.23.8"
|
"zod": "^3.23.8"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import type { ContextState, Session } from "../../domain/entities/Session.js"
|
|||||||
import type { ILLMClient } from "../../domain/services/ILLMClient.js"
|
import type { ILLMClient } from "../../domain/services/ILLMClient.js"
|
||||||
import { type ChatMessage, createSystemMessage } from "../../domain/value-objects/ChatMessage.js"
|
import { type ChatMessage, createSystemMessage } from "../../domain/value-objects/ChatMessage.js"
|
||||||
import { CONTEXT_COMPRESSION_THRESHOLD, CONTEXT_WINDOW_SIZE } from "../../domain/constants/index.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.
|
* File in context with token count.
|
||||||
@@ -39,9 +40,13 @@ export class ContextManager {
|
|||||||
private readonly filesInContext = new Map<string, FileContext>()
|
private readonly filesInContext = new Map<string, FileContext>()
|
||||||
private currentTokens = 0
|
private currentTokens = 0
|
||||||
private readonly contextWindowSize: number
|
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.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.
|
* Check if compression is needed.
|
||||||
*/
|
*/
|
||||||
needsCompression(): boolean {
|
needsCompression(): boolean {
|
||||||
return this.getUsage() > CONTEXT_COMPRESSION_THRESHOLD
|
return this.getUsage() > this.compressionThreshold
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -70,6 +70,7 @@ export interface HandleMessageOptions {
|
|||||||
maxToolCalls?: number
|
maxToolCalls?: number
|
||||||
maxHistoryMessages?: number
|
maxHistoryMessages?: number
|
||||||
saveInputHistory?: boolean
|
saveInputHistory?: boolean
|
||||||
|
contextConfig?: import("../../shared/constants/config.js").ContextConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
const DEFAULT_MAX_TOOL_CALLS = 20
|
const DEFAULT_MAX_TOOL_CALLS = 20
|
||||||
@@ -98,13 +99,14 @@ export class HandleMessage {
|
|||||||
llm: ILLMClient,
|
llm: ILLMClient,
|
||||||
tools: IToolRegistry,
|
tools: IToolRegistry,
|
||||||
projectRoot: string,
|
projectRoot: string,
|
||||||
|
contextConfig?: import("../../shared/constants/config.js").ContextConfig,
|
||||||
) {
|
) {
|
||||||
this.storage = storage
|
this.storage = storage
|
||||||
this.sessionStorage = sessionStorage
|
this.sessionStorage = sessionStorage
|
||||||
this.llm = llm
|
this.llm = llm
|
||||||
this.tools = tools
|
this.tools = tools
|
||||||
this.projectRoot = projectRoot
|
this.projectRoot = projectRoot
|
||||||
this.contextManager = new ContextManager(llm.getContextWindowSize())
|
this.contextManager = new ContextManager(llm.getContextWindowSize(), contextConfig)
|
||||||
this.executeTool = new ExecuteTool(storage, sessionStorage, tools, projectRoot)
|
this.executeTool = new ExecuteTool(storage, sessionStorage, tools, projectRoot)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ export interface ScanResult {
|
|||||||
type: "file" | "directory" | "symlink"
|
type: "file" | "directory" | "symlink"
|
||||||
size: number
|
size: number
|
||||||
lastModified: number
|
lastModified: number
|
||||||
|
symlinkTarget?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -46,7 +47,7 @@ export interface IIndexer {
|
|||||||
/**
|
/**
|
||||||
* Parse file content into AST.
|
* 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.
|
* Analyze file and compute metadata.
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ import { builtinModules } from "node:module"
|
|||||||
import Parser from "tree-sitter"
|
import Parser from "tree-sitter"
|
||||||
import TypeScript from "tree-sitter-typescript"
|
import TypeScript from "tree-sitter-typescript"
|
||||||
import JavaScript from "tree-sitter-javascript"
|
import JavaScript from "tree-sitter-javascript"
|
||||||
|
import JSON from "tree-sitter-json"
|
||||||
|
import * as yamlParser from "yaml"
|
||||||
import {
|
import {
|
||||||
createEmptyFileAST,
|
createEmptyFileAST,
|
||||||
type ExportInfo,
|
type ExportInfo,
|
||||||
@@ -13,7 +15,7 @@ import {
|
|||||||
} from "../../domain/value-objects/FileAST.js"
|
} from "../../domain/value-objects/FileAST.js"
|
||||||
import { FieldName, NodeType } from "./tree-sitter-types.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
|
type SyntaxNode = Parser.SyntaxNode
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -39,12 +41,20 @@ export class ASTParser {
|
|||||||
jsParser.setLanguage(JavaScript)
|
jsParser.setLanguage(JavaScript)
|
||||||
this.parsers.set("js", jsParser)
|
this.parsers.set("js", jsParser)
|
||||||
this.parsers.set("jsx", 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 source code and extract AST information.
|
||||||
*/
|
*/
|
||||||
parse(content: string, language: Language): FileAST {
|
parse(content: string, language: Language): FileAST {
|
||||||
|
if (language === "yaml") {
|
||||||
|
return this.parseYAML(content)
|
||||||
|
}
|
||||||
|
|
||||||
const parser = this.parsers.get(language)
|
const parser = this.parsers.get(language)
|
||||||
if (!parser) {
|
if (!parser) {
|
||||||
return {
|
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 {
|
private extractAST(root: SyntaxNode, language: Language): FileAST {
|
||||||
const ast = createEmptyFileAST()
|
const ast = createEmptyFileAST()
|
||||||
|
|
||||||
|
if (language === "json") {
|
||||||
|
return this.extractJSONStructure(root, ast)
|
||||||
|
}
|
||||||
|
|
||||||
const isTypeScript = language === "ts" || language === "tsx"
|
const isTypeScript = language === "ts" || language === "tsx"
|
||||||
|
|
||||||
for (const child of root.children) {
|
for (const child of root.children) {
|
||||||
@@ -253,6 +332,7 @@ export class ASTParser {
|
|||||||
) {
|
) {
|
||||||
const params = this.extractParameters(valueNode)
|
const params = this.extractParameters(valueNode)
|
||||||
const isAsync = valueNode.children.some((c) => c.type === NodeType.ASYNC)
|
const isAsync = valueNode.children.some((c) => c.type === NodeType.ASYNC)
|
||||||
|
const returnTypeNode = valueNode.childForFieldName(FieldName.RETURN_TYPE)
|
||||||
|
|
||||||
ast.functions.push({
|
ast.functions.push({
|
||||||
name: nameNode?.text ?? "",
|
name: nameNode?.text ?? "",
|
||||||
@@ -261,6 +341,7 @@ export class ASTParser {
|
|||||||
params,
|
params,
|
||||||
isAsync,
|
isAsync,
|
||||||
isExported,
|
isExported,
|
||||||
|
returnType: returnTypeNode?.text?.replace(/^:\s*/, ""),
|
||||||
})
|
})
|
||||||
|
|
||||||
if (isExported) {
|
if (isExported) {
|
||||||
@@ -548,4 +629,37 @@ export class ASTParser {
|
|||||||
}
|
}
|
||||||
return text
|
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",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -96,12 +96,27 @@ export class FileScanner {
|
|||||||
const stats = await this.safeStats(fullPath)
|
const stats = await this.safeStats(fullPath)
|
||||||
|
|
||||||
if (stats) {
|
if (stats) {
|
||||||
yield {
|
const type = stats.isSymbolicLink()
|
||||||
|
? "symlink"
|
||||||
|
: stats.isDirectory()
|
||||||
|
? "directory"
|
||||||
|
: "file"
|
||||||
|
|
||||||
|
const result: ScanResult = {
|
||||||
path: relativePath,
|
path: relativePath,
|
||||||
type: "file",
|
type,
|
||||||
size: stats.size,
|
size: stats.size,
|
||||||
lastModified: stats.mtimeMs,
|
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.
|
* Safely get file stats without throwing.
|
||||||
|
* Uses lstat to get information about symlinks themselves.
|
||||||
*/
|
*/
|
||||||
private async safeStats(filePath: string): Promise<Stats | null> {
|
private async safeStats(filePath: string): Promise<Stats | null> {
|
||||||
try {
|
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 {
|
} catch {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,13 @@ export interface ProjectStructure {
|
|||||||
directories: string[]
|
directories: string[]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Options for building initial context.
|
||||||
|
*/
|
||||||
|
export interface BuildContextOptions {
|
||||||
|
includeSignatures?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* System prompt for the ipuaro AI agent.
|
* System prompt for the ipuaro AI agent.
|
||||||
*/
|
*/
|
||||||
@@ -116,12 +123,14 @@ export function buildInitialContext(
|
|||||||
structure: ProjectStructure,
|
structure: ProjectStructure,
|
||||||
asts: Map<string, FileAST>,
|
asts: Map<string, FileAST>,
|
||||||
metas?: Map<string, FileMeta>,
|
metas?: Map<string, FileMeta>,
|
||||||
|
options?: BuildContextOptions,
|
||||||
): string {
|
): string {
|
||||||
const sections: string[] = []
|
const sections: string[] = []
|
||||||
|
const includeSignatures = options?.includeSignatures ?? true
|
||||||
|
|
||||||
sections.push(formatProjectHeader(structure))
|
sections.push(formatProjectHeader(structure))
|
||||||
sections.push(formatDirectoryTree(structure))
|
sections.push(formatDirectoryTree(structure))
|
||||||
sections.push(formatFileOverview(asts, metas))
|
sections.push(formatFileOverview(asts, metas, includeSignatures))
|
||||||
|
|
||||||
return sections.join("\n\n")
|
return sections.join("\n\n")
|
||||||
}
|
}
|
||||||
@@ -157,7 +166,11 @@ function formatDirectoryTree(structure: ProjectStructure): string {
|
|||||||
/**
|
/**
|
||||||
* Format file overview with AST summaries.
|
* Format file overview with AST summaries.
|
||||||
*/
|
*/
|
||||||
function formatFileOverview(asts: Map<string, FileAST>, metas?: Map<string, FileMeta>): string {
|
function formatFileOverview(
|
||||||
|
asts: Map<string, FileAST>,
|
||||||
|
metas?: Map<string, FileMeta>,
|
||||||
|
includeSignatures = true,
|
||||||
|
): string {
|
||||||
const lines: string[] = ["## Files", ""]
|
const lines: string[] = ["## Files", ""]
|
||||||
|
|
||||||
const sortedPaths = [...asts.keys()].sort()
|
const sortedPaths = [...asts.keys()].sort()
|
||||||
@@ -168,16 +181,87 @@ function formatFileOverview(asts: Map<string, FileAST>, metas?: Map<string, File
|
|||||||
}
|
}
|
||||||
|
|
||||||
const meta = metas?.get(path)
|
const meta = metas?.get(path)
|
||||||
lines.push(formatFileSummary(path, ast, meta))
|
lines.push(formatFileSummary(path, ast, meta, includeSignatures))
|
||||||
}
|
}
|
||||||
|
|
||||||
return lines.join("\n")
|
return lines.join("\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Format a single file's AST summary.
|
* Format a function signature.
|
||||||
*/
|
*/
|
||||||
function formatFileSummary(path: string, ast: FileAST, meta?: FileMeta): string {
|
function formatFunctionSignature(fn: FileAST["functions"][0]): string {
|
||||||
|
const asyncPrefix = fn.isAsync ? "async " : ""
|
||||||
|
const params = fn.params
|
||||||
|
.map((p) => {
|
||||||
|
const optional = p.optional ? "?" : ""
|
||||||
|
const type = p.type ? `: ${p.type}` : ""
|
||||||
|
return `${p.name}${optional}${type}`
|
||||||
|
})
|
||||||
|
.join(", ")
|
||||||
|
const returnType = fn.returnType ? `: ${fn.returnType}` : ""
|
||||||
|
return `${asyncPrefix}${fn.name}(${params})${returnType}`
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format a single file's AST summary.
|
||||||
|
* When includeSignatures is true, shows full function signatures.
|
||||||
|
* When false, shows compact format with just names.
|
||||||
|
*/
|
||||||
|
function formatFileSummary(
|
||||||
|
path: string,
|
||||||
|
ast: FileAST,
|
||||||
|
meta?: FileMeta,
|
||||||
|
includeSignatures = true,
|
||||||
|
): string {
|
||||||
|
const flags = formatFileFlags(meta)
|
||||||
|
|
||||||
|
if (!includeSignatures) {
|
||||||
|
return formatFileSummaryCompact(path, ast, flags)
|
||||||
|
}
|
||||||
|
|
||||||
|
const lines: string[] = []
|
||||||
|
lines.push(`### ${path}${flags}`)
|
||||||
|
|
||||||
|
if (ast.functions.length > 0) {
|
||||||
|
for (const fn of ast.functions) {
|
||||||
|
lines.push(`- ${formatFunctionSignature(fn)}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ast.classes.length > 0) {
|
||||||
|
for (const cls of ast.classes) {
|
||||||
|
const ext = cls.extends ? ` extends ${cls.extends}` : ""
|
||||||
|
const impl = cls.implements.length > 0 ? ` implements ${cls.implements.join(", ")}` : ""
|
||||||
|
lines.push(`- class ${cls.name}${ext}${impl}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ast.interfaces.length > 0) {
|
||||||
|
for (const iface of ast.interfaces) {
|
||||||
|
const extList = iface.extends ?? []
|
||||||
|
const ext = extList.length > 0 ? ` extends ${extList.join(", ")}` : ""
|
||||||
|
lines.push(`- interface ${iface.name}${ext}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ast.typeAliases.length > 0) {
|
||||||
|
for (const type of ast.typeAliases) {
|
||||||
|
lines.push(`- type ${type.name}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lines.length === 1) {
|
||||||
|
return `- ${path}${flags}`
|
||||||
|
}
|
||||||
|
|
||||||
|
return lines.join("\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format file summary in compact mode (just names, no signatures).
|
||||||
|
*/
|
||||||
|
function formatFileSummaryCompact(path: string, ast: FileAST, flags: string): string {
|
||||||
const parts: string[] = []
|
const parts: string[] = []
|
||||||
|
|
||||||
if (ast.functions.length > 0) {
|
if (ast.functions.length > 0) {
|
||||||
@@ -201,8 +285,6 @@ function formatFileSummary(path: string, ast: FileAST, meta?: FileMeta): string
|
|||||||
}
|
}
|
||||||
|
|
||||||
const summary = parts.length > 0 ? ` [${parts.join(" | ")}]` : ""
|
const summary = parts.length > 0 ? ` [${parts.join(" | ")}]` : ""
|
||||||
const flags = formatFileFlags(meta)
|
|
||||||
|
|
||||||
return `- ${path}${summary}${flags}`
|
return `- ${path}${summary}${flags}`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
createSuccessResult,
|
createSuccessResult,
|
||||||
type ToolResult,
|
type ToolResult,
|
||||||
} from "../../../domain/value-objects/ToolResult.js"
|
} from "../../../domain/value-objects/ToolResult.js"
|
||||||
|
import type { CommandsConfig } from "../../../shared/constants/config.js"
|
||||||
import { CommandSecurity } from "./CommandSecurity.js"
|
import { CommandSecurity } from "./CommandSecurity.js"
|
||||||
|
|
||||||
const execAsync = promisify(exec)
|
const execAsync = promisify(exec)
|
||||||
@@ -60,7 +61,7 @@ export class RunCommandTool implements ITool {
|
|||||||
{
|
{
|
||||||
name: "timeout",
|
name: "timeout",
|
||||||
type: "number",
|
type: "number",
|
||||||
description: "Timeout in milliseconds (default: 30000)",
|
description: "Timeout in milliseconds (default: from config or 30000, max: 600000)",
|
||||||
required: false,
|
required: false,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
@@ -69,10 +70,12 @@ export class RunCommandTool implements ITool {
|
|||||||
|
|
||||||
private readonly security: CommandSecurity
|
private readonly security: CommandSecurity
|
||||||
private readonly execFn: typeof execAsync
|
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.security = security ?? new CommandSecurity()
|
||||||
this.execFn = execFn ?? execAsync
|
this.execFn = execFn ?? execAsync
|
||||||
|
this.configTimeout = config?.timeout ?? null
|
||||||
}
|
}
|
||||||
|
|
||||||
validateParams(params: Record<string, unknown>): string | null {
|
validateParams(params: Record<string, unknown>): string | null {
|
||||||
@@ -104,7 +107,7 @@ export class RunCommandTool implements ITool {
|
|||||||
const callId = `${this.name}-${String(startTime)}`
|
const callId = `${this.name}-${String(startTime)}`
|
||||||
|
|
||||||
const command = params.command as string
|
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)
|
const securityCheck = this.security.check(command)
|
||||||
|
|
||||||
|
|||||||
@@ -106,6 +106,33 @@ export const SessionConfigSchema = z.object({
|
|||||||
saveInputHistory: z.boolean().default(true),
|
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"),
|
||||||
|
includeSignatures: z.boolean().default(true),
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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.
|
* Full configuration schema.
|
||||||
*/
|
*/
|
||||||
@@ -119,6 +146,9 @@ export const ConfigSchema = z.object({
|
|||||||
input: InputConfigSchema.default({}),
|
input: InputConfigSchema.default({}),
|
||||||
display: DisplayConfigSchema.default({}),
|
display: DisplayConfigSchema.default({}),
|
||||||
session: SessionConfigSchema.default({}),
|
session: SessionConfigSchema.default({}),
|
||||||
|
context: ContextConfigSchema.default({}),
|
||||||
|
autocomplete: AutocompleteConfigSchema.default({}),
|
||||||
|
commands: CommandsConfigSchema.default({}),
|
||||||
})
|
})
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -134,6 +164,9 @@ export type EditConfig = z.infer<typeof EditConfigSchema>
|
|||||||
export type InputConfig = z.infer<typeof InputConfigSchema>
|
export type InputConfig = z.infer<typeof InputConfigSchema>
|
||||||
export type DisplayConfig = z.infer<typeof DisplayConfigSchema>
|
export type DisplayConfig = z.infer<typeof DisplayConfigSchema>
|
||||||
export type SessionConfig = z.infer<typeof SessionConfigSchema>
|
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.
|
* Default configuration.
|
||||||
|
|||||||
@@ -120,9 +120,7 @@ function AssistantMessage({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function ToolMessage({ message, theme }: MessageComponentProps): React.JSX.Element {
|
function ToolMessage({ message }: MessageComponentProps): React.JSX.Element {
|
||||||
const roleColor = getRoleColor("tool", theme)
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box flexDirection="column" marginBottom={1} marginLeft={2}>
|
<Box flexDirection="column" marginBottom={1} marginLeft={2}>
|
||||||
{message.toolResults?.map((result) => (
|
{message.toolResults?.map((result) => (
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
|
|
||||||
import { useCallback, useEffect, useState } from "react"
|
import { useCallback, useEffect, useState } from "react"
|
||||||
import type { IStorage } from "../../domain/services/IStorage.js"
|
import type { IStorage } from "../../domain/services/IStorage.js"
|
||||||
|
import type { AutocompleteConfig } from "../../shared/constants/config.js"
|
||||||
import path from "node:path"
|
import path from "node:path"
|
||||||
|
|
||||||
export interface UseAutocompleteOptions {
|
export interface UseAutocompleteOptions {
|
||||||
@@ -12,6 +13,7 @@ export interface UseAutocompleteOptions {
|
|||||||
projectRoot: string
|
projectRoot: string
|
||||||
enabled?: boolean
|
enabled?: boolean
|
||||||
maxSuggestions?: number
|
maxSuggestions?: number
|
||||||
|
config?: AutocompleteConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UseAutocompleteReturn {
|
export interface UseAutocompleteReturn {
|
||||||
@@ -107,13 +109,18 @@ function getCommonPrefix(suggestions: string[]): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function useAutocomplete(options: UseAutocompleteOptions): UseAutocompleteReturn {
|
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 [filePaths, setFilePaths] = useState<string[]>([])
|
||||||
const [suggestions, setSuggestions] = useState<string[]>([])
|
const [suggestions, setSuggestions] = useState<string[]>([])
|
||||||
|
|
||||||
// Load file paths from storage
|
// Load file paths from storage
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!enabled) {
|
if (!isEnabled) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -135,11 +142,11 @@ export function useAutocomplete(options: UseAutocompleteOptions): UseAutocomplet
|
|||||||
loadPaths().catch(() => {
|
loadPaths().catch(() => {
|
||||||
// Ignore errors
|
// Ignore errors
|
||||||
})
|
})
|
||||||
}, [storage, projectRoot, enabled])
|
}, [storage, projectRoot, isEnabled])
|
||||||
|
|
||||||
const complete = useCallback(
|
const complete = useCallback(
|
||||||
(partial: string): string[] => {
|
(partial: string): string[] => {
|
||||||
if (!enabled || !partial.trim()) {
|
if (!isEnabled || !partial.trim()) {
|
||||||
setSuggestions([])
|
setSuggestions([])
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
@@ -154,13 +161,13 @@ export function useAutocomplete(options: UseAutocompleteOptions): UseAutocomplet
|
|||||||
}))
|
}))
|
||||||
.filter((item) => item.score > 0)
|
.filter((item) => item.score > 0)
|
||||||
.sort((a, b) => b.score - a.score)
|
.sort((a, b) => b.score - a.score)
|
||||||
.slice(0, maxSuggestions)
|
.slice(0, maxSuggestionsCount)
|
||||||
.map((item) => item.path)
|
.map((item) => item.path)
|
||||||
|
|
||||||
setSuggestions(scored)
|
setSuggestions(scored)
|
||||||
return scored
|
return scored
|
||||||
},
|
},
|
||||||
[enabled, filePaths, maxSuggestions],
|
[isEnabled, filePaths, maxSuggestionsCount],
|
||||||
)
|
)
|
||||||
|
|
||||||
const accept = useCallback(
|
const accept = useCallback(
|
||||||
|
|||||||
@@ -109,6 +109,7 @@ async function initializeSession(
|
|||||||
deps.llm,
|
deps.llm,
|
||||||
deps.tools,
|
deps.tools,
|
||||||
deps.projectRoot,
|
deps.projectRoot,
|
||||||
|
deps.config?.context,
|
||||||
)
|
)
|
||||||
if (deps.projectStructure) {
|
if (deps.projectStructure) {
|
||||||
handleMessage.setProjectStructure(deps.projectStructure)
|
handleMessage.setProjectStructure(deps.projectStructure)
|
||||||
@@ -117,6 +118,7 @@ async function initializeSession(
|
|||||||
autoApply: options.autoApply,
|
autoApply: options.autoApply,
|
||||||
maxHistoryMessages: deps.config?.session.maxHistoryMessages,
|
maxHistoryMessages: deps.config?.session.maxHistoryMessages,
|
||||||
saveInputHistory: deps.config?.session.saveInputHistory,
|
saveInputHistory: deps.config?.session.saveInputHistory,
|
||||||
|
contextConfig: deps.config?.context,
|
||||||
})
|
})
|
||||||
handleMessage.setEvents(createEventHandlers(setters, options))
|
handleMessage.setEvents(createEventHandlers(setters, options))
|
||||||
refs.current.handleMessage = handleMessage
|
refs.current.handleMessage = handleMessage
|
||||||
|
|||||||
@@ -245,4 +245,65 @@ describe("ContextManager", () => {
|
|||||||
expect(state.needsCompression).toBe(false)
|
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)
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -404,4 +404,106 @@ function mix(
|
|||||||
expect(ast.exports.length).toBeGreaterThanOrEqual(4)
|
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)
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -108,13 +108,23 @@ describe("prompts", () => {
|
|||||||
expect(context).toContain("tests/")
|
expect(context).toContain("tests/")
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should include file overview with AST summaries", () => {
|
it("should include file overview with AST summaries (signatures format)", () => {
|
||||||
const context = buildInitialContext(structure, asts)
|
const context = buildInitialContext(structure, asts)
|
||||||
|
|
||||||
expect(context).toContain("## Files")
|
expect(context).toContain("## Files")
|
||||||
expect(context).toContain("src/index.ts")
|
expect(context).toContain("### src/index.ts")
|
||||||
|
expect(context).toContain("- main()")
|
||||||
|
expect(context).toContain("### src/utils.ts")
|
||||||
|
expect(context).toContain("- class Helper")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should use compact format when includeSignatures is false", () => {
|
||||||
|
const context = buildInitialContext(structure, asts, undefined, {
|
||||||
|
includeSignatures: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(context).toContain("## Files")
|
||||||
expect(context).toContain("fn: main")
|
expect(context).toContain("fn: main")
|
||||||
expect(context).toContain("src/utils.ts")
|
|
||||||
expect(context).toContain("class: Helper")
|
expect(context).toContain("class: Helper")
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -506,7 +516,16 @@ describe("prompts", () => {
|
|||||||
exports: [],
|
exports: [],
|
||||||
functions: [],
|
functions: [],
|
||||||
classes: [],
|
classes: [],
|
||||||
interfaces: [{ name: "IFoo", lineStart: 1, lineEnd: 5, isExported: true }],
|
interfaces: [
|
||||||
|
{
|
||||||
|
name: "IFoo",
|
||||||
|
lineStart: 1,
|
||||||
|
lineEnd: 5,
|
||||||
|
properties: [],
|
||||||
|
extends: [],
|
||||||
|
isExported: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
typeAliases: [],
|
typeAliases: [],
|
||||||
parseError: false,
|
parseError: false,
|
||||||
},
|
},
|
||||||
@@ -515,6 +534,44 @@ describe("prompts", () => {
|
|||||||
|
|
||||||
const context = buildInitialContext(structure, asts)
|
const context = buildInitialContext(structure, asts)
|
||||||
|
|
||||||
|
expect(context).toContain("- interface IFoo")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should handle file with only interfaces (compact format)", () => {
|
||||||
|
const structure: ProjectStructure = {
|
||||||
|
name: "test",
|
||||||
|
rootPath: "/test",
|
||||||
|
files: ["types.ts"],
|
||||||
|
directories: [],
|
||||||
|
}
|
||||||
|
const asts = new Map<string, FileAST>([
|
||||||
|
[
|
||||||
|
"types.ts",
|
||||||
|
{
|
||||||
|
imports: [],
|
||||||
|
exports: [],
|
||||||
|
functions: [],
|
||||||
|
classes: [],
|
||||||
|
interfaces: [
|
||||||
|
{
|
||||||
|
name: "IFoo",
|
||||||
|
lineStart: 1,
|
||||||
|
lineEnd: 5,
|
||||||
|
properties: [],
|
||||||
|
extends: [],
|
||||||
|
isExported: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
typeAliases: [],
|
||||||
|
parseError: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
])
|
||||||
|
|
||||||
|
const context = buildInitialContext(structure, asts, undefined, {
|
||||||
|
includeSignatures: false,
|
||||||
|
})
|
||||||
|
|
||||||
expect(context).toContain("interface: IFoo")
|
expect(context).toContain("interface: IFoo")
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -534,9 +591,7 @@ describe("prompts", () => {
|
|||||||
functions: [],
|
functions: [],
|
||||||
classes: [],
|
classes: [],
|
||||||
interfaces: [],
|
interfaces: [],
|
||||||
typeAliases: [
|
typeAliases: [{ name: "MyType", line: 1, isExported: true }],
|
||||||
{ name: "MyType", lineStart: 1, lineEnd: 1, isExported: true },
|
|
||||||
],
|
|
||||||
parseError: false,
|
parseError: false,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -544,6 +599,35 @@ describe("prompts", () => {
|
|||||||
|
|
||||||
const context = buildInitialContext(structure, asts)
|
const context = buildInitialContext(structure, asts)
|
||||||
|
|
||||||
|
expect(context).toContain("- type MyType")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should handle file with only type aliases (compact format)", () => {
|
||||||
|
const structure: ProjectStructure = {
|
||||||
|
name: "test",
|
||||||
|
rootPath: "/test",
|
||||||
|
files: ["types.ts"],
|
||||||
|
directories: [],
|
||||||
|
}
|
||||||
|
const asts = new Map<string, FileAST>([
|
||||||
|
[
|
||||||
|
"types.ts",
|
||||||
|
{
|
||||||
|
imports: [],
|
||||||
|
exports: [],
|
||||||
|
functions: [],
|
||||||
|
classes: [],
|
||||||
|
interfaces: [],
|
||||||
|
typeAliases: [{ name: "MyType", line: 1, isExported: true }],
|
||||||
|
parseError: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
])
|
||||||
|
|
||||||
|
const context = buildInitialContext(structure, asts, undefined, {
|
||||||
|
includeSignatures: false,
|
||||||
|
})
|
||||||
|
|
||||||
expect(context).toContain("type: MyType")
|
expect(context).toContain("type: MyType")
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -686,6 +770,22 @@ describe("prompts", () => {
|
|||||||
expect(context).toContain("exists.ts")
|
expect(context).toContain("exists.ts")
|
||||||
expect(context).not.toContain("missing.ts")
|
expect(context).not.toContain("missing.ts")
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it("should skip undefined AST entries", () => {
|
||||||
|
const structure: ProjectStructure = {
|
||||||
|
name: "test",
|
||||||
|
rootPath: "/test",
|
||||||
|
files: ["file.ts"],
|
||||||
|
directories: [],
|
||||||
|
}
|
||||||
|
const asts = new Map<string, FileAST>()
|
||||||
|
asts.set("file.ts", undefined as unknown as FileAST)
|
||||||
|
|
||||||
|
const context = buildInitialContext(structure, asts)
|
||||||
|
|
||||||
|
expect(context).toContain("## Files")
|
||||||
|
expect(context).not.toContain("file.ts")
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe("truncateContext", () => {
|
describe("truncateContext", () => {
|
||||||
@@ -714,4 +814,276 @@ describe("prompts", () => {
|
|||||||
expect(result).toContain("truncated")
|
expect(result).toContain("truncated")
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe("function signatures with types", () => {
|
||||||
|
it("should format function with typed parameters", () => {
|
||||||
|
const structure: ProjectStructure = {
|
||||||
|
name: "test",
|
||||||
|
rootPath: "/test",
|
||||||
|
files: ["user.ts"],
|
||||||
|
directories: [],
|
||||||
|
}
|
||||||
|
const asts = new Map<string, FileAST>([
|
||||||
|
[
|
||||||
|
"user.ts",
|
||||||
|
{
|
||||||
|
imports: [],
|
||||||
|
exports: [],
|
||||||
|
functions: [
|
||||||
|
{
|
||||||
|
name: "getUser",
|
||||||
|
lineStart: 1,
|
||||||
|
lineEnd: 5,
|
||||||
|
params: [
|
||||||
|
{
|
||||||
|
name: "id",
|
||||||
|
type: "string",
|
||||||
|
optional: false,
|
||||||
|
hasDefault: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
isAsync: false,
|
||||||
|
isExported: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
classes: [],
|
||||||
|
interfaces: [],
|
||||||
|
typeAliases: [],
|
||||||
|
parseError: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
])
|
||||||
|
|
||||||
|
const context = buildInitialContext(structure, asts)
|
||||||
|
|
||||||
|
expect(context).toContain("- getUser(id: string)")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should format async function with return type", () => {
|
||||||
|
const structure: ProjectStructure = {
|
||||||
|
name: "test",
|
||||||
|
rootPath: "/test",
|
||||||
|
files: ["user.ts"],
|
||||||
|
directories: [],
|
||||||
|
}
|
||||||
|
const asts = new Map<string, FileAST>([
|
||||||
|
[
|
||||||
|
"user.ts",
|
||||||
|
{
|
||||||
|
imports: [],
|
||||||
|
exports: [],
|
||||||
|
functions: [
|
||||||
|
{
|
||||||
|
name: "getUser",
|
||||||
|
lineStart: 1,
|
||||||
|
lineEnd: 5,
|
||||||
|
params: [
|
||||||
|
{
|
||||||
|
name: "id",
|
||||||
|
type: "string",
|
||||||
|
optional: false,
|
||||||
|
hasDefault: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
isAsync: true,
|
||||||
|
isExported: true,
|
||||||
|
returnType: "Promise<User>",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
classes: [],
|
||||||
|
interfaces: [],
|
||||||
|
typeAliases: [],
|
||||||
|
parseError: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
])
|
||||||
|
|
||||||
|
const context = buildInitialContext(structure, asts)
|
||||||
|
|
||||||
|
expect(context).toContain("- async getUser(id: string): Promise<User>")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should format function with optional parameters", () => {
|
||||||
|
const structure: ProjectStructure = {
|
||||||
|
name: "test",
|
||||||
|
rootPath: "/test",
|
||||||
|
files: ["utils.ts"],
|
||||||
|
directories: [],
|
||||||
|
}
|
||||||
|
const asts = new Map<string, FileAST>([
|
||||||
|
[
|
||||||
|
"utils.ts",
|
||||||
|
{
|
||||||
|
imports: [],
|
||||||
|
exports: [],
|
||||||
|
functions: [
|
||||||
|
{
|
||||||
|
name: "format",
|
||||||
|
lineStart: 1,
|
||||||
|
lineEnd: 5,
|
||||||
|
params: [
|
||||||
|
{
|
||||||
|
name: "value",
|
||||||
|
type: "string",
|
||||||
|
optional: false,
|
||||||
|
hasDefault: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "options",
|
||||||
|
type: "FormatOptions",
|
||||||
|
optional: true,
|
||||||
|
hasDefault: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
isAsync: false,
|
||||||
|
isExported: true,
|
||||||
|
returnType: "string",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
classes: [],
|
||||||
|
interfaces: [],
|
||||||
|
typeAliases: [],
|
||||||
|
parseError: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
])
|
||||||
|
|
||||||
|
const context = buildInitialContext(structure, asts)
|
||||||
|
|
||||||
|
expect(context).toContain("- format(value: string, options?: FormatOptions): string")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should format function with multiple typed parameters", () => {
|
||||||
|
const structure: ProjectStructure = {
|
||||||
|
name: "test",
|
||||||
|
rootPath: "/test",
|
||||||
|
files: ["api.ts"],
|
||||||
|
directories: [],
|
||||||
|
}
|
||||||
|
const asts = new Map<string, FileAST>([
|
||||||
|
[
|
||||||
|
"api.ts",
|
||||||
|
{
|
||||||
|
imports: [],
|
||||||
|
exports: [],
|
||||||
|
functions: [
|
||||||
|
{
|
||||||
|
name: "createUser",
|
||||||
|
lineStart: 1,
|
||||||
|
lineEnd: 10,
|
||||||
|
params: [
|
||||||
|
{
|
||||||
|
name: "name",
|
||||||
|
type: "string",
|
||||||
|
optional: false,
|
||||||
|
hasDefault: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "email",
|
||||||
|
type: "string",
|
||||||
|
optional: false,
|
||||||
|
hasDefault: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "age",
|
||||||
|
type: "number",
|
||||||
|
optional: true,
|
||||||
|
hasDefault: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
isAsync: true,
|
||||||
|
isExported: true,
|
||||||
|
returnType: "Promise<User>",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
classes: [],
|
||||||
|
interfaces: [],
|
||||||
|
typeAliases: [],
|
||||||
|
parseError: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
])
|
||||||
|
|
||||||
|
const context = buildInitialContext(structure, asts)
|
||||||
|
|
||||||
|
expect(context).toContain(
|
||||||
|
"- async createUser(name: string, email: string, age?: number): Promise<User>",
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should format function without types (JavaScript style)", () => {
|
||||||
|
const structure: ProjectStructure = {
|
||||||
|
name: "test",
|
||||||
|
rootPath: "/test",
|
||||||
|
files: ["legacy.js"],
|
||||||
|
directories: [],
|
||||||
|
}
|
||||||
|
const asts = new Map<string, FileAST>([
|
||||||
|
[
|
||||||
|
"legacy.js",
|
||||||
|
{
|
||||||
|
imports: [],
|
||||||
|
exports: [],
|
||||||
|
functions: [
|
||||||
|
{
|
||||||
|
name: "doSomething",
|
||||||
|
lineStart: 1,
|
||||||
|
lineEnd: 5,
|
||||||
|
params: [
|
||||||
|
{ name: "x", optional: false, hasDefault: false },
|
||||||
|
{ name: "y", optional: false, hasDefault: false },
|
||||||
|
],
|
||||||
|
isAsync: false,
|
||||||
|
isExported: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
classes: [],
|
||||||
|
interfaces: [],
|
||||||
|
typeAliases: [],
|
||||||
|
parseError: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
])
|
||||||
|
|
||||||
|
const context = buildInitialContext(structure, asts)
|
||||||
|
|
||||||
|
expect(context).toContain("- doSomething(x, y)")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should format interface with extends", () => {
|
||||||
|
const structure: ProjectStructure = {
|
||||||
|
name: "test",
|
||||||
|
rootPath: "/test",
|
||||||
|
files: ["types.ts"],
|
||||||
|
directories: [],
|
||||||
|
}
|
||||||
|
const asts = new Map<string, FileAST>([
|
||||||
|
[
|
||||||
|
"types.ts",
|
||||||
|
{
|
||||||
|
imports: [],
|
||||||
|
exports: [],
|
||||||
|
functions: [],
|
||||||
|
classes: [],
|
||||||
|
interfaces: [
|
||||||
|
{
|
||||||
|
name: "AdminUser",
|
||||||
|
lineStart: 1,
|
||||||
|
lineEnd: 5,
|
||||||
|
properties: [],
|
||||||
|
extends: ["User", "Admin"],
|
||||||
|
isExported: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
typeAliases: [],
|
||||||
|
parseError: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
])
|
||||||
|
|
||||||
|
const context = buildInitialContext(structure, asts)
|
||||||
|
|
||||||
|
expect(context).toContain("- interface AdminUser extends User, Admin")
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -354,6 +354,36 @@ describe("RunCommandTool", () => {
|
|||||||
expect(execFn).toHaveBeenCalledWith("ls", expect.objectContaining({ timeout: 5000 }))
|
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 () => {
|
it("should execute in project root", async () => {
|
||||||
const execFn = createMockExec({})
|
const execFn = createMockExec({})
|
||||||
const toolWithMock = new RunCommandTool(undefined, execFn)
|
const toolWithMock = new RunCommandTool(undefined, execFn)
|
||||||
|
|||||||
204
packages/ipuaro/tests/unit/shared/autocomplete-config.test.ts
Normal file
204
packages/ipuaro/tests/unit/shared/autocomplete-config.test.ts
Normal 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)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
137
packages/ipuaro/tests/unit/shared/commands-config.test.ts
Normal file
137
packages/ipuaro/tests/unit/shared/commands-config.test.ts
Normal 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)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
253
packages/ipuaro/tests/unit/shared/context-config.test.ts
Normal file
253
packages/ipuaro/tests/unit/shared/context-config.test.ts
Normal file
@@ -0,0 +1,253 @@
|
|||||||
|
/**
|
||||||
|
* 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",
|
||||||
|
includeSignatures: true,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
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",
|
||||||
|
includeSignatures: true,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
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",
|
||||||
|
includeSignatures: true,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
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",
|
||||||
|
includeSignatures: true,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
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",
|
||||||
|
includeSignatures: true,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("full config", () => {
|
||||||
|
it("should accept valid full config", () => {
|
||||||
|
const config = {
|
||||||
|
systemPromptTokens: 3000,
|
||||||
|
maxContextUsage: 0.9,
|
||||||
|
autoCompressAt: 0.85,
|
||||||
|
compressionMethod: "truncate" as const,
|
||||||
|
includeSignatures: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
includeSignatures: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = ContextConfigSchema.parse(config)
|
||||||
|
expect(result).toEqual(config)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("includeSignatures", () => {
|
||||||
|
it("should accept true", () => {
|
||||||
|
const result = ContextConfigSchema.parse({ includeSignatures: true })
|
||||||
|
expect(result.includeSignatures).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should accept false", () => {
|
||||||
|
const result = ContextConfigSchema.parse({ includeSignatures: false })
|
||||||
|
expect(result.includeSignatures).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should default to true", () => {
|
||||||
|
const result = ContextConfigSchema.parse({})
|
||||||
|
expect(result.includeSignatures).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should reject non-boolean", () => {
|
||||||
|
expect(() => ContextConfigSchema.parse({ includeSignatures: "true" })).toThrow()
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should reject number", () => {
|
||||||
|
expect(() => ContextConfigSchema.parse({ includeSignatures: 1 })).toThrow()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -24,7 +24,7 @@ export default defineConfig({
|
|||||||
thresholds: {
|
thresholds: {
|
||||||
lines: 95,
|
lines: 95,
|
||||||
functions: 95,
|
functions: 95,
|
||||||
branches: 91.3,
|
branches: 91,
|
||||||
statements: 95,
|
statements: 95,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
56
pnpm-lock.yaml
generated
56
pnpm-lock.yaml
generated
@@ -131,7 +131,7 @@ importers:
|
|||||||
version: 5.9.3
|
version: 5.9.3
|
||||||
vitest:
|
vitest:
|
||||||
specifier: ^4.0.10
|
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:
|
packages/ipuaro:
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -168,9 +168,15 @@ importers:
|
|||||||
tree-sitter-javascript:
|
tree-sitter-javascript:
|
||||||
specifier: ^0.21.0
|
specifier: ^0.21.0
|
||||||
version: 0.21.4(tree-sitter@0.21.1)
|
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:
|
tree-sitter-typescript:
|
||||||
specifier: ^0.21.2
|
specifier: ^0.21.2
|
||||||
version: 0.21.2(tree-sitter@0.21.1)
|
version: 0.21.2(tree-sitter@0.21.1)
|
||||||
|
yaml:
|
||||||
|
specifier: ^2.8.2
|
||||||
|
version: 2.8.2
|
||||||
zod:
|
zod:
|
||||||
specifier: ^3.23.8
|
specifier: ^3.23.8
|
||||||
version: 3.25.76
|
version: 3.25.76
|
||||||
@@ -201,7 +207,7 @@ importers:
|
|||||||
version: 18.3.1(react@18.3.1)
|
version: 18.3.1(react@18.3.1)
|
||||||
tsup:
|
tsup:
|
||||||
specifier: ^8.3.5
|
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:
|
typescript:
|
||||||
specifier: ^5.7.2
|
specifier: ^5.7.2
|
||||||
version: 5.9.3
|
version: 5.9.3
|
||||||
@@ -4172,6 +4178,14 @@ packages:
|
|||||||
tree-sitter:
|
tree-sitter:
|
||||||
optional: true
|
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:
|
tree-sitter-typescript@0.21.2:
|
||||||
resolution: {integrity: sha512-/RyNK41ZpkA8PuPZimR6pGLvNR1p0ibRUJwwQn4qAjyyLEIQD/BNlwS3NSxWtGsAWZe9gZ44VK1mWx2+eQVldg==}
|
resolution: {integrity: sha512-/RyNK41ZpkA8PuPZimR6pGLvNR1p0ibRUJwwQn4qAjyyLEIQD/BNlwS3NSxWtGsAWZe9gZ44VK1mWx2+eQVldg==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -4636,6 +4650,11 @@ packages:
|
|||||||
yallist@3.1.1:
|
yallist@3.1.1:
|
||||||
resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==}
|
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:
|
yargs-parser@21.1.1:
|
||||||
resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==}
|
resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==}
|
||||||
engines: {node: '>=12'}
|
engines: {node: '>=12'}
|
||||||
@@ -6325,7 +6344,7 @@ snapshots:
|
|||||||
magicast: 0.5.1
|
magicast: 0.5.1
|
||||||
std-env: 3.10.0
|
std-env: 3.10.0
|
||||||
tinyrainbow: 3.0.3
|
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:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
@@ -6344,13 +6363,13 @@ snapshots:
|
|||||||
chai: 6.2.1
|
chai: 6.2.1
|
||||||
tinyrainbow: 3.0.3
|
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:
|
dependencies:
|
||||||
'@vitest/spy': 4.0.13
|
'@vitest/spy': 4.0.13
|
||||||
estree-walker: 3.0.3
|
estree-walker: 3.0.3
|
||||||
magic-string: 0.30.21
|
magic-string: 0.30.21
|
||||||
optionalDependencies:
|
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':
|
'@vitest/pretty-format@4.0.13':
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -6405,7 +6424,7 @@ snapshots:
|
|||||||
sirv: 3.0.2
|
sirv: 3.0.2
|
||||||
tinyglobby: 0.2.15
|
tinyglobby: 0.2.15
|
||||||
tinyrainbow: 3.0.3
|
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':
|
'@vitest/utils@1.6.1':
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -8392,12 +8411,13 @@ snapshots:
|
|||||||
|
|
||||||
pluralize@8.0.0: {}
|
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:
|
dependencies:
|
||||||
lilconfig: 3.1.3
|
lilconfig: 3.1.3
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
postcss: 8.5.6
|
postcss: 8.5.6
|
||||||
tsx: 4.20.6
|
tsx: 4.20.6
|
||||||
|
yaml: 2.8.2
|
||||||
|
|
||||||
postcss@8.5.6:
|
postcss@8.5.6:
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -8911,6 +8931,13 @@ snapshots:
|
|||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
tree-sitter: 0.21.1
|
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):
|
tree-sitter-typescript@0.21.2(tree-sitter@0.21.1):
|
||||||
dependencies:
|
dependencies:
|
||||||
node-addon-api: 8.5.0
|
node-addon-api: 8.5.0
|
||||||
@@ -8999,7 +9026,7 @@ snapshots:
|
|||||||
|
|
||||||
tslib@2.8.1: {}
|
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:
|
dependencies:
|
||||||
bundle-require: 5.1.0(esbuild@0.27.0)
|
bundle-require: 5.1.0(esbuild@0.27.0)
|
||||||
cac: 6.7.14
|
cac: 6.7.14
|
||||||
@@ -9010,7 +9037,7 @@ snapshots:
|
|||||||
fix-dts-default-cjs-exports: 1.0.1
|
fix-dts-default-cjs-exports: 1.0.1
|
||||||
joycon: 3.1.1
|
joycon: 3.1.1
|
||||||
picocolors: 1.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
|
resolve-from: 5.0.0
|
||||||
rollup: 4.53.3
|
rollup: 4.53.3
|
||||||
source-map: 0.7.6
|
source-map: 0.7.6
|
||||||
@@ -9156,7 +9183,7 @@ snapshots:
|
|||||||
fsevents: 2.3.3
|
fsevents: 2.3.3
|
||||||
terser: 5.44.1
|
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:
|
dependencies:
|
||||||
esbuild: 0.25.12
|
esbuild: 0.25.12
|
||||||
fdir: 6.5.0(picomatch@4.0.3)
|
fdir: 6.5.0(picomatch@4.0.3)
|
||||||
@@ -9169,6 +9196,7 @@ snapshots:
|
|||||||
fsevents: 2.3.3
|
fsevents: 2.3.3
|
||||||
terser: 5.44.1
|
terser: 5.44.1
|
||||||
tsx: 4.20.6
|
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):
|
vitest@1.6.1(@types/node@22.19.1)(@vitest/ui@1.6.1)(jsdom@27.2.0)(terser@5.44.1):
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -9206,10 +9234,10 @@ snapshots:
|
|||||||
- supports-color
|
- supports-color
|
||||||
- terser
|
- 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:
|
dependencies:
|
||||||
'@vitest/expect': 4.0.13
|
'@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/pretty-format': 4.0.13
|
||||||
'@vitest/runner': 4.0.13
|
'@vitest/runner': 4.0.13
|
||||||
'@vitest/snapshot': 4.0.13
|
'@vitest/snapshot': 4.0.13
|
||||||
@@ -9226,7 +9254,7 @@ snapshots:
|
|||||||
tinyexec: 0.3.2
|
tinyexec: 0.3.2
|
||||||
tinyglobby: 0.2.15
|
tinyglobby: 0.2.15
|
||||||
tinyrainbow: 3.0.3
|
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
|
why-is-node-running: 2.3.0
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@types/node': 22.19.1
|
'@types/node': 22.19.1
|
||||||
@@ -9366,6 +9394,8 @@ snapshots:
|
|||||||
|
|
||||||
yallist@3.1.1: {}
|
yallist@3.1.1: {}
|
||||||
|
|
||||||
|
yaml@2.8.2: {}
|
||||||
|
|
||||||
yargs-parser@21.1.1: {}
|
yargs-parser@21.1.1: {}
|
||||||
|
|
||||||
yargs@17.7.2:
|
yargs@17.7.2:
|
||||||
|
|||||||
Reference in New Issue
Block a user