mirror of
https://github.com/samiyev/puaros.git
synced 2025-12-27 23:06:54 +05:00
Compare commits
20 Commits
ipuaro-v0.
...
ipuaro-v0.
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c82006bbda | ||
|
|
2e84472e49 | ||
|
|
17d75dbd54 | ||
|
|
fac5966678 | ||
|
|
92ba3fd9ba | ||
|
|
e9aaa708fe | ||
|
|
d6d15dd271 | ||
|
|
d63d85d850 | ||
|
|
41cfc21f20 | ||
|
|
eeaa223436 | ||
|
|
36768c06d1 | ||
|
|
5a22cd5c9b | ||
|
|
806c9281b0 | ||
|
|
12197a9624 | ||
|
|
1489b69e69 | ||
|
|
2dcb22812c | ||
|
|
7d7c99fe4d | ||
|
|
a3f0ba948f | ||
|
|
141888bf59 | ||
|
|
b0f1778f3a |
@@ -20,6 +20,21 @@ This document provides authoritative sources, academic papers, industry standard
|
||||
12. [Aggregate Boundary Validation (DDD Tactical Patterns)](#12-aggregate-boundary-validation-ddd-tactical-patterns)
|
||||
13. [Secret Detection & Security](#13-secret-detection--security)
|
||||
14. [Severity-Based Prioritization & Technical Debt](#14-severity-based-prioritization--technical-debt)
|
||||
15. [Domain Event Usage Validation](#15-domain-event-usage-validation)
|
||||
16. [Value Object Immutability](#16-value-object-immutability)
|
||||
17. [Command Query Separation (CQS/CQRS)](#17-command-query-separation-cqscqrs)
|
||||
18. [Factory Pattern](#18-factory-pattern)
|
||||
19. [Specification Pattern](#19-specification-pattern)
|
||||
20. [Bounded Context](#20-bounded-context)
|
||||
21. [Persistence Ignorance](#21-persistence-ignorance)
|
||||
22. [Null Object Pattern](#22-null-object-pattern)
|
||||
23. [Primitive Obsession](#23-primitive-obsession)
|
||||
24. [Service Locator Anti-pattern](#24-service-locator-anti-pattern)
|
||||
25. [Double Dispatch and Visitor Pattern](#25-double-dispatch-and-visitor-pattern)
|
||||
26. [Entity Identity](#26-entity-identity)
|
||||
27. [Saga Pattern](#27-saga-pattern)
|
||||
28. [Anti-Corruption Layer](#28-anti-corruption-layer)
|
||||
29. [Ubiquitous Language](#29-ubiquitous-language)
|
||||
|
||||
---
|
||||
|
||||
@@ -801,22 +816,840 @@ This document provides authoritative sources, academic papers, industry standard
|
||||
|
||||
---
|
||||
|
||||
## 15. Domain Event Usage Validation
|
||||
|
||||
### Eric Evans: Domain-Driven Design (2003)
|
||||
|
||||
**Original Definition:**
|
||||
- Domain Events: "Something happened that domain experts care about"
|
||||
- Events capture facts about the domain that have already occurred
|
||||
- Distinct from system events - they model business-relevant occurrences
|
||||
- Reference: [Martin Fowler - Domain Event](https://martinfowler.com/eaaDev/DomainEvent.html)
|
||||
|
||||
**Book: Domain-Driven Design** (2003)
|
||||
- Author: Eric Evans
|
||||
- Publisher: Addison-Wesley Professional
|
||||
- ISBN: 978-0321125217
|
||||
- Domain Events weren't explicitly in the original book but evolved from DDD community
|
||||
- Reference: [DDD Community - Domain Events](https://www.domainlanguage.com/)
|
||||
|
||||
### Vaughn Vernon: Implementing Domain-Driven Design (2013)
|
||||
|
||||
**Chapter 8: Domain Events**
|
||||
- Author: Vaughn Vernon
|
||||
- Comprehensive coverage of Domain Events implementation
|
||||
- "Model information about activity in the domain as a series of discrete events"
|
||||
- Reference: [Amazon - Implementing DDD](https://www.amazon.com/Implementing-Domain-Driven-Design-Vaughn-Vernon/dp/0321834577)
|
||||
|
||||
**Key Principles:**
|
||||
- Events should be immutable
|
||||
- Named in past tense (OrderPlaced, UserRegistered)
|
||||
- Contain all data needed by handlers
|
||||
- Enable loose coupling between aggregates
|
||||
|
||||
### Martin Fowler's Event Patterns
|
||||
|
||||
**Event Sourcing:**
|
||||
- "Capture all changes to an application state as a sequence of events"
|
||||
- Events become the primary source of truth
|
||||
- Reference: [Martin Fowler - Event Sourcing](https://martinfowler.com/eaaDev/EventSourcing.html)
|
||||
|
||||
**Event-Driven Architecture:**
|
||||
- Promotes loose coupling between components
|
||||
- Enables asynchronous processing
|
||||
- Reference: [Martin Fowler - Event-Driven](https://martinfowler.com/articles/201701-event-driven.html)
|
||||
|
||||
### Why Direct Infrastructure Calls Are Bad
|
||||
|
||||
**Coupling Issues:**
|
||||
- Direct calls create tight coupling between domain and infrastructure
|
||||
- Makes testing difficult (need to mock infrastructure)
|
||||
- Violates Single Responsibility Principle
|
||||
- Reference: [Microsoft - Domain Events Design](https://learn.microsoft.com/en-us/dotnet/architecture/microservices/microservice-ddd-cqrs-patterns/domain-events-design-implementation)
|
||||
|
||||
**Benefits of Domain Events:**
|
||||
- Decouples domain from side effects
|
||||
- Enables eventual consistency
|
||||
- Improves testability
|
||||
- Supports audit logging naturally
|
||||
- Reference: [Jimmy Bogard - Domain Events](https://lostechies.com/jimmybogard/2010/04/08/strengthening-your-domain-domain-events/)
|
||||
|
||||
---
|
||||
|
||||
## 16. Value Object Immutability
|
||||
|
||||
### Eric Evans: Domain-Driven Design (2003)
|
||||
|
||||
**Value Object Definition:**
|
||||
- "An object that describes some characteristic or attribute but carries no concept of identity"
|
||||
- "Value Objects should be immutable"
|
||||
- When you care only about the attributes of an element, classify it as a Value Object
|
||||
- Reference: [Martin Fowler - Value Object](https://martinfowler.com/bliki/ValueObject.html)
|
||||
|
||||
**Immutability Requirement:**
|
||||
- "Treat the Value Object as immutable"
|
||||
- "Don't give it any identity and avoid the design complexities necessary to maintain Entities"
|
||||
- Reference: [DDD Reference - Value Objects](https://www.domainlanguage.com/wp-content/uploads/2016/05/DDD_Reference_2015-03.pdf)
|
||||
|
||||
### Martin Fowler on Value Objects
|
||||
|
||||
**Blog Post: Value Object** (2016)
|
||||
- "A small simple object, like money or a date range, whose equality isn't based on identity"
|
||||
- "I consider value objects to be one of the most important building blocks of good domain models"
|
||||
- Reference: [Martin Fowler - Value Object](https://martinfowler.com/bliki/ValueObject.html)
|
||||
|
||||
**Key Properties:**
|
||||
- Equality based on attribute values, not identity
|
||||
- Should be immutable (once created, cannot be changed)
|
||||
- Side-effect free behavior
|
||||
- Self-validating (validate in constructor)
|
||||
|
||||
### Vaughn Vernon: Implementing DDD
|
||||
|
||||
**Chapter 6: Value Objects**
|
||||
- Detailed implementation guidance
|
||||
- "Measures, quantifies, or describes a thing in the domain"
|
||||
- "Can be compared with other Value Objects using value equality"
|
||||
- "Completely replaceable when the measurement changes"
|
||||
- Reference: [Vaughn Vernon - Implementing DDD](https://www.amazon.com/Implementing-Domain-Driven-Design-Vaughn-Vernon/dp/0321834577)
|
||||
|
||||
### Why Immutability Matters
|
||||
|
||||
**Thread Safety:**
|
||||
- Immutable objects are inherently thread-safe
|
||||
- No synchronization needed for concurrent access
|
||||
- Reference: [Effective Java - Item 17](https://www.amazon.com/Effective-Java-Joshua-Bloch/dp/0134685997)
|
||||
|
||||
**Reasoning About Code:**
|
||||
- Easier to understand code when objects don't change
|
||||
- No defensive copying needed
|
||||
- Simplifies caching and optimization
|
||||
- Reference: [Oracle Java Tutorials - Immutable Objects](https://docs.oracle.com/javase/tutorial/essential/concurrency/immutable.html)
|
||||
|
||||
**Functional Programming Influence:**
|
||||
- Immutability is a core principle of functional programming
|
||||
- Reduces side effects and makes code more predictable
|
||||
- Reference: [Wikipedia - Immutable Object](https://en.wikipedia.org/wiki/Immutable_object)
|
||||
|
||||
---
|
||||
|
||||
## 17. Command Query Separation (CQS/CQRS)
|
||||
|
||||
### Bertrand Meyer: Original CQS Principle
|
||||
|
||||
**Book: Object-Oriented Software Construction** (1988, 2nd Ed. 1997)
|
||||
- Author: Bertrand Meyer
|
||||
- Publisher: Prentice Hall
|
||||
- ISBN: 978-0136291558
|
||||
- Introduced Command Query Separation principle
|
||||
- Reference: [Wikipedia - CQS](https://en.wikipedia.org/wiki/Command%E2%80%93query_separation)
|
||||
|
||||
**CQS Principle:**
|
||||
- "Every method should either be a command that performs an action, or a query that returns data to the caller, but not both"
|
||||
- Commands: change state, return nothing (void)
|
||||
- Queries: return data, change nothing (side-effect free)
|
||||
- Reference: [Martin Fowler - CommandQuerySeparation](https://martinfowler.com/bliki/CommandQuerySeparation.html)
|
||||
|
||||
### Greg Young: CQRS Pattern
|
||||
|
||||
**CQRS Documents** (2010)
|
||||
- Author: Greg Young
|
||||
- Extended CQS to architectural pattern
|
||||
- "CQRS is simply the creation of two objects where there was previously only one"
|
||||
- Reference: [Greg Young - CQRS Documents](https://cqrs.files.wordpress.com/2010/11/cqrs_documents.pdf)
|
||||
|
||||
**Key Concepts:**
|
||||
- Separate models for reading and writing
|
||||
- Write model (commands) optimized for business logic
|
||||
- Read model (queries) optimized for display/reporting
|
||||
- Reference: [Microsoft - CQRS Pattern](https://learn.microsoft.com/en-us/azure/architecture/patterns/cqrs)
|
||||
|
||||
### Martin Fowler on CQRS
|
||||
|
||||
**Blog Post: CQRS** (2011)
|
||||
- "At its heart is the notion that you can use a different model to update information than the model you use to read information"
|
||||
- Warns against overuse: "CQRS is a significant mental leap for all concerned"
|
||||
- Reference: [Martin Fowler - CQRS](https://martinfowler.com/bliki/CQRS.html)
|
||||
|
||||
### Benefits and Trade-offs
|
||||
|
||||
**Benefits:**
|
||||
- Independent scaling of read and write workloads
|
||||
- Optimized data schemas for each side
|
||||
- Improved security (separate read/write permissions)
|
||||
- Reference: [AWS - CQRS Pattern](https://docs.aws.amazon.com/prescriptive-guidance/latest/modernization-data-persistence/cqrs-pattern.html)
|
||||
|
||||
**Trade-offs:**
|
||||
- Increased complexity
|
||||
- Eventual consistency challenges
|
||||
- More code to maintain
|
||||
- Reference: [Microsoft - CQRS Considerations](https://learn.microsoft.com/en-us/azure/architecture/patterns/cqrs#issues-and-considerations)
|
||||
|
||||
---
|
||||
|
||||
## 18. Factory Pattern
|
||||
|
||||
### Gang of Four: Design Patterns (1994)
|
||||
|
||||
**Book: Design Patterns: Elements of Reusable Object-Oriented Software**
|
||||
- Authors: Erich Gamma, Richard Helm, Ralph Johnson, John Vlissides (Gang of Four)
|
||||
- Publisher: Addison-Wesley
|
||||
- ISBN: 978-0201633610
|
||||
- Defines Factory Method and Abstract Factory patterns
|
||||
- Reference: [Wikipedia - Design Patterns](https://en.wikipedia.org/wiki/Design_Patterns)
|
||||
|
||||
**Factory Method Pattern:**
|
||||
- "Define an interface for creating an object, but let subclasses decide which class to instantiate"
|
||||
- Lets a class defer instantiation to subclasses
|
||||
- Reference: [Refactoring Guru - Factory Method](https://refactoring.guru/design-patterns/factory-method)
|
||||
|
||||
**Abstract Factory Pattern:**
|
||||
- "Provide an interface for creating families of related or dependent objects without specifying their concrete classes"
|
||||
- Reference: [Refactoring Guru - Abstract Factory](https://refactoring.guru/design-patterns/abstract-factory)
|
||||
|
||||
### Eric Evans: Factory in DDD Context
|
||||
|
||||
**Domain-Driven Design** (2003)
|
||||
- Chapter 6: "The Life Cycle of a Domain Object"
|
||||
- Factories encapsulate complex object creation
|
||||
- "Shift the responsibility for creating instances of complex objects and Aggregates to a separate object"
|
||||
- Reference: [DDD Reference](https://www.domainlanguage.com/wp-content/uploads/2016/05/DDD_Reference_2015-03.pdf)
|
||||
|
||||
**DDD Factory Guidelines:**
|
||||
- Factory should create valid objects (invariants satisfied)
|
||||
- Two types: Factory for new objects, Factory for reconstitution
|
||||
- Keep creation logic out of the entity itself
|
||||
- Reference: Already in Section 10 - Domain-Driven Design
|
||||
|
||||
### Why Factories Matter in DDD
|
||||
|
||||
**Encapsulation of Creation Logic:**
|
||||
- Complex aggregates need coordinated creation
|
||||
- Business rules should be enforced at creation time
|
||||
- Clients shouldn't know construction details
|
||||
- Reference: [Vaughn Vernon - Implementing DDD, Chapter 11](https://www.amazon.com/Implementing-Domain-Driven-Design-Vaughn-Vernon/dp/0321834577)
|
||||
|
||||
**Factory vs Constructor:**
|
||||
- Constructors should be simple (assign values)
|
||||
- Factories handle complex creation logic
|
||||
- Factories can return different types
|
||||
- Reference: [Effective Java - Item 1: Static Factory Methods](https://www.amazon.com/Effective-Java-Joshua-Bloch/dp/0134685997)
|
||||
|
||||
---
|
||||
|
||||
## 19. Specification Pattern
|
||||
|
||||
### Eric Evans & Martin Fowler
|
||||
|
||||
**Original Paper: Specifications** (1997)
|
||||
- Authors: Eric Evans and Martin Fowler
|
||||
- Introduced the Specification pattern
|
||||
- "A Specification states a constraint on the state of another object"
|
||||
- Reference: [Martin Fowler - Specification](https://martinfowler.com/apsupp/spec.pdf)
|
||||
|
||||
**Domain-Driven Design** (2003)
|
||||
- Chapter 9: "Making Implicit Concepts Explicit"
|
||||
- Specifications make business rules explicit and reusable
|
||||
- "Create explicit predicate-like Value Objects for specialized purposes"
|
||||
- Reference: [DDD Reference](https://www.domainlanguage.com/wp-content/uploads/2016/05/DDD_Reference_2015-03.pdf)
|
||||
|
||||
### Pattern Definition
|
||||
|
||||
**Core Concept:**
|
||||
- Specification is a predicate that determines if an object satisfies some criteria
|
||||
- Encapsulates business rules that can be reused and combined
|
||||
- Reference: [Wikipedia - Specification Pattern](https://en.wikipedia.org/wiki/Specification_pattern)
|
||||
|
||||
**Three Main Uses:**
|
||||
1. **Selection**: Finding objects that match criteria
|
||||
2. **Validation**: Checking if object satisfies rules
|
||||
3. **Construction**: Describing what needs to be created
|
||||
- Reference: [Martin Fowler - Specification](https://martinfowler.com/apsupp/spec.pdf)
|
||||
|
||||
### Composite Specifications
|
||||
|
||||
**Combining Specifications:**
|
||||
- AND: Both specifications must be satisfied
|
||||
- OR: Either specification must be satisfied
|
||||
- NOT: Specification must not be satisfied
|
||||
- Reference: [Refactoring Guru - Specification Pattern](https://refactoring.guru/design-patterns/specification)
|
||||
|
||||
**Benefits:**
|
||||
- Reusable business rules
|
||||
- Testable in isolation
|
||||
- Readable domain language
|
||||
- Composable for complex rules
|
||||
- Reference: [Enterprise Craftsmanship - Specification Pattern](https://enterprisecraftsmanship.com/posts/specification-pattern-c-implementation/)
|
||||
|
||||
---
|
||||
|
||||
## 20. Bounded Context
|
||||
|
||||
### Eric Evans: Domain-Driven Design (2003)
|
||||
|
||||
**Original Definition:**
|
||||
- "A Bounded Context delimits the applicability of a particular model"
|
||||
- "Explicitly define the context within which a model applies"
|
||||
- Chapter 14: "Maintaining Model Integrity"
|
||||
- Reference: [Martin Fowler - Bounded Context](https://martinfowler.com/bliki/BoundedContext.html)
|
||||
|
||||
**Key Principles:**
|
||||
- Each Bounded Context has its own Ubiquitous Language
|
||||
- Same term can mean different things in different contexts
|
||||
- Models should not be shared across context boundaries
|
||||
- Reference: [DDD Reference](https://www.domainlanguage.com/wp-content/uploads/2016/05/DDD_Reference_2015-03.pdf)
|
||||
|
||||
### Vaughn Vernon: Strategic Design
|
||||
|
||||
**Implementing Domain-Driven Design** (2013)
|
||||
- Chapter 2: "Domains, Subdomains, and Bounded Contexts"
|
||||
- Detailed guidance on identifying and mapping contexts
|
||||
- Reference: [Vaughn Vernon - Implementing DDD](https://www.amazon.com/Implementing-Domain-Driven-Design-Vaughn-Vernon/dp/0321834577)
|
||||
|
||||
**Context Mapping Patterns:**
|
||||
- Shared Kernel
|
||||
- Customer/Supplier
|
||||
- Conformist
|
||||
- Anti-Corruption Layer
|
||||
- Open Host Service / Published Language
|
||||
- Reference: [Context Mapping Patterns](https://www.infoq.com/articles/ddd-contextmapping/)
|
||||
|
||||
### Why Bounded Contexts Matter
|
||||
|
||||
**Avoiding Big Ball of Mud:**
|
||||
- Without explicit boundaries, models become entangled
|
||||
- Different teams step on each other's models
|
||||
- Reference: [Wikipedia - Big Ball of Mud](https://en.wikipedia.org/wiki/Big_ball_of_mud)
|
||||
|
||||
**Microservices and Bounded Contexts:**
|
||||
- "Microservices should be designed around business capabilities, aligned with bounded contexts"
|
||||
- Each microservice typically represents one bounded context
|
||||
- Reference: [Microsoft - Microservices and Bounded Contexts](https://learn.microsoft.com/en-us/azure/architecture/microservices/model/domain-analysis)
|
||||
|
||||
### Cross-Context Communication
|
||||
|
||||
**Integration Patterns:**
|
||||
- Never share domain models across contexts
|
||||
- Use integration events or APIs
|
||||
- Translate between context languages
|
||||
- Reference: [Microsoft - Tactical DDD](https://learn.microsoft.com/en-us/azure/architecture/microservices/model/tactical-ddd)
|
||||
|
||||
---
|
||||
|
||||
## 21. Persistence Ignorance
|
||||
|
||||
### Definition and Principles
|
||||
|
||||
**Core Concept:**
|
||||
- Domain objects should have no knowledge of how they are persisted
|
||||
- Business logic remains pure and testable
|
||||
- Infrastructure concerns are separated from domain
|
||||
- Reference: [Microsoft - Persistence Ignorance](https://learn.microsoft.com/en-us/dotnet/architecture/microservices/microservice-ddd-cqrs-patterns/infrastructure-persistence-layer-design#the-persistence-ignorance-principle)
|
||||
|
||||
**Wikipedia Definition:**
|
||||
- "Persistence ignorance is the ability of a class to be used without any underlying persistence mechanism"
|
||||
- Objects don't know if/how they'll be stored
|
||||
- Reference: [Wikipedia - Persistence Ignorance](https://en.wikipedia.org/wiki/Persistence_ignorance)
|
||||
|
||||
### Eric Evans: DDD and Persistence
|
||||
|
||||
**Domain-Driven Design** (2003)
|
||||
- Repositories abstract away persistence details
|
||||
- Domain model should not reference ORM or database concepts
|
||||
- Reference: Already covered in Section 6 - Repository Pattern
|
||||
|
||||
**Key Quote:**
|
||||
- "The domain layer should be kept clean of all technical concerns"
|
||||
- ORM annotations violate this principle
|
||||
- Reference: [Clean Architecture and DDD](https://herbertograca.com/2017/11/16/explicit-architecture-01-ddd-hexagonal-onion-clean-cqrs-how-i-put-it-all-together/)
|
||||
|
||||
### Clean Architecture Alignment
|
||||
|
||||
**Robert C. Martin:**
|
||||
- "The database is a detail"
|
||||
- Domain entities should not depend on persistence frameworks
|
||||
- Use Repository interfaces to abstract persistence
|
||||
- Reference: [Clean Architecture Book](https://www.amazon.com/Clean-Architecture-Craftsmans-Software-Structure/dp/0134494164)
|
||||
|
||||
### Practical Implementation
|
||||
|
||||
**Two-Model Approach:**
|
||||
- Domain Model: Pure business objects
|
||||
- Persistence Model: ORM-annotated entities
|
||||
- Mappers translate between them
|
||||
- Reference: [Microsoft - Infrastructure Layer](https://learn.microsoft.com/en-us/dotnet/architecture/microservices/microservice-ddd-cqrs-patterns/infrastructure-persistence-layer-design)
|
||||
|
||||
**Benefits:**
|
||||
- Domain model can evolve independently of database schema
|
||||
- Easier testing (no ORM required)
|
||||
- Database can be changed without affecting domain
|
||||
- Reference: [Enterprise Craftsmanship - Persistence Ignorance](https://enterprisecraftsmanship.com/posts/persistence-ignorance/)
|
||||
|
||||
---
|
||||
|
||||
## 22. Null Object Pattern
|
||||
|
||||
### Original Pattern
|
||||
|
||||
**Pattern Languages of Program Design 3** (1997)
|
||||
- Author: Bobby Woolf
|
||||
- Chapter: "Null Object"
|
||||
- Publisher: Addison-Wesley
|
||||
- ISBN: 978-0201310115
|
||||
- Reference: [Wikipedia - Null Object Pattern](https://en.wikipedia.org/wiki/Null_object_pattern)
|
||||
|
||||
**Definition:**
|
||||
- "A Null Object provides a 'do nothing' behavior, hiding the details from its collaborators"
|
||||
- Replaces null checks with polymorphism
|
||||
- Reference: [Refactoring Guru - Null Object](https://refactoring.guru/introduce-null-object)
|
||||
|
||||
### Martin Fowler's Coverage
|
||||
|
||||
**Refactoring Book** (1999, 2018)
|
||||
- "Introduce Null Object" refactoring
|
||||
- "Replace conditional logic that checks for null with a null object"
|
||||
- Reference: [Refactoring Catalog](https://refactoring.com/catalog/introduceNullObject.html)
|
||||
|
||||
**Special Case Pattern:**
|
||||
- More general pattern that includes Null Object
|
||||
- "A subclass that provides special behavior for particular cases"
|
||||
- Reference: [Martin Fowler - Special Case](https://martinfowler.com/eaaCatalog/specialCase.html)
|
||||
|
||||
### Benefits
|
||||
|
||||
**Eliminates Null Checks:**
|
||||
- Reduces cyclomatic complexity
|
||||
- Cleaner, more readable code
|
||||
- Follows "Tell, Don't Ask" principle
|
||||
- Reference: [SourceMaking - Null Object](https://sourcemaking.com/design_patterns/null_object)
|
||||
|
||||
**Polymorphism Over Conditionals:**
|
||||
- Null Object responds to same interface as real object
|
||||
- Default/neutral behavior instead of null checks
|
||||
- Reference: [C2 Wiki - Null Object](https://wiki.c2.com/?NullObject)
|
||||
|
||||
### When to Use
|
||||
|
||||
**Good Candidates:**
|
||||
- Objects frequently checked for null
|
||||
- Null represents "absence" with sensible default behavior
|
||||
- Reference: [Baeldung - Null Object Pattern](https://www.baeldung.com/java-null-object-pattern)
|
||||
|
||||
**Cautions:**
|
||||
- Don't use when null has semantic meaning
|
||||
- Can hide bugs if misapplied
|
||||
- Reference: [Stack Overflow - Null Object Considerations](https://stackoverflow.com/questions/1274792/is-the-null-object-pattern-a-bad-practice)
|
||||
|
||||
---
|
||||
|
||||
## 23. Primitive Obsession
|
||||
|
||||
### Code Smell Definition
|
||||
|
||||
**Martin Fowler: Refactoring** (1999, 2018)
|
||||
- Primitive Obsession is a code smell
|
||||
- "Using primitives instead of small objects for simple tasks"
|
||||
- Reference: [Refactoring Catalog](https://refactoring.com/catalog/)
|
||||
|
||||
**Wikipedia Definition:**
|
||||
- "Using primitive data types to represent domain ideas"
|
||||
- Example: Using string for email, int for money
|
||||
- Reference: [Wikipedia - Code Smell](https://en.wikipedia.org/wiki/Code_smell)
|
||||
|
||||
### Why It's a Problem
|
||||
|
||||
**Lost Type Safety:**
|
||||
- String can contain anything, Email cannot
|
||||
- Compiler can't catch domain errors
|
||||
- Reference: [Refactoring Guru - Primitive Obsession](https://refactoring.guru/smells/primitive-obsession)
|
||||
|
||||
**Scattered Validation:**
|
||||
- Same validation repeated in multiple places
|
||||
- Violates DRY principle
|
||||
- Reference: [SourceMaking - Primitive Obsession](https://sourcemaking.com/refactoring/smells/primitive-obsession)
|
||||
|
||||
**Missing Behavior:**
|
||||
- Primitives can't have domain-specific methods
|
||||
- Logic lives in services instead of objects
|
||||
- Reference: [Enterprise Craftsmanship - Primitive Obsession](https://enterprisecraftsmanship.com/posts/functional-c-primitive-obsession/)
|
||||
|
||||
### Solutions
|
||||
|
||||
**Replace with Value Objects:**
|
||||
- Money instead of decimal
|
||||
- Email instead of string
|
||||
- PhoneNumber instead of string
|
||||
- Reference: Already covered in Section 16 - Value Object Immutability
|
||||
|
||||
**Replace Data Value with Object:**
|
||||
- Refactoring: "Replace Data Value with Object"
|
||||
- Introduce Parameter Object for related primitives
|
||||
- Reference: [Refactoring - Replace Data Value with Object](https://refactoring.com/catalog/replaceDataValueWithObject.html)
|
||||
|
||||
### Common Primitive Obsession Examples
|
||||
|
||||
**Frequently Misused Primitives:**
|
||||
- string for: email, phone, URL, currency code, country code
|
||||
- int/decimal for: money, percentage, age, quantity
|
||||
- DateTime for: date ranges, business dates
|
||||
- Reference: [DDD - Value Objects](https://martinfowler.com/bliki/ValueObject.html)
|
||||
|
||||
---
|
||||
|
||||
## 24. Service Locator Anti-pattern
|
||||
|
||||
### Martin Fowler's Analysis
|
||||
|
||||
**Blog Post: Inversion of Control Containers and the Dependency Injection pattern** (2004)
|
||||
- Compares Service Locator with Dependency Injection
|
||||
- "With service locator the application class asks for it explicitly by a message to the locator"
|
||||
- Reference: [Martin Fowler - Inversion of Control](https://martinfowler.com/articles/injection.html)
|
||||
|
||||
**Service Locator Definition:**
|
||||
- "The basic idea behind a service locator is to have an object that knows how to get hold of all of the services that an application might need"
|
||||
- Acts as a registry that provides dependencies on demand
|
||||
- Reference: [Martin Fowler - Service Locator](https://martinfowler.com/articles/injection.html#UsingAServiceLocator)
|
||||
|
||||
### Why It's Considered an Anti-pattern
|
||||
|
||||
**Mark Seemann: Dependency Injection in .NET** (2011, 2nd Ed. 2019)
|
||||
- Author: Mark Seemann
|
||||
- Extensively covers why Service Locator is problematic
|
||||
- "Service Locator is an anti-pattern"
|
||||
- Reference: [Mark Seemann - Service Locator is an Anti-Pattern](https://blog.ploeh.dk/2010/02/03/ServiceLocatorisanAnti-Pattern/)
|
||||
|
||||
**Hidden Dependencies:**
|
||||
- Dependencies are not visible in constructor
|
||||
- Makes code harder to understand and test
|
||||
- Violates Explicit Dependencies Principle
|
||||
- Reference: [DevIQ - Explicit Dependencies](https://deviq.com/principles/explicit-dependencies-principle)
|
||||
|
||||
**Testing Difficulties:**
|
||||
- Need to set up global locator for tests
|
||||
- Tests become coupled to locator setup
|
||||
- Reference: [Stack Overflow - Service Locator Testing](https://stackoverflow.com/questions/1557781/is-service-locator-an-anti-pattern)
|
||||
|
||||
### Dependency Injection Alternative
|
||||
|
||||
**Constructor Injection:**
|
||||
- Dependencies declared in constructor
|
||||
- Compiler enforces dependency provision
|
||||
- Clear, testable code
|
||||
- Reference: Already covered in Section 6 - Repository Pattern
|
||||
|
||||
**Benefits over Service Locator:**
|
||||
- Explicit dependencies
|
||||
- Easier testing (just pass mocks)
|
||||
- IDE support for navigation
|
||||
- Compile-time checking
|
||||
- Reference: [Martin Fowler - Constructor Injection](https://martinfowler.com/articles/injection.html#ConstructorInjectionWithPicocontainer)
|
||||
|
||||
---
|
||||
|
||||
## 25. Double Dispatch and Visitor Pattern
|
||||
|
||||
### Gang of Four: Visitor Pattern
|
||||
|
||||
**Design Patterns** (1994)
|
||||
- Authors: Gang of Four
|
||||
- Visitor Pattern chapter
|
||||
- "Represent an operation to be performed on the elements of an object structure"
|
||||
- Reference: [Wikipedia - Visitor Pattern](https://en.wikipedia.org/wiki/Visitor_pattern)
|
||||
|
||||
**Intent:**
|
||||
- "Lets you define a new operation without changing the classes of the elements on which it operates"
|
||||
- Separates algorithms from object structure
|
||||
- Reference: [Refactoring Guru - Visitor](https://refactoring.guru/design-patterns/visitor)
|
||||
|
||||
### Double Dispatch Mechanism
|
||||
|
||||
**Definition:**
|
||||
- "A mechanism that dispatches a function call to different concrete functions depending on the runtime types of two objects involved in the call"
|
||||
- Visitor pattern uses double dispatch
|
||||
- Reference: [Wikipedia - Double Dispatch](https://en.wikipedia.org/wiki/Double_dispatch)
|
||||
|
||||
**How It Works:**
|
||||
1. Client calls element.accept(visitor)
|
||||
2. Element calls visitor.visit(this) - first dispatch
|
||||
3. Correct visit() overload selected - second dispatch
|
||||
- Reference: [SourceMaking - Visitor](https://sourcemaking.com/design_patterns/visitor)
|
||||
|
||||
### When to Use
|
||||
|
||||
**Good Use Cases:**
|
||||
- Operations on complex object structures
|
||||
- Many distinct operations needed
|
||||
- Object structure rarely changes but operations change often
|
||||
- Reference: [Refactoring Guru - Visitor Use Cases](https://refactoring.guru/design-patterns/visitor)
|
||||
|
||||
**Alternative to Type Checking:**
|
||||
- Replace instanceof/typeof checks with polymorphism
|
||||
- More maintainable and extensible
|
||||
- Reference: [Replace Conditional with Polymorphism](https://refactoring.guru/replace-conditional-with-polymorphism)
|
||||
|
||||
### Trade-offs
|
||||
|
||||
**Advantages:**
|
||||
- Open/Closed Principle for new operations
|
||||
- Related operations grouped in one class
|
||||
- Accumulate state while traversing
|
||||
- Reference: [GoF Design Patterns](https://www.amazon.com/Design-Patterns-Elements-Reusable-Object-Oriented/dp/0201633612)
|
||||
|
||||
**Disadvantages:**
|
||||
- Adding new element types requires changing all visitors
|
||||
- May break encapsulation (visitors need access to element internals)
|
||||
- Reference: [C2 Wiki - Visitor Pattern](https://wiki.c2.com/?VisitorPattern)
|
||||
|
||||
---
|
||||
|
||||
## 26. Entity Identity
|
||||
|
||||
### Eric Evans: Domain-Driven Design (2003)
|
||||
|
||||
**Entity Definition:**
|
||||
- "An object that is not defined by its attributes, but rather by a thread of continuity and its identity"
|
||||
- "Some objects are not defined primarily by their attributes. They represent a thread of identity"
|
||||
- Reference: [Martin Fowler - Evans Classification](https://martinfowler.com/bliki/EvansClassification.html)
|
||||
|
||||
**Identity Characteristics:**
|
||||
- Unique within the system
|
||||
- Stable over time (doesn't change)
|
||||
- Survives state changes
|
||||
- Reference: [DDD Reference](https://www.domainlanguage.com/wp-content/uploads/2016/05/DDD_Reference_2015-03.pdf)
|
||||
|
||||
### Vaughn Vernon: Identity Implementation
|
||||
|
||||
**Implementing Domain-Driven Design** (2013)
|
||||
- Chapter 5: "Entities"
|
||||
- Detailed coverage of identity strategies
|
||||
- "The primary characteristic of an Entity is that it has a unique identity"
|
||||
- Reference: [Vaughn Vernon - Implementing DDD](https://www.amazon.com/Implementing-Domain-Driven-Design-Vaughn-Vernon/dp/0321834577)
|
||||
|
||||
**Identity Types:**
|
||||
- Natural keys (SSN, email)
|
||||
- Surrogate keys (UUID, auto-increment)
|
||||
- Domain-generated IDs
|
||||
- Reference: [Microsoft - Entity Keys](https://learn.microsoft.com/en-us/ef/core/modeling/keys)
|
||||
|
||||
### Identity Best Practices
|
||||
|
||||
**Immutability of Identity:**
|
||||
- Identity should never change after creation
|
||||
- Use readonly/final fields
|
||||
- Reference: [StackExchange - Mutable Entity ID](https://softwareengineering.stackexchange.com/questions/375765/is-it-bad-practice-to-have-mutable-entity-ids)
|
||||
|
||||
**Value Object for Identity:**
|
||||
- Wrap identity in Value Object (UserId, OrderId)
|
||||
- Type safety prevents mixing IDs
|
||||
- Can include validation logic
|
||||
- Reference: [Enterprise Craftsmanship - Strongly Typed IDs](https://enterprisecraftsmanship.com/posts/strongly-typed-ids/)
|
||||
|
||||
**Equality Based on Identity:**
|
||||
- Entity equality should compare only identity
|
||||
- Not all attributes
|
||||
- Reference: [Vaughn Vernon - Entity Equality](https://www.amazon.com/Implementing-Domain-Driven-Design-Vaughn-Vernon/dp/0321834577)
|
||||
|
||||
---
|
||||
|
||||
## 27. Saga Pattern
|
||||
|
||||
### Original Research
|
||||
|
||||
**Paper: Sagas** (1987)
|
||||
- Authors: Hector Garcia-Molina and Kenneth Salem
|
||||
- Published: ACM SIGMOD Conference
|
||||
- Introduced Sagas for long-lived transactions
|
||||
- Reference: [ACM Digital Library - Sagas](https://dl.acm.org/doi/10.1145/38713.38742)
|
||||
|
||||
**Definition:**
|
||||
- "A saga is a sequence of local transactions where each transaction updates data within a single service"
|
||||
- Alternative to distributed transactions
|
||||
- Reference: [Microsoft - Saga Pattern](https://learn.microsoft.com/en-us/azure/architecture/reference-architectures/saga/saga)
|
||||
|
||||
### Chris Richardson: Microservices Patterns
|
||||
|
||||
**Book: Microservices Patterns** (2018)
|
||||
- Author: Chris Richardson
|
||||
- Publisher: Manning
|
||||
- ISBN: 978-1617294549
|
||||
- Chapter 4: "Managing Transactions with Sagas"
|
||||
- Reference: [Manning - Microservices Patterns](https://www.manning.com/books/microservices-patterns)
|
||||
|
||||
**Saga Types:**
|
||||
1. **Choreography**: Each service publishes events that trigger next steps
|
||||
2. **Orchestration**: Central coordinator tells services what to do
|
||||
- Reference: [Microservices.io - Saga](https://microservices.io/patterns/data/saga.html)
|
||||
|
||||
### Compensating Transactions
|
||||
|
||||
**Core Concept:**
|
||||
- Each step has a compensating action to undo it
|
||||
- If step N fails, compensate steps N-1, N-2, ..., 1
|
||||
- Reference: [AWS - Saga Pattern](https://docs.aws.amazon.com/prescriptive-guidance/latest/modernization-data-persistence/saga-pattern.html)
|
||||
|
||||
**Compensation Examples:**
|
||||
- CreateOrder → DeleteOrder
|
||||
- ReserveInventory → ReleaseInventory
|
||||
- ChargePayment → RefundPayment
|
||||
- Reference: [Microsoft - Compensating Transactions](https://learn.microsoft.com/en-us/azure/architecture/patterns/compensating-transaction)
|
||||
|
||||
### Trade-offs
|
||||
|
||||
**Advantages:**
|
||||
- Works across service boundaries
|
||||
- No distributed locks
|
||||
- Services remain autonomous
|
||||
- Reference: [Chris Richardson - Saga](https://chrisrichardson.net/post/microservices/patterns/data/2019/07/22/design-sagas.html)
|
||||
|
||||
**Challenges:**
|
||||
- Complexity of compensation logic
|
||||
- Eventual consistency
|
||||
- Debugging distributed sagas
|
||||
- Reference: [Microsoft - Saga Considerations](https://learn.microsoft.com/en-us/azure/architecture/reference-architectures/saga/saga#issues-and-considerations)
|
||||
|
||||
---
|
||||
|
||||
## 28. Anti-Corruption Layer
|
||||
|
||||
### Eric Evans: Domain-Driven Design (2003)
|
||||
|
||||
**Original Definition:**
|
||||
- Chapter 14: "Maintaining Model Integrity"
|
||||
- "Create an isolating layer to provide clients with functionality in terms of their own domain model"
|
||||
- Protects your model from external/legacy models
|
||||
- Reference: [DDD Reference](https://www.domainlanguage.com/wp-content/uploads/2016/05/DDD_Reference_2015-03.pdf)
|
||||
|
||||
**Purpose:**
|
||||
- "The translation layer between a new system and an external system"
|
||||
- Prevents external model concepts from leaking in
|
||||
- Reference: [Martin Fowler - Anti-Corruption Layer](https://martinfowler.com/bliki/AntiCorruptionLayer.html)
|
||||
|
||||
### Microsoft Guidance
|
||||
|
||||
**Azure Architecture Center:**
|
||||
- "Implement a facade or adapter layer between different subsystems that don't share the same semantics"
|
||||
- Isolate subsystems by placing an anti-corruption layer between them
|
||||
- Reference: [Microsoft - ACL Pattern](https://learn.microsoft.com/en-us/azure/architecture/patterns/anti-corruption-layer)
|
||||
|
||||
**When to Use:**
|
||||
- Integrating with legacy systems
|
||||
- Migrating from monolith to microservices
|
||||
- Working with third-party APIs
|
||||
- Reference: [Microsoft - ACL When to Use](https://learn.microsoft.com/en-us/azure/architecture/patterns/anti-corruption-layer#when-to-use-this-pattern)
|
||||
|
||||
### Components of ACL
|
||||
|
||||
**Facade:**
|
||||
- Simplified interface to external system
|
||||
- Hides complexity from domain
|
||||
- Reference: [Refactoring Guru - Facade](https://refactoring.guru/design-patterns/facade)
|
||||
|
||||
**Adapter:**
|
||||
- Translates between interfaces
|
||||
- Maps external model to domain model
|
||||
- Reference: [Refactoring Guru - Adapter](https://refactoring.guru/design-patterns/adapter)
|
||||
|
||||
**Translator:**
|
||||
- Converts data structures
|
||||
- Maps field names and types
|
||||
- Handles semantic differences
|
||||
- Reference: [Evans DDD - Model Translation](https://www.domainlanguage.com/)
|
||||
|
||||
### Benefits
|
||||
|
||||
**Isolation:**
|
||||
- Changes to external system don't ripple through domain
|
||||
- Domain model remains pure
|
||||
- Reference: [Microsoft - ACL Benefits](https://learn.microsoft.com/en-us/azure/architecture/patterns/anti-corruption-layer)
|
||||
|
||||
**Gradual Migration:**
|
||||
- Replace legacy components incrementally
|
||||
- Strangler Fig pattern compatibility
|
||||
- Reference: [Martin Fowler - Strangler Fig](https://martinfowler.com/bliki/StranglerFigApplication.html)
|
||||
|
||||
---
|
||||
|
||||
## 29. Ubiquitous Language
|
||||
|
||||
### Eric Evans: Domain-Driven Design (2003)
|
||||
|
||||
**Original Definition:**
|
||||
- Chapter 2: "Communication and the Use of Language"
|
||||
- "A language structured around the domain model and used by all team members"
|
||||
- "The vocabulary of that Ubiquitous Language includes the names of classes and prominent operations"
|
||||
- Reference: [Martin Fowler - Ubiquitous Language](https://martinfowler.com/bliki/UbiquitousLanguage.html)
|
||||
|
||||
**Key Principles:**
|
||||
- Shared by developers and domain experts
|
||||
- Used in code, conversations, and documentation
|
||||
- Changes to language reflect model changes
|
||||
- Reference: [DDD Reference](https://www.domainlanguage.com/wp-content/uploads/2016/05/DDD_Reference_2015-03.pdf)
|
||||
|
||||
### Why It Matters
|
||||
|
||||
**Communication Benefits:**
|
||||
- Reduces translation between business and tech
|
||||
- Catches misunderstandings early
|
||||
- Domain experts can read code names
|
||||
- Reference: [InfoQ - Ubiquitous Language](https://www.infoq.com/articles/ddd-ubiquitous-language/)
|
||||
|
||||
**Design Benefits:**
|
||||
- Model reflects real domain concepts
|
||||
- Code becomes self-documenting
|
||||
- Easier onboarding for new team members
|
||||
- Reference: [Vaughn Vernon - Implementing DDD](https://www.amazon.com/Implementing-Domain-Driven-Design-Vaughn-Vernon/dp/0321834577)
|
||||
|
||||
### Building Ubiquitous Language
|
||||
|
||||
**Glossary:**
|
||||
- Document key terms and definitions
|
||||
- Keep updated as understanding evolves
|
||||
- Reference: [DDD Community - Glossary](https://thedomaindrivendesign.io/glossary/)
|
||||
|
||||
**Event Storming:**
|
||||
- Collaborative workshop technique
|
||||
- Discover domain events and concepts
|
||||
- Build shared understanding and language
|
||||
- Reference: [Alberto Brandolini - Event Storming](https://www.eventstorming.com/)
|
||||
|
||||
### Common Pitfalls
|
||||
|
||||
**Inconsistent Terminology:**
|
||||
- Same concept with different names (Customer/Client/User)
|
||||
- Different concepts with same name
|
||||
- Reference: [Domain Language - Building UL](https://www.domainlanguage.com/)
|
||||
|
||||
**Technical Terms in Domain:**
|
||||
- "DTO", "Entity", "Repository" are technical
|
||||
- Domain should use business terms
|
||||
- Reference: [Evans DDD - Model-Driven Design](https://www.domainlanguage.com/)
|
||||
|
||||
---
|
||||
|
||||
## Conclusion
|
||||
|
||||
The code quality detection rules implemented in Guardian are firmly grounded in:
|
||||
|
||||
1. **Academic Research**: Peer-reviewed papers on software maintainability, complexity metrics, code quality, technical debt prioritization, and severity classification
|
||||
1. **Academic Research**: Peer-reviewed papers on software maintainability, complexity metrics, code quality, technical debt prioritization, severity classification, and distributed systems (Sagas)
|
||||
2. **Industry Standards**: ISO/IEC 25010, SonarQube rules, OWASP security guidelines, Google and Airbnb style guides
|
||||
3. **Authoritative Books**:
|
||||
- Gang of Four's "Design Patterns" (1994)
|
||||
- Bertrand Meyer's "Object-Oriented Software Construction" (1988, 1997)
|
||||
- Robert C. Martin's "Clean Architecture" (2017)
|
||||
- Vaughn Vernon's "Implementing Domain-Driven Design" (2013)
|
||||
- Chris Richardson's "Microservices Patterns" (2018)
|
||||
- Eric Evans' "Domain-Driven Design" (2003)
|
||||
- Martin Fowler's "Patterns of Enterprise Application Architecture" (2002)
|
||||
- Martin Fowler's "Refactoring" (1999, 2018)
|
||||
- Steve McConnell's "Code Complete" (1993, 2004)
|
||||
4. **Expert Guidance**: Martin Fowler, Robert C. Martin (Uncle Bob), Eric Evans, Vaughn Vernon, Alistair Cockburn, Kent Beck
|
||||
- Joshua Bloch's "Effective Java" (2001, 2018)
|
||||
- Mark Seemann's "Dependency Injection in .NET" (2011, 2019)
|
||||
- Bobby Woolf's "Null Object" in PLoPD3 (1997)
|
||||
4. **Expert Guidance**: Martin Fowler, Robert C. Martin (Uncle Bob), Eric Evans, Vaughn Vernon, Alistair Cockburn, Kent Beck, Greg Young, Bertrand Meyer, Mark Seemann, Chris Richardson, Alberto Brandolini
|
||||
5. **Security Standards**: OWASP Secrets Management, GitHub Secret Scanning, GitGuardian best practices
|
||||
6. **Open Source Tools**: ArchUnit, SonarQube, ESLint, Secretlint - widely adopted in enterprise environments
|
||||
7. **DDD Tactical & Strategic Patterns**: Domain Events, Value Objects, Entities, Aggregates, Bounded Contexts, Anti-Corruption Layer, Ubiquitous Language, Specifications, Factories
|
||||
8. **Architectural Patterns**: CQS/CQRS, Saga, Visitor/Double Dispatch, Null Object, Persistence Ignorance
|
||||
|
||||
These rules represent decades of software engineering wisdom, empirical research, security best practices, and battle-tested practices from the world's leading software organizations and thought leaders.
|
||||
|
||||
@@ -845,9 +1678,9 @@ These rules represent decades of software engineering wisdom, empirical research
|
||||
|
||||
---
|
||||
|
||||
**Document Version**: 1.1
|
||||
**Last Updated**: 2025-11-26
|
||||
**Document Version**: 2.0
|
||||
**Last Updated**: 2025-12-04
|
||||
**Questions or want to contribute research?**
|
||||
- 📧 Email: fozilbek.samiyev@gmail.com
|
||||
- 🐙 GitHub: https://github.com/samiyev/puaros/issues
|
||||
**Based on research as of**: November 2025
|
||||
**Based on research as of**: December 2025
|
||||
|
||||
@@ -5,6 +5,453 @@ All notable changes to this project will be documented in this file.
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [0.30.1] - 2025-12-05 - Display Transitive Counts in Context
|
||||
|
||||
### Changed
|
||||
|
||||
- **High Impact Files table now includes transitive counts**
|
||||
- Table header changed from `| File | Impact | Dependents |` to `| File | Impact | Direct | Transitive |`
|
||||
- Shows both direct dependent count and transitive dependent count
|
||||
- Sorting changed: now sorts by transitive count first, then by impact score
|
||||
- Example: `| utils/validation | 67% | 12 | 24 |`
|
||||
|
||||
### Technical Details
|
||||
|
||||
- Total tests: 1839 passed
|
||||
- 0 ESLint errors, 3 warnings (pre-existing complexity)
|
||||
|
||||
---
|
||||
|
||||
## [0.30.0] - 2025-12-05 - Transitive Dependencies Count
|
||||
|
||||
### Added
|
||||
|
||||
- **Transitive Dependency Counts in FileMeta (v0.30.0)**
|
||||
- New `transitiveDepCount: number` field - count of files that depend on this file transitively
|
||||
- New `transitiveDepByCount: number` field - count of files this file depends on transitively
|
||||
- Includes both direct and indirect dependencies/dependents
|
||||
- Excludes the file itself from counts (handles circular dependencies)
|
||||
|
||||
- **Transitive Dependency Computation in MetaAnalyzer**
|
||||
- New `computeTransitiveCounts()` method - computes transitive counts for all files
|
||||
- New `getTransitiveDependents()` method - DFS with cycle detection for dependents
|
||||
- New `getTransitiveDependencies()` method - DFS with cycle detection for dependencies
|
||||
- Top-level caching for efficiency (avoids re-computing for each file)
|
||||
- Graceful handling of circular dependencies
|
||||
|
||||
### Technical Details
|
||||
|
||||
- Total tests: 1840 passed (was 1826, +14 new tests)
|
||||
- 9 new tests for computeTransitiveCounts()
|
||||
- 2 new tests for getTransitiveDependents()
|
||||
- 2 new tests for getTransitiveDependencies()
|
||||
- 1 new test for analyzeAll with transitive counts
|
||||
- Coverage: 97.58% lines, 91.5% branches, 98.64% functions
|
||||
- 0 ESLint errors, 3 warnings (pre-existing complexity)
|
||||
- Build successful
|
||||
|
||||
### Notes
|
||||
|
||||
This completes v0.30.0 - the final feature milestone before v1.0.0:
|
||||
- ✅ 0.27.0 - Inline Dependency Graph
|
||||
- ✅ 0.28.0 - Circular Dependencies in Context
|
||||
- ✅ 0.29.0 - Impact Score
|
||||
- ✅ 0.30.0 - Transitive Dependencies Count
|
||||
|
||||
Next milestone: v1.0.0 - Production Ready
|
||||
|
||||
---
|
||||
|
||||
## [0.29.0] - 2025-12-05 - Impact Score
|
||||
|
||||
### Added
|
||||
|
||||
- **High Impact Files in Initial Context (v0.29.0)**
|
||||
- New `## High Impact Files` section in initial context
|
||||
- Shows files with highest impact scores (percentage of codebase depending on them)
|
||||
- Table format with File, Impact %, and Dependents count
|
||||
- Files sorted by impact score descending
|
||||
- Default: shows top 10 files with impact score >= 5%
|
||||
|
||||
- **Impact Score Computation**
|
||||
- New `impactScore: number` field in `FileMeta` (0-100)
|
||||
- Formula: `(dependents.length / (totalFiles - 1)) * 100`
|
||||
- Computed in `MetaAnalyzer.analyzeAll()` after all files analyzed
|
||||
- New `calculateImpactScore()` helper function in FileMeta.ts
|
||||
|
||||
- **Configuration Option**
|
||||
- `includeHighImpactFiles: boolean` in ContextConfigSchema (default: `true`)
|
||||
- `includeHighImpactFiles` option in `BuildContextOptions`
|
||||
- Users can disable to save tokens: `context.includeHighImpactFiles: false`
|
||||
|
||||
- **New Helper Function in prompts.ts**
|
||||
- `formatHighImpactFiles()` - formats high impact files table for display
|
||||
|
||||
### New Context Format
|
||||
|
||||
```
|
||||
## High Impact Files
|
||||
|
||||
| File | Impact | Dependents |
|
||||
|------|--------|------------|
|
||||
| utils/validation | 67% | 12 files |
|
||||
| types/user | 45% | 8 files |
|
||||
| services/user | 34% | 6 files |
|
||||
```
|
||||
|
||||
### Technical Details
|
||||
|
||||
- Total tests: 1826 passed (was 1798, +28 new tests)
|
||||
- 9 new tests for calculateImpactScore()
|
||||
- 14 new tests for formatHighImpactFiles() and buildInitialContext
|
||||
- 5 new tests for includeHighImpactFiles config option
|
||||
- Coverage: 97.52% lines, 91.3% branches, 98.63% functions
|
||||
- 0 ESLint errors, 3 warnings (pre-existing complexity)
|
||||
- Build successful
|
||||
|
||||
### Notes
|
||||
|
||||
This completes v0.29.0 of the Graph Metrics milestone:
|
||||
- ✅ 0.27.0 - Inline Dependency Graph
|
||||
- ✅ 0.28.0 - Circular Dependencies in Context
|
||||
- ✅ 0.29.0 - Impact Score
|
||||
|
||||
Next milestone: v0.30.0 - Transitive Dependencies Count
|
||||
|
||||
---
|
||||
|
||||
## [0.28.0] - 2025-12-05 - Circular Dependencies in Context
|
||||
|
||||
### Added
|
||||
|
||||
- **Circular Dependencies in Initial Context (v0.28.0)**
|
||||
- New `## ⚠️ Circular Dependencies` section in initial context
|
||||
- Shows cycle chains immediately without requiring tool calls
|
||||
- Format: `- services/user → services/auth → services/user`
|
||||
- Uses same path shortening as dependency graph (removes `src/`, extensions, `/index`)
|
||||
|
||||
- **Configuration Option**
|
||||
- `includeCircularDeps: boolean` in ContextConfigSchema (default: `true`)
|
||||
- `includeCircularDeps` option in `BuildContextOptions`
|
||||
- `circularDeps: string[][]` parameter to pass pre-computed cycles
|
||||
- Users can disable to save tokens: `context.includeCircularDeps: false`
|
||||
|
||||
- **New Helper Function in prompts.ts**
|
||||
- `formatCircularDeps()` - formats circular dependency cycles for display
|
||||
|
||||
### New Context Format
|
||||
|
||||
```
|
||||
## ⚠️ Circular Dependencies
|
||||
|
||||
- services/user → services/auth → services/user
|
||||
- utils/a → utils/b → utils/c → utils/a
|
||||
```
|
||||
|
||||
### Technical Details
|
||||
|
||||
- Total tests: 1798 passed (was 1775, +23 new tests)
|
||||
- 12 new tests for formatCircularDeps()
|
||||
- 6 new tests for buildInitialContext with includeCircularDeps
|
||||
- 5 new tests for includeCircularDeps config option
|
||||
- Coverage: 97.48% lines, 91.13% branches, 98.63% functions
|
||||
- 0 ESLint errors, 3 warnings (pre-existing complexity in ASTParser and prompts)
|
||||
- Build successful
|
||||
|
||||
## [0.27.0] - 2025-12-05 - Inline Dependency Graph
|
||||
|
||||
### Added
|
||||
|
||||
- **Dependency Graph in Initial Context (v0.27.0)**
|
||||
- New `## Dependency Graph` section in initial context
|
||||
- Shows file relationships without requiring tool calls
|
||||
- Format: `services/user: → types/user, utils/validation ← controllers/user`
|
||||
- `→` indicates files this file imports (dependencies)
|
||||
- `←` indicates files that import this file (dependents)
|
||||
- Hub files (>5 dependents) shown first
|
||||
- Files sorted by total connections (descending)
|
||||
|
||||
- **Configuration Option**
|
||||
- `includeDepsGraph: boolean` in ContextConfigSchema (default: `true`)
|
||||
- `includeDepsGraph` option in `BuildContextOptions`
|
||||
- Users can disable to save tokens: `context.includeDepsGraph: false`
|
||||
|
||||
- **New Helper Functions in prompts.ts**
|
||||
- `formatDependencyGraph()` - formats entire dependency graph from metas
|
||||
- `formatDepsEntry()` - formats single file's dependencies/dependents
|
||||
- `shortenPath()` - shortens paths (removes `src/`, extensions, `/index`)
|
||||
|
||||
### New Context Format
|
||||
|
||||
```
|
||||
## Dependency Graph
|
||||
|
||||
utils/validation: ← services/user, services/auth, controllers/api
|
||||
services/user: → types/user, utils/validation ← controllers/user, api/routes
|
||||
services/auth: → services/user, utils/jwt ← controllers/auth
|
||||
types/user: ← services/user, services/auth
|
||||
```
|
||||
|
||||
### Technical Details
|
||||
|
||||
- Total tests: 1775 passed (was 1754, +21 new tests)
|
||||
- 16 new tests for formatDependencyGraph()
|
||||
- 5 new tests for includeDepsGraph config option
|
||||
- Coverage: 97.48% lines, 91.07% branches, 98.62% functions
|
||||
- 0 ESLint errors, 2 warnings (pre-existing complexity in ASTParser and prompts)
|
||||
- Build successful
|
||||
|
||||
### Notes
|
||||
|
||||
This completes v0.27.0 of the Graph Metrics milestone:
|
||||
- ✅ 0.27.0 - Inline Dependency Graph
|
||||
|
||||
Next milestone: v0.28.0 - Circular Dependencies in Context
|
||||
|
||||
---
|
||||
|
||||
## [0.26.0] - 2025-12-05 - Rich Initial Context: Decorator Extraction
|
||||
|
||||
### Added
|
||||
|
||||
- **Decorator Extraction (0.24.4)**
|
||||
- Functions now show their decorators in initial context
|
||||
- Classes now show their decorators in initial context
|
||||
- Methods show decorators per-method
|
||||
- New format: `@Controller('users') class UserController`
|
||||
- Function format: `@Get(':id') async getUser(id: string): Promise<User>`
|
||||
- Supports NestJS decorators: `@Controller`, `@Get`, `@Post`, `@Injectable`, `@UseGuards`, etc.
|
||||
- Supports Angular decorators: `@Component`, `@Injectable`, `@Input`, `@Output`, etc.
|
||||
|
||||
- **FileAST.ts Enhancements**
|
||||
- `decorators?: string[]` field on `FunctionInfo`
|
||||
- `decorators?: string[]` field on `MethodInfo`
|
||||
- `decorators?: string[]` field on `ClassInfo`
|
||||
|
||||
- **ASTParser.ts Enhancements**
|
||||
- `formatDecorator()` - formats decorator node to string (e.g., `@Get(':id')`)
|
||||
- `extractNodeDecorators()` - extracts decorators that are direct children of a node
|
||||
- `extractDecoratorsFromSiblings()` - extracts decorators before the declaration in export statements
|
||||
- Decorators are extracted for classes, methods, and exported functions
|
||||
|
||||
- **prompts.ts Enhancements**
|
||||
- `formatDecoratorsPrefix()` - formats decorators as a prefix string for display
|
||||
- Used in `formatFunctionSignature()` for function decorators
|
||||
- Used in `formatFileSummary()` for class decorators
|
||||
|
||||
### New Context Format
|
||||
|
||||
```
|
||||
### src/controllers/user.controller.ts
|
||||
- @Controller('users') class UserController extends BaseController
|
||||
- @Get(':id') @Auth() async getUser(id: string): Promise<User>
|
||||
- @Post() @ValidateBody() async createUser(data: UserDTO): Promise<User>
|
||||
```
|
||||
|
||||
### Technical Details
|
||||
|
||||
- Total tests: 1754 passed (was 1720, +34 new tests)
|
||||
- 14 new tests for ASTParser decorator extraction
|
||||
- 6 new tests for prompts decorator formatting
|
||||
- +14 other tests from internal improvements
|
||||
- Coverage: 97.49% lines, 91.14% branches, 98.61% functions
|
||||
- 0 ESLint errors, 2 warnings (pre-existing complexity in ASTParser and prompts)
|
||||
- Build successful
|
||||
|
||||
### Notes
|
||||
|
||||
This completes the 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
|
||||
|
||||
Next milestone: v0.25.0 - Graph Metrics in Context
|
||||
|
||||
---
|
||||
|
||||
## [0.25.0] - 2025-12-04 - Rich Initial Context: Interface Fields & Type Definitions
|
||||
|
||||
### Added
|
||||
|
||||
- **Interface Field Definitions (0.24.2)**
|
||||
- Interfaces now show their fields in initial context
|
||||
- New format: `interface User { id: string, name: string, email: string }`
|
||||
- Readonly fields marked: `interface Config { readonly version: string }`
|
||||
- Extends still supported: `interface AdminUser extends User { role: string }`
|
||||
|
||||
- **Type Alias Definitions (0.24.2)**
|
||||
- Type aliases now show their definitions in initial context
|
||||
- Simple types: `type UserId = string`
|
||||
- Union types: `type Status = "pending" | "active" | "done"`
|
||||
- Intersection types: `type AdminUser = User & Admin`
|
||||
- Function types: `type Handler = (event: Event) => void`
|
||||
- Long definitions truncated at 80 characters with `...`
|
||||
|
||||
- **New Helper Functions in prompts.ts**
|
||||
- `formatInterfaceSignature()` - formats interface with fields
|
||||
- `formatTypeAliasSignature()` - formats type alias with definition
|
||||
- `truncateDefinition()` - truncates long type definitions
|
||||
|
||||
### Changed
|
||||
|
||||
- **FileAST.ts**
|
||||
- Added `definition?: string` field to `TypeAliasInfo` interface
|
||||
|
||||
- **ASTParser.ts**
|
||||
- `extractTypeAlias()` now extracts the type definition via `childForFieldName(VALUE)`
|
||||
- Supports all type kinds: simple, union, intersection, object, function, generic, array, tuple
|
||||
|
||||
- **prompts.ts**
|
||||
- `formatFileSummary()` now uses `formatInterfaceSignature()` for interfaces
|
||||
- `formatFileSummary()` now uses `formatTypeAliasSignature()` for type aliases
|
||||
|
||||
### New Context Format
|
||||
|
||||
```
|
||||
### src/types/user.ts
|
||||
- interface User { id: string, name: string, email: string }
|
||||
- interface UserDTO { name: string, email?: string }
|
||||
- type UserId = string
|
||||
- type Status = "pending" | "active" | "done"
|
||||
- type AdminUser = User & Admin
|
||||
```
|
||||
|
||||
### Technical Details
|
||||
|
||||
- Total tests: 1720 passed (was 1702, +18 new tests)
|
||||
- 10 new tests for interface field formatting
|
||||
- 8 new tests for type alias definition extraction
|
||||
- Coverage: 97.5% lines, 91.04% branches, 98.6% functions
|
||||
- 0 ESLint errors, 1 warning (pre-existing complexity in ASTParser)
|
||||
- Build successful
|
||||
|
||||
### Notes
|
||||
|
||||
This completes the second part of Rich Initial Context milestone:
|
||||
- ✅ 0.24.1 - Function Signatures with Types
|
||||
- ✅ 0.24.2 - Interface/Type Field Definitions
|
||||
- ⏳ 0.24.3 - Enum Value Definitions
|
||||
- ⏳ 0.24.4 - Decorator Extraction
|
||||
|
||||
---
|
||||
|
||||
## [0.24.0] - 2025-12-04 - Rich Initial Context: Function Signatures
|
||||
|
||||
### Added
|
||||
|
||||
- **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
|
||||
|
||||
@@ -1333,40 +1333,40 @@ class ErrorHandler {
|
||||
**Priority:** HIGH
|
||||
**Status:** Complete (v0.19.0 released)
|
||||
|
||||
Рефакторинг: переход на чистый XML формат для tool calls (как в CONCEPT.md).
|
||||
Refactoring: transition to pure XML format for tool calls (as in CONCEPT.md).
|
||||
|
||||
### Текущая проблема
|
||||
### Current Problem
|
||||
|
||||
OllamaClient использует Ollama native tool calling (JSON Schema), а ResponseParser реализует XML парсинг. Это создаёт путаницу и не соответствует CONCEPT.md.
|
||||
OllamaClient uses Ollama native tool calling (JSON Schema), while ResponseParser implements XML parsing. This creates confusion and doesn't match CONCEPT.md.
|
||||
|
||||
### 0.19.1 - OllamaClient Refactor
|
||||
|
||||
```typescript
|
||||
// src/infrastructure/llm/OllamaClient.ts
|
||||
|
||||
// БЫЛО:
|
||||
// - Передаём tools в Ollama SDK format
|
||||
// - Извлекаем tool_calls из response.message.tool_calls
|
||||
// BEFORE:
|
||||
// - Pass tools in Ollama SDK format
|
||||
// - Extract tool_calls from response.message.tool_calls
|
||||
|
||||
// СТАНЕТ:
|
||||
// - НЕ передаём tools в SDK
|
||||
// - Tools описаны в system prompt как XML
|
||||
// - LLM возвращает XML в content
|
||||
// - Парсим через ResponseParser
|
||||
// AFTER:
|
||||
// - DON'T pass tools to SDK
|
||||
// - Tools described in system prompt as XML
|
||||
// - LLM returns XML in content
|
||||
// - Parse via ResponseParser
|
||||
```
|
||||
|
||||
**Изменения:**
|
||||
- [x] Удалить `convertTools()` метод
|
||||
- [x] Удалить `extractToolCalls()` метод
|
||||
- [x] Убрать передачу `tools` в `client.chat()`
|
||||
- [x] Возвращать только `content` без `toolCalls`
|
||||
**Changes:**
|
||||
- [x] Remove `convertTools()` method
|
||||
- [x] Remove `extractToolCalls()` method
|
||||
- [x] Remove `tools` from `client.chat()` call
|
||||
- [x] Return only `content` without `toolCalls`
|
||||
|
||||
### 0.19.2 - System Prompt Update
|
||||
|
||||
```typescript
|
||||
// src/infrastructure/llm/prompts.ts
|
||||
|
||||
// Добавить в SYSTEM_PROMPT полное описание XML формата:
|
||||
// Add full XML format description to SYSTEM_PROMPT:
|
||||
|
||||
const TOOL_FORMAT_INSTRUCTIONS = `
|
||||
## Tool Calling Format
|
||||
@@ -1397,94 +1397,91 @@ Always wait for tool results before making conclusions.
|
||||
`
|
||||
```
|
||||
|
||||
**Изменения:**
|
||||
- [x] Добавить `TOOL_FORMAT_INSTRUCTIONS` в prompts.ts
|
||||
- [x] Включить в `SYSTEM_PROMPT`
|
||||
- [x] Добавить примеры для всех 18 tools
|
||||
**Changes:**
|
||||
- [x] Add `TOOL_FORMAT_INSTRUCTIONS` to prompts.ts
|
||||
- [x] Include in `SYSTEM_PROMPT`
|
||||
- [x] Add examples for all 18 tools
|
||||
|
||||
### 0.19.3 - HandleMessage Simplification
|
||||
|
||||
```typescript
|
||||
// src/application/use-cases/HandleMessage.ts
|
||||
|
||||
// БЫЛО:
|
||||
// BEFORE:
|
||||
// const response = await this.llm.chat(messages)
|
||||
// const parsed = parseToolCalls(response.content)
|
||||
|
||||
// СТАНЕТ:
|
||||
// const response = await this.llm.chat(messages) // без tools
|
||||
// const parsed = parseToolCalls(response.content) // единственный источник
|
||||
// AFTER:
|
||||
// const response = await this.llm.chat(messages) // without tools
|
||||
// const parsed = parseToolCalls(response.content) // single source
|
||||
```
|
||||
|
||||
**Изменения:**
|
||||
- [x] Убрать передачу tool definitions в `llm.chat()`
|
||||
- [x] ResponseParser — единственный источник tool calls
|
||||
- [x] Упростить логику обработки
|
||||
**Changes:**
|
||||
- [x] Remove tool definitions from `llm.chat()`
|
||||
- [x] ResponseParser — single source of tool calls
|
||||
- [x] Simplify processing logic
|
||||
|
||||
### 0.19.4 - ILLMClient Interface Update
|
||||
|
||||
```typescript
|
||||
// src/domain/services/ILLMClient.ts
|
||||
|
||||
// БЫЛО:
|
||||
// BEFORE:
|
||||
interface ILLMClient {
|
||||
chat(messages: ChatMessage[], tools?: ToolDef[]): Promise<LLMResponse>
|
||||
}
|
||||
|
||||
// СТАНЕТ:
|
||||
// AFTER:
|
||||
interface ILLMClient {
|
||||
chat(messages: ChatMessage[]): Promise<LLMResponse>
|
||||
// tools больше не передаются - они в system prompt
|
||||
// tools no longer passed - they're in system prompt
|
||||
}
|
||||
```
|
||||
|
||||
**Изменения:**
|
||||
- [x] Убрать `tools` параметр из `chat()`
|
||||
- [x] Убрать `toolCalls` из `LLMResponse` (парсятся из content)
|
||||
- [x] Обновить все реализации
|
||||
**Changes:**
|
||||
- [x] Remove `tools` parameter from `chat()`
|
||||
- [x] Remove `toolCalls` from `LLMResponse` (parsed from content)
|
||||
- [x] Update all implementations
|
||||
|
||||
### 0.19.5 - ResponseParser Enhancements
|
||||
|
||||
```typescript
|
||||
// src/infrastructure/llm/ResponseParser.ts
|
||||
|
||||
// Улучшения:
|
||||
// - Лучшая обработка ошибок парсинга
|
||||
// - Поддержка CDATA для многострочного content
|
||||
// - Валидация имён tools
|
||||
// Improvements:
|
||||
// - Better error handling for parsing
|
||||
// - CDATA support for multiline content
|
||||
// - Tool name validation
|
||||
```
|
||||
|
||||
**Изменения:**
|
||||
- [x] Добавить поддержку `<![CDATA[...]]>` для content
|
||||
- [x] Валидация: tool name должен быть из известного списка
|
||||
- [x] Улучшить сообщения об ошибках парсинга
|
||||
**Changes:**
|
||||
- [x] Add `<![CDATA[...]]>` support for content
|
||||
- [x] Validation: tool name must be from known list
|
||||
- [x] Improve parsing error messages
|
||||
|
||||
**Tests:**
|
||||
- [x] Обновить тесты OllamaClient
|
||||
- [x] Обновить тесты HandleMessage
|
||||
- [x] Добавить тесты ResponseParser для edge cases
|
||||
- [ ] E2E тест полного flow с XML (опционально, может быть в 0.20.0)
|
||||
- [x] Update OllamaClient tests
|
||||
- [x] Update HandleMessage tests
|
||||
- [x] Add ResponseParser tests for edge cases
|
||||
- [ ] E2E test for full XML flow (optional, may be in 0.20.0)
|
||||
|
||||
---
|
||||
|
||||
## Version 0.20.0 - Missing Use Cases 🔧
|
||||
## Version 0.20.0 - Missing Use Cases 🔧 ✅
|
||||
|
||||
**Priority:** HIGH
|
||||
**Status:** Pending
|
||||
**Status:** Complete (v0.20.0 released)
|
||||
|
||||
### 0.20.1 - IndexProject Use Case
|
||||
### 0.20.1 - IndexProject Use Case ✅
|
||||
|
||||
```typescript
|
||||
// src/application/use-cases/IndexProject.ts
|
||||
class IndexProject {
|
||||
constructor(
|
||||
private storage: IStorage,
|
||||
private indexer: IIndexer
|
||||
)
|
||||
constructor(storage: IStorage, projectRoot: string)
|
||||
|
||||
async execute(
|
||||
projectRoot: string,
|
||||
onProgress?: (progress: IndexProgress) => void
|
||||
options?: IndexProjectOptions
|
||||
): Promise<IndexingStats>
|
||||
// Full indexing pipeline:
|
||||
// 1. Scan files
|
||||
@@ -1496,50 +1493,51 @@ class IndexProject {
|
||||
```
|
||||
|
||||
**Deliverables:**
|
||||
- [ ] IndexProject use case implementation
|
||||
- [ ] Integration with CLI `index` command
|
||||
- [ ] Integration with `/reindex` slash command
|
||||
- [ ] Progress reporting via callback
|
||||
- [ ] Unit tests
|
||||
- [x] IndexProject use case implementation (184 LOC)
|
||||
- [x] Progress reporting via callback
|
||||
- [x] Unit tests (318 LOC)
|
||||
|
||||
### 0.20.2 - ExecuteTool Use Case
|
||||
### 0.20.2 - ExecuteTool Use Case ✅
|
||||
|
||||
```typescript
|
||||
// src/application/use-cases/ExecuteTool.ts
|
||||
class ExecuteTool {
|
||||
constructor(
|
||||
private tools: IToolRegistry,
|
||||
private storage: IStorage
|
||||
storage: IStorage,
|
||||
sessionStorage: ISessionStorage,
|
||||
tools: IToolRegistry,
|
||||
projectRoot: string
|
||||
)
|
||||
|
||||
async execute(
|
||||
toolName: string,
|
||||
params: Record<string, unknown>,
|
||||
context: ToolContext
|
||||
): Promise<ToolResult>
|
||||
toolCall: ToolCall,
|
||||
session: Session,
|
||||
options?: ExecuteToolOptions
|
||||
): Promise<ExecuteToolResult>
|
||||
// Orchestrates tool execution with:
|
||||
// - Parameter validation
|
||||
// - Confirmation flow
|
||||
// - Confirmation flow (with edit support)
|
||||
// - Undo stack management
|
||||
// - Storage updates
|
||||
}
|
||||
```
|
||||
|
||||
**Deliverables:**
|
||||
- [ ] ExecuteTool use case implementation
|
||||
- [ ] Refactor HandleMessage to use ExecuteTool
|
||||
- [ ] Unit tests
|
||||
- [x] ExecuteTool use case implementation (225 LOC)
|
||||
- [x] HandleMessage uses ExecuteTool
|
||||
- [x] Support for edited content from confirmation dialog
|
||||
- [ ] Dedicated unit tests (covered indirectly via integration)
|
||||
|
||||
**Tests:**
|
||||
- [ ] Unit tests for IndexProject
|
||||
- [ ] Unit tests for ExecuteTool
|
||||
- [x] Unit tests for IndexProject
|
||||
- [ ] Unit tests for ExecuteTool (optional - covered via integration)
|
||||
|
||||
---
|
||||
|
||||
## Version 0.21.0 - TUI Enhancements 🎨
|
||||
## Version 0.21.0 - TUI Enhancements 🎨 ✅
|
||||
|
||||
**Priority:** MEDIUM
|
||||
**Status:** In Progress (2/4 complete)
|
||||
**Status:** Complete (v0.21.0 released)
|
||||
|
||||
### 0.21.1 - useAutocomplete Hook ✅
|
||||
|
||||
@@ -1596,52 +1594,45 @@ interface ConfirmDialogProps {
|
||||
- [x] ConfirmationResult type with editedContent field
|
||||
- [x] All existing tests passing (1484 tests)
|
||||
|
||||
### 0.21.3 - Multiline Input
|
||||
### 0.21.3 - Multiline Input ✅
|
||||
|
||||
```typescript
|
||||
// src/tui/components/Input.tsx enhancements
|
||||
// src/tui/components/Input.tsx
|
||||
interface InputProps {
|
||||
// ... existing props
|
||||
multiline?: boolean | "auto" // auto = detect based on content
|
||||
}
|
||||
|
||||
// Shift+Enter for new line
|
||||
// Auto-expand height
|
||||
```
|
||||
|
||||
**Deliverables:**
|
||||
- [ ] Multiline support in Input component
|
||||
- [ ] Shift+Enter handling
|
||||
- [ ] Auto-height adjustment
|
||||
- [ ] Config option: `input.multiline`
|
||||
- [ ] Unit tests
|
||||
- [x] Multiline support in Input component
|
||||
- [x] Line navigation support
|
||||
- [x] Auto-expand based on content
|
||||
- [x] Unit tests (37 tests)
|
||||
|
||||
### 0.21.4 - Syntax Highlighting in DiffView
|
||||
### 0.21.4 - Syntax Highlighting in DiffView ✅
|
||||
|
||||
```typescript
|
||||
// src/tui/components/DiffView.tsx enhancements
|
||||
// Full syntax highlighting for code in diff
|
||||
// src/tui/utils/syntax-highlighter.ts (167 LOC)
|
||||
// Custom tokenizer for TypeScript/JavaScript/JSON/YAML
|
||||
// Highlights keywords, strings, comments, numbers, operators
|
||||
|
||||
interface DiffViewProps {
|
||||
// ... existing props
|
||||
language?: "ts" | "tsx" | "js" | "jsx"
|
||||
language?: Language
|
||||
syntaxHighlight?: boolean
|
||||
}
|
||||
|
||||
// Use ink-syntax-highlight or custom tokenizer
|
||||
```
|
||||
|
||||
**Deliverables:**
|
||||
- [ ] Syntax highlighting integration
|
||||
- [ ] Language detection from file extension
|
||||
- [ ] Config option: `edit.syntaxHighlight`
|
||||
- [ ] Unit tests
|
||||
- [x] Syntax highlighter implementation (167 LOC)
|
||||
- [x] Language detection from file extension
|
||||
- [x] Integration with DiffView and ConfirmDialog
|
||||
- [x] Unit tests (24 tests)
|
||||
|
||||
**Tests:**
|
||||
- [ ] Unit tests for useAutocomplete
|
||||
- [ ] Unit tests for enhanced ConfirmDialog
|
||||
- [ ] Unit tests for multiline Input
|
||||
- [ ] Unit tests for syntax highlighting
|
||||
- [x] Unit tests for useAutocomplete (21 tests)
|
||||
- [x] Unit tests for enhanced ConfirmDialog
|
||||
- [x] Unit tests for multiline Input (37 tests)
|
||||
- [x] Unit tests for syntax highlighting (24 tests)
|
||||
|
||||
---
|
||||
|
||||
@@ -1741,30 +1732,30 @@ export const CommandsConfigSchema = z.object({
|
||||
|
||||
---
|
||||
|
||||
## Version 0.23.0 - JSON/YAML & Symlinks 📄
|
||||
## Version 0.23.0 - JSON/YAML & Symlinks 📄 ✅
|
||||
|
||||
**Priority:** LOW
|
||||
**Status:** Pending
|
||||
**Status:** Complete (v0.23.0 released)
|
||||
|
||||
### 0.23.1 - JSON/YAML AST Parsing
|
||||
### 0.23.1 - JSON/YAML AST Parsing ✅
|
||||
|
||||
```typescript
|
||||
// src/infrastructure/indexer/ASTParser.ts enhancements
|
||||
type Language = "ts" | "tsx" | "js" | "jsx" | "json" | "yaml"
|
||||
|
||||
// For JSON: extract keys, structure
|
||||
// For YAML: extract keys, structure
|
||||
// Use tree-sitter-json and tree-sitter-yaml
|
||||
// For JSON: extract keys, structure (tree-sitter-json)
|
||||
// For YAML: extract keys, structure (yaml npm package)
|
||||
```
|
||||
|
||||
**Deliverables:**
|
||||
- [ ] Add tree-sitter-json dependency
|
||||
- [ ] Add tree-sitter-yaml dependency
|
||||
- [ ] JSON parsing in ASTParser
|
||||
- [ ] YAML parsing in ASTParser
|
||||
- [ ] Unit tests
|
||||
**Note:** YAML parsing uses `yaml` npm package instead of `tree-sitter-yaml` due to native binding compatibility issues.
|
||||
|
||||
### 0.23.2 - Symlinks Metadata
|
||||
**Deliverables:**
|
||||
- [x] Add tree-sitter-json dependency
|
||||
- [x] JSON parsing in ASTParser
|
||||
- [x] YAML parsing in ASTParser (using `yaml` package)
|
||||
- [x] Unit tests (2 tests)
|
||||
|
||||
### 0.23.2 - Symlinks Metadata ✅
|
||||
|
||||
```typescript
|
||||
// src/domain/services/IIndexer.ts enhancements
|
||||
@@ -1775,20 +1766,260 @@ export interface ScanResult {
|
||||
lastModified: number
|
||||
symlinkTarget?: string // <-- NEW: target path for symlinks
|
||||
}
|
||||
|
||||
// Store symlink metadata in Redis
|
||||
// project:{name}:meta includes symlink info
|
||||
```
|
||||
|
||||
**Deliverables:**
|
||||
- [ ] Add symlinkTarget to ScanResult
|
||||
- [ ] FileScanner extracts symlink targets
|
||||
- [ ] Store symlink metadata in Redis
|
||||
- [ ] Unit tests
|
||||
- [x] Add symlinkTarget to ScanResult
|
||||
- [x] FileScanner extracts symlink targets via safeReadlink()
|
||||
- [x] Unit tests (FileScanner tests)
|
||||
|
||||
**Tests:**
|
||||
- [ ] Unit tests for JSON/YAML parsing
|
||||
- [ ] Unit tests for symlink handling
|
||||
- [x] Unit tests for JSON/YAML parsing (2 tests)
|
||||
- [x] Unit tests for symlink handling (FileScanner tests)
|
||||
|
||||
---
|
||||
|
||||
## Version 0.24.0 - Rich Initial Context 📋 ✅
|
||||
|
||||
**Priority:** HIGH
|
||||
**Status:** Complete (v0.24.0 released)
|
||||
|
||||
Enhance initial context for LLM: add function signatures, interface field types, and enum values. This allows LLM to answer questions about types and parameters without tool calls.
|
||||
|
||||
### 0.24.1 - Function Signatures with Types ⭐ ✅
|
||||
|
||||
**Problem:** Currently LLM only sees function names: `fn: getUser, createUser`
|
||||
**Solution:** Show full signatures: `async getUser(id: string): Promise<User>`
|
||||
|
||||
```typescript
|
||||
// src/infrastructure/llm/prompts.ts changes
|
||||
|
||||
// BEFORE:
|
||||
// - src/services/user.ts [fn: getUser, createUser]
|
||||
|
||||
// AFTER:
|
||||
// ### src/services/user.ts
|
||||
// - async getUser(id: string): Promise<User>
|
||||
// - async createUser(data: UserDTO): Promise<User>
|
||||
// - validateEmail(email: string): boolean
|
||||
```
|
||||
|
||||
**Changes:**
|
||||
- [x] Extend `FunctionInfo` in FileAST for parameter types and return type (already existed)
|
||||
- [x] Update `ASTParser.ts` to extract parameter types and return types (arrow functions fixed)
|
||||
- [x] Update `formatFileSummary()` in prompts.ts to output signatures
|
||||
- [x] Add `includeSignatures: boolean` option to config
|
||||
|
||||
**Why:** LLM won't hallucinate parameters and return types.
|
||||
|
||||
### 0.24.2 - Interface/Type Field Definitions ⭐ ✅
|
||||
|
||||
**Problem:** LLM only sees `interface: User, UserDTO`
|
||||
**Solution:** Show fields: `User { id: string, name: string, email: string }`
|
||||
|
||||
```typescript
|
||||
// BEFORE:
|
||||
// - src/types/user.ts [interface: User, UserDTO]
|
||||
|
||||
// AFTER:
|
||||
// ### src/types/user.ts
|
||||
// - interface User { id: string, name: string, email: string, createdAt: Date }
|
||||
// - interface UserDTO { name: string, email: string }
|
||||
// - type UserId = string
|
||||
```
|
||||
|
||||
**Changes:**
|
||||
- [x] Extend `InterfaceInfo` in FileAST for field types (already existed)
|
||||
- [x] Update `ASTParser.ts` to extract interface fields (already existed)
|
||||
- [x] Update `formatFileSummary()` to output fields
|
||||
- [x] Handle type aliases with their definitions
|
||||
|
||||
**Why:** LLM knows data structure, won't invent fields.
|
||||
|
||||
### 0.24.3 - Enum Value Definitions ⭐ ✅
|
||||
|
||||
**Problem:** LLM only sees `type: Status`
|
||||
**Solution:** Show values: `Status { Active=1, Inactive=0, Pending=2 }`
|
||||
|
||||
```typescript
|
||||
// BEFORE:
|
||||
// - src/types/enums.ts [type: Status, Role]
|
||||
|
||||
// AFTER:
|
||||
// ### src/types/enums.ts
|
||||
// - enum Status { Active=1, Inactive=0, Pending=2 }
|
||||
// - enum Role { Admin="admin", User="user" }
|
||||
```
|
||||
|
||||
**Changes:**
|
||||
- [x] Add `EnumInfo` to FileAST with members and values
|
||||
- [x] Update `ASTParser.ts` to extract enum members
|
||||
- [x] Update `formatFileSummary()` to output enum values
|
||||
|
||||
**Why:** LLM knows valid enum values.
|
||||
|
||||
### 0.24.4 - Decorator Extraction ⭐ ✅
|
||||
|
||||
**Problem:** LLM doesn't see decorators (important for NestJS, Angular)
|
||||
**Solution:** Show decorators in context
|
||||
|
||||
```typescript
|
||||
// AFTER:
|
||||
// ### src/controllers/user.controller.ts
|
||||
// - @Controller('users') class UserController
|
||||
// - @Get(':id') async getUser(id: string): Promise<User>
|
||||
// - @Post() @Body() async createUser(data: UserDTO): Promise<User>
|
||||
```
|
||||
|
||||
**Changes:**
|
||||
- [x] Add `decorators: string[]` to FunctionInfo, MethodInfo, and ClassInfo
|
||||
- [x] Update `ASTParser.ts` to extract decorators via `extractNodeDecorators()` and `extractDecoratorsFromSiblings()`
|
||||
- [x] Update `prompts.ts` to display decorators via `formatDecoratorsPrefix()`
|
||||
|
||||
**Why:** LLM understands routing, DI, guards in NestJS/Angular.
|
||||
|
||||
**Tests:**
|
||||
- [x] Unit tests for ASTParser decorator extraction (14 tests)
|
||||
- [x] Unit tests for prompts decorator formatting (6 tests)
|
||||
|
||||
---
|
||||
|
||||
## Version 0.27.0 - Inline Dependency Graph 📊 ✅
|
||||
|
||||
**Priority:** MEDIUM
|
||||
**Status:** Complete (v0.27.0 released)
|
||||
|
||||
### Description
|
||||
|
||||
**Problem:** LLM doesn't see file relationships without tool calls
|
||||
**Solution:** Show dependency graph in context
|
||||
|
||||
```typescript
|
||||
// Add to 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/*
|
||||
```
|
||||
|
||||
**Changes:**
|
||||
- [x] Add `formatDependencyGraph()` to prompts.ts
|
||||
- [x] Use data from `FileMeta.dependencies` and `FileMeta.dependents`
|
||||
- [x] Group by hub files (many connections)
|
||||
- [x] Add `includeDepsGraph: boolean` option to config
|
||||
|
||||
**Tests:**
|
||||
- [x] Unit tests for formatDependencyGraph() (16 tests)
|
||||
- [x] Unit tests for includeDepsGraph config option (5 tests)
|
||||
|
||||
**Why:** LLM sees architecture without tool call.
|
||||
|
||||
---
|
||||
|
||||
## Version 0.28.0 - Circular Dependencies in Context 🔄 ✅
|
||||
|
||||
**Priority:** MEDIUM
|
||||
**Status:** Complete (v0.28.0 released)
|
||||
|
||||
### Description
|
||||
|
||||
**Problem:** Circular deps are computed but not shown in context
|
||||
**Solution:** Show cycles immediately
|
||||
|
||||
```typescript
|
||||
// Add to initial context:
|
||||
|
||||
// ## ⚠️ Circular Dependencies
|
||||
// - services/user → services/auth → services/user
|
||||
// - utils/a → utils/b → utils/c → utils/a
|
||||
```
|
||||
|
||||
**Changes:**
|
||||
- [x] Add `formatCircularDeps()` to prompts.ts
|
||||
- [x] Add `includeCircularDeps: boolean` config option (default: true)
|
||||
- [x] Add `circularDeps: string[][]` parameter to `BuildContextOptions`
|
||||
- [x] Integrate into `buildInitialContext()`
|
||||
|
||||
**Tests:**
|
||||
- [x] Unit tests for formatCircularDeps() (12 tests)
|
||||
- [x] Unit tests for buildInitialContext with includeCircularDeps (6 tests)
|
||||
- [x] Unit tests for includeCircularDeps config option (5 tests)
|
||||
|
||||
**Why:** LLM immediately sees architecture problems.
|
||||
|
||||
---
|
||||
|
||||
## Version 0.29.0 - Impact Score 📈 ✅
|
||||
|
||||
**Priority:** MEDIUM
|
||||
**Status:** Complete (v0.29.0 released)
|
||||
|
||||
### Description
|
||||
|
||||
**Problem:** LLM doesn't know which files are critical
|
||||
**Solution:** Show impact score (% of codebase that depends on file)
|
||||
|
||||
```typescript
|
||||
// Add to 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 |
|
||||
```
|
||||
|
||||
**Changes:**
|
||||
- [x] Add `impactScore: number` to FileMeta (0-100)
|
||||
- [x] Compute in MetaAnalyzer: (dependents.length / (totalFiles - 1)) * 100
|
||||
- [x] Add `formatHighImpactFiles()` to prompts.ts
|
||||
- [x] Show top-10 high impact files
|
||||
- [x] Add `includeHighImpactFiles` config option (default: true)
|
||||
|
||||
**Tests:**
|
||||
- [x] Unit tests for calculateImpactScore (9 tests)
|
||||
- [x] Unit tests for formatHighImpactFiles (14 tests)
|
||||
- [x] Unit tests for includeHighImpactFiles config (5 tests)
|
||||
|
||||
**Why:** LLM understands which files are critical for changes.
|
||||
|
||||
---
|
||||
|
||||
## Version 0.30.0 - Transitive Dependencies Count 🔢 ✅
|
||||
|
||||
**Priority:** MEDIUM
|
||||
**Status:** Complete (v0.30.0 released)
|
||||
|
||||
### Description
|
||||
|
||||
**Problem:** Currently only counting direct dependencies
|
||||
**Solution:** Add transitive dependencies to meta
|
||||
|
||||
```typescript
|
||||
// FileMeta additions:
|
||||
interface FileMeta {
|
||||
// existing...
|
||||
transitiveDepCount: number; // how many files depend on this (transitively)
|
||||
transitiveDepByCount: number; // how many files this depends on (transitively)
|
||||
}
|
||||
```
|
||||
|
||||
**Changes:**
|
||||
- [x] Add `transitiveDepCount` and `transitiveDepByCount` to FileMeta
|
||||
- [x] Add `computeTransitiveCounts()` to MetaAnalyzer
|
||||
- [x] Add `getTransitiveDependents()` with DFS and cycle detection
|
||||
- [x] Add `getTransitiveDependencies()` with DFS and cycle detection
|
||||
- [x] Use top-level caching for efficiency
|
||||
- [x] Handle circular dependencies gracefully (exclude self from count)
|
||||
|
||||
**Tests:**
|
||||
- [x] Unit tests for transitive dependencies computation (14 tests)
|
||||
- [x] Tests for circular dependencies
|
||||
- [x] Tests for diamond dependency patterns
|
||||
- [x] Tests for deep dependency chains
|
||||
- [x] Cache behavior tests
|
||||
|
||||
---
|
||||
|
||||
@@ -1803,10 +2034,12 @@ export interface ScanResult {
|
||||
- [x] Error handling complete ✅ (v0.16.0)
|
||||
- [ ] Performance optimized
|
||||
- [x] Documentation complete ✅ (v0.17.0)
|
||||
- [x] Test coverage ≥92% branches, ≥95% lines/functions/statements ✅ (92.01% branches, 97.84% lines, 99.16% functions, 97.84% statements - 1441 tests)
|
||||
- [x] Test coverage ≥91% branches, ≥95% lines/functions/statements ✅ (91.5% branches, 97.58% lines, 98.64% functions, 97.58% statements - 1840 tests)
|
||||
- [x] 0 ESLint errors ✅
|
||||
- [x] Examples working ✅ (v0.18.0)
|
||||
- [x] CHANGELOG.md up to date ✅
|
||||
- [x] Rich initial context (v0.24.0-v0.26.0) — function signatures, interface fields, enum values, decorators ✅
|
||||
- [x] Graph metrics in context (v0.27.0-v0.30.0) — dependency graph ✅, circular deps ✅, impact score ✅, transitive deps ✅
|
||||
|
||||
---
|
||||
|
||||
@@ -1875,11 +2108,17 @@ sessions:list # List<session_id>
|
||||
| Component | Tokens | % |
|
||||
|-----------|--------|---|
|
||||
| System prompt | ~2,000 | 1.5% |
|
||||
| Structure + AST | ~10,000 | 8% |
|
||||
| **Available** | ~116,000 | 90% |
|
||||
| Structure + AST (v0.23) | ~10,000 | 8% |
|
||||
| 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-05
|
||||
**Target Version:** 1.0.0
|
||||
**Current Version:** 0.22.1
|
||||
**Current Version:** 0.30.0
|
||||
**Next Milestones:** v1.0.0 (Production Ready)
|
||||
|
||||
> **Note:** Rich Initial Context complete ✅ (v0.24.0-v0.26.0). Graph Metrics complete ✅ (v0.27.0-v0.30.0). All feature milestones done, ready for v1.0.0 stabilization.
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@samiyev/ipuaro",
|
||||
"version": "0.22.4",
|
||||
"version": "0.30.1",
|
||||
"description": "Local AI agent for codebase operations with infinite context feeling",
|
||||
"author": "Fozilbek Samiyev <fozilbek.samiyev@gmail.com>",
|
||||
"license": "MIT",
|
||||
@@ -44,7 +44,9 @@
|
||||
"simple-git": "^3.27.0",
|
||||
"tree-sitter": "^0.21.1",
|
||||
"tree-sitter-javascript": "^0.21.0",
|
||||
"tree-sitter-json": "^0.24.8",
|
||||
"tree-sitter-typescript": "^0.21.2",
|
||||
"yaml": "^2.8.2",
|
||||
"zod": "^3.23.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -21,6 +21,7 @@ export interface ScanResult {
|
||||
type: "file" | "directory" | "symlink"
|
||||
size: number
|
||||
lastModified: number
|
||||
symlinkTarget?: string
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -46,7 +47,7 @@ export interface IIndexer {
|
||||
/**
|
||||
* Parse file content into AST.
|
||||
*/
|
||||
parseFile(content: string, language: "ts" | "tsx" | "js" | "jsx"): FileAST
|
||||
parseFile(content: string, language: "ts" | "tsx" | "js" | "jsx" | "json" | "yaml"): FileAST
|
||||
|
||||
/**
|
||||
* Analyze file and compute metadata.
|
||||
|
||||
@@ -52,6 +52,8 @@ export interface FunctionInfo {
|
||||
isExported: boolean
|
||||
/** Return type (if available) */
|
||||
returnType?: string
|
||||
/** Decorators applied to the function (e.g., ["@Get(':id')", "@Auth()"]) */
|
||||
decorators?: string[]
|
||||
}
|
||||
|
||||
export interface MethodInfo {
|
||||
@@ -69,6 +71,8 @@ export interface MethodInfo {
|
||||
visibility: "public" | "private" | "protected"
|
||||
/** Whether it's static */
|
||||
isStatic: boolean
|
||||
/** Decorators applied to the method (e.g., ["@Get(':id')", "@UseGuards(AuthGuard)"]) */
|
||||
decorators?: string[]
|
||||
}
|
||||
|
||||
export interface PropertyInfo {
|
||||
@@ -105,6 +109,8 @@ export interface ClassInfo {
|
||||
isExported: boolean
|
||||
/** Whether class is abstract */
|
||||
isAbstract: boolean
|
||||
/** Decorators applied to the class (e.g., ["@Controller('users')", "@Injectable()"]) */
|
||||
decorators?: string[]
|
||||
}
|
||||
|
||||
export interface InterfaceInfo {
|
||||
@@ -129,6 +135,30 @@ export interface TypeAliasInfo {
|
||||
line: number
|
||||
/** Whether it's exported */
|
||||
isExported: boolean
|
||||
/** Type definition (e.g., "string", "User & Admin", "{ id: string }") */
|
||||
definition?: string
|
||||
}
|
||||
|
||||
export interface EnumMemberInfo {
|
||||
/** Member name */
|
||||
name: string
|
||||
/** Member value (string or number, if specified) */
|
||||
value?: string | number
|
||||
}
|
||||
|
||||
export interface EnumInfo {
|
||||
/** Enum name */
|
||||
name: string
|
||||
/** Start line number */
|
||||
lineStart: number
|
||||
/** End line number */
|
||||
lineEnd: number
|
||||
/** Enum members with values */
|
||||
members: EnumMemberInfo[]
|
||||
/** Whether it's exported */
|
||||
isExported: boolean
|
||||
/** Whether it's a const enum */
|
||||
isConst: boolean
|
||||
}
|
||||
|
||||
export interface FileAST {
|
||||
@@ -144,6 +174,8 @@ export interface FileAST {
|
||||
interfaces: InterfaceInfo[]
|
||||
/** Type alias declarations */
|
||||
typeAliases: TypeAliasInfo[]
|
||||
/** Enum declarations */
|
||||
enums: EnumInfo[]
|
||||
/** Whether parsing encountered errors */
|
||||
parseError: boolean
|
||||
/** Parse error message if any */
|
||||
@@ -158,6 +190,7 @@ export function createEmptyFileAST(): FileAST {
|
||||
classes: [],
|
||||
interfaces: [],
|
||||
typeAliases: [],
|
||||
enums: [],
|
||||
parseError: false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,6 +26,12 @@ export interface FileMeta {
|
||||
isEntryPoint: boolean
|
||||
/** File type classification */
|
||||
fileType: "source" | "test" | "config" | "types" | "unknown"
|
||||
/** Impact score (0-100): percentage of codebase that depends on this file */
|
||||
impactScore: number
|
||||
/** Count of files that depend on this file transitively (including indirect dependents) */
|
||||
transitiveDepCount: number
|
||||
/** Count of files this file depends on transitively (including indirect dependencies) */
|
||||
transitiveDepByCount: number
|
||||
}
|
||||
|
||||
export function createFileMeta(partial: Partial<FileMeta> = {}): FileMeta {
|
||||
@@ -41,6 +47,9 @@ export function createFileMeta(partial: Partial<FileMeta> = {}): FileMeta {
|
||||
isHub: false,
|
||||
isEntryPoint: false,
|
||||
fileType: "unknown",
|
||||
impactScore: 0,
|
||||
transitiveDepCount: 0,
|
||||
transitiveDepByCount: 0,
|
||||
...partial,
|
||||
}
|
||||
}
|
||||
@@ -48,3 +57,20 @@ export function createFileMeta(partial: Partial<FileMeta> = {}): FileMeta {
|
||||
export function isHubFile(dependentCount: number): boolean {
|
||||
return dependentCount > 5
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate impact score based on number of dependents and total files.
|
||||
* Impact score represents what percentage of the codebase depends on this file.
|
||||
* @param dependentCount - Number of files that depend on this file
|
||||
* @param totalFiles - Total number of files in the project
|
||||
* @returns Impact score from 0 to 100
|
||||
*/
|
||||
export function calculateImpactScore(dependentCount: number, totalFiles: number): number {
|
||||
if (totalFiles <= 1) {
|
||||
return 0
|
||||
}
|
||||
// Exclude the file itself from the total
|
||||
const maxPossibleDependents = totalFiles - 1
|
||||
const score = (dependentCount / maxPossibleDependents) * 100
|
||||
return Math.round(Math.min(100, score))
|
||||
}
|
||||
|
||||
@@ -2,8 +2,11 @@ import { builtinModules } from "node:module"
|
||||
import Parser from "tree-sitter"
|
||||
import TypeScript from "tree-sitter-typescript"
|
||||
import JavaScript from "tree-sitter-javascript"
|
||||
import JSON from "tree-sitter-json"
|
||||
import * as yamlParser from "yaml"
|
||||
import {
|
||||
createEmptyFileAST,
|
||||
type EnumMemberInfo,
|
||||
type ExportInfo,
|
||||
type FileAST,
|
||||
type ImportInfo,
|
||||
@@ -13,7 +16,7 @@ import {
|
||||
} from "../../domain/value-objects/FileAST.js"
|
||||
import { FieldName, NodeType } from "./tree-sitter-types.js"
|
||||
|
||||
type Language = "ts" | "tsx" | "js" | "jsx"
|
||||
type Language = "ts" | "tsx" | "js" | "jsx" | "json" | "yaml"
|
||||
type SyntaxNode = Parser.SyntaxNode
|
||||
|
||||
/**
|
||||
@@ -39,12 +42,20 @@ export class ASTParser {
|
||||
jsParser.setLanguage(JavaScript)
|
||||
this.parsers.set("js", jsParser)
|
||||
this.parsers.set("jsx", jsParser)
|
||||
|
||||
const jsonParser = new Parser()
|
||||
jsonParser.setLanguage(JSON)
|
||||
this.parsers.set("json", jsonParser)
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse source code and extract AST information.
|
||||
*/
|
||||
parse(content: string, language: Language): FileAST {
|
||||
if (language === "yaml") {
|
||||
return this.parseYAML(content)
|
||||
}
|
||||
|
||||
const parser = this.parsers.get(language)
|
||||
if (!parser) {
|
||||
return {
|
||||
@@ -75,8 +86,77 @@ export class ASTParser {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse YAML content using yaml package.
|
||||
*/
|
||||
private parseYAML(content: string): FileAST {
|
||||
const ast = createEmptyFileAST()
|
||||
|
||||
try {
|
||||
const doc = yamlParser.parseDocument(content)
|
||||
|
||||
if (doc.errors.length > 0) {
|
||||
return {
|
||||
...createEmptyFileAST(),
|
||||
parseError: true,
|
||||
parseErrorMessage: doc.errors[0].message,
|
||||
}
|
||||
}
|
||||
|
||||
const contents = doc.contents
|
||||
|
||||
if (yamlParser.isSeq(contents)) {
|
||||
ast.exports.push({
|
||||
name: "(array)",
|
||||
line: 1,
|
||||
isDefault: false,
|
||||
kind: "variable",
|
||||
})
|
||||
} else if (yamlParser.isMap(contents)) {
|
||||
for (const item of contents.items) {
|
||||
if (yamlParser.isPair(item) && yamlParser.isScalar(item.key)) {
|
||||
const keyRange = item.key.range
|
||||
const line = keyRange ? this.getLineFromOffset(content, keyRange[0]) : 1
|
||||
ast.exports.push({
|
||||
name: String(item.key.value),
|
||||
line,
|
||||
isDefault: false,
|
||||
kind: "variable",
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ast
|
||||
} catch (error) {
|
||||
return {
|
||||
...createEmptyFileAST(),
|
||||
parseError: true,
|
||||
parseErrorMessage: error instanceof Error ? error.message : "YAML parse error",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get line number from character offset.
|
||||
*/
|
||||
private getLineFromOffset(content: string, offset: number): number {
|
||||
let line = 1
|
||||
for (let i = 0; i < offset && i < content.length; i++) {
|
||||
if (content[i] === "\n") {
|
||||
line++
|
||||
}
|
||||
}
|
||||
return line
|
||||
}
|
||||
|
||||
private extractAST(root: SyntaxNode, language: Language): FileAST {
|
||||
const ast = createEmptyFileAST()
|
||||
|
||||
if (language === "json") {
|
||||
return this.extractJSONStructure(root, ast)
|
||||
}
|
||||
|
||||
const isTypeScript = language === "ts" || language === "tsx"
|
||||
|
||||
for (const child of root.children) {
|
||||
@@ -113,6 +193,11 @@ export class ASTParser {
|
||||
this.extractTypeAlias(node, ast, false)
|
||||
}
|
||||
break
|
||||
case NodeType.ENUM_DECLARATION:
|
||||
if (isTypeScript) {
|
||||
this.extractEnum(node, ast, false)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
@@ -179,13 +264,15 @@ export class ASTParser {
|
||||
const declaration = node.childForFieldName(FieldName.DECLARATION)
|
||||
|
||||
if (declaration) {
|
||||
const decorators = this.extractDecoratorsFromSiblings(declaration)
|
||||
|
||||
switch (declaration.type) {
|
||||
case NodeType.FUNCTION_DECLARATION:
|
||||
this.extractFunction(declaration, ast, true)
|
||||
this.extractFunction(declaration, ast, true, decorators)
|
||||
this.addExportInfo(ast, declaration, "function", isDefault)
|
||||
break
|
||||
case NodeType.CLASS_DECLARATION:
|
||||
this.extractClass(declaration, ast, true)
|
||||
this.extractClass(declaration, ast, true, decorators)
|
||||
this.addExportInfo(ast, declaration, "class", isDefault)
|
||||
break
|
||||
case NodeType.INTERFACE_DECLARATION:
|
||||
@@ -196,6 +283,10 @@ export class ASTParser {
|
||||
this.extractTypeAlias(declaration, ast, true)
|
||||
this.addExportInfo(ast, declaration, "type", isDefault)
|
||||
break
|
||||
case NodeType.ENUM_DECLARATION:
|
||||
this.extractEnum(declaration, ast, true)
|
||||
this.addExportInfo(ast, declaration, "type", isDefault)
|
||||
break
|
||||
case NodeType.LEXICAL_DECLARATION:
|
||||
this.extractLexicalDeclaration(declaration, ast, true)
|
||||
break
|
||||
@@ -220,7 +311,12 @@ export class ASTParser {
|
||||
}
|
||||
}
|
||||
|
||||
private extractFunction(node: SyntaxNode, ast: FileAST, isExported: boolean): void {
|
||||
private extractFunction(
|
||||
node: SyntaxNode,
|
||||
ast: FileAST,
|
||||
isExported: boolean,
|
||||
externalDecorators: string[] = [],
|
||||
): void {
|
||||
const nameNode = node.childForFieldName(FieldName.NAME)
|
||||
if (!nameNode) {
|
||||
return
|
||||
@@ -230,6 +326,9 @@ export class ASTParser {
|
||||
const isAsync = node.children.some((c) => c.type === NodeType.ASYNC)
|
||||
const returnTypeNode = node.childForFieldName(FieldName.RETURN_TYPE)
|
||||
|
||||
const nodeDecorators = this.extractNodeDecorators(node)
|
||||
const decorators = [...externalDecorators, ...nodeDecorators]
|
||||
|
||||
ast.functions.push({
|
||||
name: nameNode.text,
|
||||
lineStart: node.startPosition.row + 1,
|
||||
@@ -238,6 +337,7 @@ export class ASTParser {
|
||||
isAsync,
|
||||
isExported,
|
||||
returnType: returnTypeNode?.text?.replace(/^:\s*/, ""),
|
||||
decorators,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -253,6 +353,7 @@ export class ASTParser {
|
||||
) {
|
||||
const params = this.extractParameters(valueNode)
|
||||
const isAsync = valueNode.children.some((c) => c.type === NodeType.ASYNC)
|
||||
const returnTypeNode = valueNode.childForFieldName(FieldName.RETURN_TYPE)
|
||||
|
||||
ast.functions.push({
|
||||
name: nameNode?.text ?? "",
|
||||
@@ -261,6 +362,8 @@ export class ASTParser {
|
||||
params,
|
||||
isAsync,
|
||||
isExported,
|
||||
returnType: returnTypeNode?.text?.replace(/^:\s*/, ""),
|
||||
decorators: [],
|
||||
})
|
||||
|
||||
if (isExported) {
|
||||
@@ -283,7 +386,12 @@ export class ASTParser {
|
||||
}
|
||||
}
|
||||
|
||||
private extractClass(node: SyntaxNode, ast: FileAST, isExported: boolean): void {
|
||||
private extractClass(
|
||||
node: SyntaxNode,
|
||||
ast: FileAST,
|
||||
isExported: boolean,
|
||||
externalDecorators: string[] = [],
|
||||
): void {
|
||||
const nameNode = node.childForFieldName(FieldName.NAME)
|
||||
if (!nameNode) {
|
||||
return
|
||||
@@ -294,14 +402,19 @@ export class ASTParser {
|
||||
const properties: PropertyInfo[] = []
|
||||
|
||||
if (body) {
|
||||
let pendingDecorators: string[] = []
|
||||
for (const member of body.children) {
|
||||
if (member.type === NodeType.METHOD_DEFINITION) {
|
||||
methods.push(this.extractMethod(member))
|
||||
if (member.type === NodeType.DECORATOR) {
|
||||
pendingDecorators.push(this.formatDecorator(member))
|
||||
} else if (member.type === NodeType.METHOD_DEFINITION) {
|
||||
methods.push(this.extractMethod(member, pendingDecorators))
|
||||
pendingDecorators = []
|
||||
} else if (
|
||||
member.type === NodeType.PUBLIC_FIELD_DEFINITION ||
|
||||
member.type === NodeType.FIELD_DEFINITION
|
||||
) {
|
||||
properties.push(this.extractProperty(member))
|
||||
pendingDecorators = []
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -309,6 +422,9 @@ export class ASTParser {
|
||||
const { extendsName, implementsList } = this.extractClassHeritage(node)
|
||||
const isAbstract = node.children.some((c) => c.type === NodeType.ABSTRACT)
|
||||
|
||||
const nodeDecorators = this.extractNodeDecorators(node)
|
||||
const decorators = [...externalDecorators, ...nodeDecorators]
|
||||
|
||||
ast.classes.push({
|
||||
name: nameNode.text,
|
||||
lineStart: node.startPosition.row + 1,
|
||||
@@ -319,6 +435,7 @@ export class ASTParser {
|
||||
implements: implementsList,
|
||||
isExported,
|
||||
isAbstract,
|
||||
decorators,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -372,7 +489,7 @@ export class ASTParser {
|
||||
}
|
||||
}
|
||||
|
||||
private extractMethod(node: SyntaxNode): MethodInfo {
|
||||
private extractMethod(node: SyntaxNode, decorators: string[] = []): MethodInfo {
|
||||
const nameNode = node.childForFieldName(FieldName.NAME)
|
||||
const params = this.extractParameters(node)
|
||||
const isAsync = node.children.some((c) => c.type === NodeType.ASYNC)
|
||||
@@ -394,6 +511,7 @@ export class ASTParser {
|
||||
isAsync,
|
||||
visibility,
|
||||
isStatic,
|
||||
decorators,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -473,13 +591,86 @@ export class ASTParser {
|
||||
return
|
||||
}
|
||||
|
||||
const valueNode = node.childForFieldName(FieldName.VALUE)
|
||||
const definition = valueNode?.text
|
||||
|
||||
ast.typeAliases.push({
|
||||
name: nameNode.text,
|
||||
line: node.startPosition.row + 1,
|
||||
isExported,
|
||||
definition,
|
||||
})
|
||||
}
|
||||
|
||||
private extractEnum(node: SyntaxNode, ast: FileAST, isExported: boolean): void {
|
||||
const nameNode = node.childForFieldName(FieldName.NAME)
|
||||
if (!nameNode) {
|
||||
return
|
||||
}
|
||||
|
||||
const body = node.childForFieldName(FieldName.BODY)
|
||||
const members: EnumMemberInfo[] = []
|
||||
|
||||
if (body) {
|
||||
for (const child of body.children) {
|
||||
if (child.type === NodeType.ENUM_ASSIGNMENT) {
|
||||
const memberName = child.childForFieldName(FieldName.NAME)
|
||||
const memberValue = child.childForFieldName(FieldName.VALUE)
|
||||
if (memberName) {
|
||||
members.push({
|
||||
name: memberName.text,
|
||||
value: this.parseEnumValue(memberValue),
|
||||
})
|
||||
}
|
||||
} else if (
|
||||
child.type === NodeType.IDENTIFIER ||
|
||||
child.type === NodeType.PROPERTY_IDENTIFIER
|
||||
) {
|
||||
members.push({
|
||||
name: child.text,
|
||||
value: undefined,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const isConst = node.children.some((c) => c.text === "const")
|
||||
|
||||
ast.enums.push({
|
||||
name: nameNode.text,
|
||||
lineStart: node.startPosition.row + 1,
|
||||
lineEnd: node.endPosition.row + 1,
|
||||
members,
|
||||
isExported,
|
||||
isConst,
|
||||
})
|
||||
}
|
||||
|
||||
private parseEnumValue(valueNode: SyntaxNode | null): string | number | undefined {
|
||||
if (!valueNode) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
const text = valueNode.text
|
||||
|
||||
if (valueNode.type === "number") {
|
||||
return Number(text)
|
||||
}
|
||||
|
||||
if (valueNode.type === "string") {
|
||||
return this.getStringValue(valueNode)
|
||||
}
|
||||
|
||||
if (valueNode.type === "unary_expression" && text.startsWith("-")) {
|
||||
const num = Number(text)
|
||||
if (!isNaN(num)) {
|
||||
return num
|
||||
}
|
||||
}
|
||||
|
||||
return text
|
||||
}
|
||||
|
||||
private extractParameters(node: SyntaxNode): ParameterInfo[] {
|
||||
const params: ParameterInfo[] = []
|
||||
const paramsNode = node.childForFieldName(FieldName.PARAMETERS)
|
||||
@@ -528,6 +719,49 @@ export class ASTParser {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Format a decorator node to a string like "@Get(':id')" or "@Injectable()".
|
||||
*/
|
||||
private formatDecorator(node: SyntaxNode): string {
|
||||
return node.text.replace(/\s+/g, " ").trim()
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract decorators that are direct children of a node.
|
||||
* In tree-sitter, decorators are children of the class/function declaration.
|
||||
*/
|
||||
private extractNodeDecorators(node: SyntaxNode): string[] {
|
||||
const decorators: string[] = []
|
||||
for (const child of node.children) {
|
||||
if (child.type === NodeType.DECORATOR) {
|
||||
decorators.push(this.formatDecorator(child))
|
||||
}
|
||||
}
|
||||
return decorators
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract decorators from sibling nodes before the current node.
|
||||
* Decorators appear as children before the declaration in export statements.
|
||||
*/
|
||||
private extractDecoratorsFromSiblings(node: SyntaxNode): string[] {
|
||||
const decorators: string[] = []
|
||||
const parent = node.parent
|
||||
if (!parent) {
|
||||
return decorators
|
||||
}
|
||||
|
||||
for (const sibling of parent.children) {
|
||||
if (sibling.type === NodeType.DECORATOR) {
|
||||
decorators.push(this.formatDecorator(sibling))
|
||||
} else if (sibling === node) {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return decorators
|
||||
}
|
||||
|
||||
private classifyImport(from: string): ImportInfo["type"] {
|
||||
if (from.startsWith(".") || from.startsWith("/")) {
|
||||
return "internal"
|
||||
@@ -548,4 +782,37 @@ export class ASTParser {
|
||||
}
|
||||
return text
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract structure from JSON file.
|
||||
* For JSON files, we extract top-level keys from objects.
|
||||
*/
|
||||
private extractJSONStructure(root: SyntaxNode, ast: FileAST): FileAST {
|
||||
for (const child of root.children) {
|
||||
if (child.type === "object") {
|
||||
this.extractJSONKeys(child, ast)
|
||||
}
|
||||
}
|
||||
return ast
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract keys from JSON object.
|
||||
*/
|
||||
private extractJSONKeys(node: SyntaxNode, ast: FileAST): void {
|
||||
for (const child of node.children) {
|
||||
if (child.type === "pair") {
|
||||
const keyNode = child.childForFieldName("key")
|
||||
if (keyNode) {
|
||||
const keyName = this.getStringValue(keyNode)
|
||||
ast.exports.push({
|
||||
name: keyName,
|
||||
line: keyNode.startPosition.row + 1,
|
||||
isDefault: false,
|
||||
kind: "variable",
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -96,12 +96,27 @@ export class FileScanner {
|
||||
const stats = await this.safeStats(fullPath)
|
||||
|
||||
if (stats) {
|
||||
yield {
|
||||
const type = stats.isSymbolicLink()
|
||||
? "symlink"
|
||||
: stats.isDirectory()
|
||||
? "directory"
|
||||
: "file"
|
||||
|
||||
const result: ScanResult = {
|
||||
path: relativePath,
|
||||
type: "file",
|
||||
type,
|
||||
size: stats.size,
|
||||
lastModified: stats.mtimeMs,
|
||||
}
|
||||
|
||||
if (type === "symlink") {
|
||||
const target = await this.safeReadlink(fullPath)
|
||||
if (target) {
|
||||
result.symlinkTarget = target
|
||||
}
|
||||
}
|
||||
|
||||
yield result
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -127,10 +142,22 @@ export class FileScanner {
|
||||
|
||||
/**
|
||||
* Safely get file stats without throwing.
|
||||
* Uses lstat to get information about symlinks themselves.
|
||||
*/
|
||||
private async safeStats(filePath: string): Promise<Stats | null> {
|
||||
try {
|
||||
return await fs.stat(filePath)
|
||||
return await fs.lstat(filePath)
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Safely read symlink target without throwing.
|
||||
*/
|
||||
private async safeReadlink(filePath: string): Promise<string | null> {
|
||||
try {
|
||||
return await fs.readlink(filePath)
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import * as path from "node:path"
|
||||
import {
|
||||
calculateImpactScore,
|
||||
type ComplexityMetrics,
|
||||
createFileMeta,
|
||||
type FileMeta,
|
||||
@@ -430,6 +431,7 @@ export class MetaAnalyzer {
|
||||
|
||||
/**
|
||||
* Batch analyze multiple files.
|
||||
* Computes impact scores and transitive dependencies after all files are analyzed.
|
||||
*/
|
||||
analyzeAll(files: Map<string, { ast: FileAST; content: string }>): Map<string, FileMeta> {
|
||||
const allASTs = new Map<string, FileAST>()
|
||||
@@ -443,6 +445,171 @@ export class MetaAnalyzer {
|
||||
results.set(filePath, meta)
|
||||
}
|
||||
|
||||
// Compute impact scores now that we know total file count
|
||||
const totalFiles = results.size
|
||||
for (const [, meta] of results) {
|
||||
meta.impactScore = calculateImpactScore(meta.dependents.length, totalFiles)
|
||||
}
|
||||
|
||||
// Compute transitive dependency counts
|
||||
this.computeTransitiveCounts(results)
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute transitive dependency counts for all files.
|
||||
* Uses DFS with memoization for efficiency.
|
||||
*/
|
||||
computeTransitiveCounts(metas: Map<string, FileMeta>): void {
|
||||
// Memoization caches
|
||||
const transitiveDepCache = new Map<string, Set<string>>()
|
||||
const transitiveDepByCache = new Map<string, Set<string>>()
|
||||
|
||||
// Compute transitive dependents (files that depend on this file, directly or transitively)
|
||||
for (const [filePath, meta] of metas) {
|
||||
const transitiveDeps = this.getTransitiveDependents(filePath, metas, transitiveDepCache)
|
||||
// Exclude the file itself from count (can happen in cycles)
|
||||
meta.transitiveDepCount = transitiveDeps.has(filePath)
|
||||
? transitiveDeps.size - 1
|
||||
: transitiveDeps.size
|
||||
}
|
||||
|
||||
// Compute transitive dependencies (files this file depends on, directly or transitively)
|
||||
for (const [filePath, meta] of metas) {
|
||||
const transitiveDepsBy = this.getTransitiveDependencies(
|
||||
filePath,
|
||||
metas,
|
||||
transitiveDepByCache,
|
||||
)
|
||||
// Exclude the file itself from count (can happen in cycles)
|
||||
meta.transitiveDepByCount = transitiveDepsBy.has(filePath)
|
||||
? transitiveDepsBy.size - 1
|
||||
: transitiveDepsBy.size
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all files that depend on the given file transitively.
|
||||
* Uses DFS with cycle detection. Caching only at the top level.
|
||||
*/
|
||||
getTransitiveDependents(
|
||||
filePath: string,
|
||||
metas: Map<string, FileMeta>,
|
||||
cache: Map<string, Set<string>>,
|
||||
visited?: Set<string>,
|
||||
): Set<string> {
|
||||
// Return cached result if available (only valid for top-level calls)
|
||||
if (!visited) {
|
||||
const cached = cache.get(filePath)
|
||||
if (cached) {
|
||||
return cached
|
||||
}
|
||||
}
|
||||
|
||||
const isTopLevel = !visited
|
||||
if (!visited) {
|
||||
visited = new Set()
|
||||
}
|
||||
|
||||
// Detect cycles
|
||||
if (visited.has(filePath)) {
|
||||
return new Set()
|
||||
}
|
||||
|
||||
visited.add(filePath)
|
||||
const result = new Set<string>()
|
||||
|
||||
const meta = metas.get(filePath)
|
||||
if (!meta) {
|
||||
if (isTopLevel) {
|
||||
cache.set(filePath, result)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// Add direct dependents
|
||||
for (const dependent of meta.dependents) {
|
||||
result.add(dependent)
|
||||
|
||||
// Recursively add transitive dependents
|
||||
const transitive = this.getTransitiveDependents(
|
||||
dependent,
|
||||
metas,
|
||||
cache,
|
||||
new Set(visited),
|
||||
)
|
||||
for (const t of transitive) {
|
||||
result.add(t)
|
||||
}
|
||||
}
|
||||
|
||||
// Only cache top-level results (not intermediate results during recursion)
|
||||
if (isTopLevel) {
|
||||
cache.set(filePath, result)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all files that the given file depends on transitively.
|
||||
* Uses DFS with cycle detection. Caching only at the top level.
|
||||
*/
|
||||
getTransitiveDependencies(
|
||||
filePath: string,
|
||||
metas: Map<string, FileMeta>,
|
||||
cache: Map<string, Set<string>>,
|
||||
visited?: Set<string>,
|
||||
): Set<string> {
|
||||
// Return cached result if available (only valid for top-level calls)
|
||||
if (!visited) {
|
||||
const cached = cache.get(filePath)
|
||||
if (cached) {
|
||||
return cached
|
||||
}
|
||||
}
|
||||
|
||||
const isTopLevel = !visited
|
||||
if (!visited) {
|
||||
visited = new Set()
|
||||
}
|
||||
|
||||
// Detect cycles
|
||||
if (visited.has(filePath)) {
|
||||
return new Set()
|
||||
}
|
||||
|
||||
visited.add(filePath)
|
||||
const result = new Set<string>()
|
||||
|
||||
const meta = metas.get(filePath)
|
||||
if (!meta) {
|
||||
if (isTopLevel) {
|
||||
cache.set(filePath, result)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// Add direct dependencies
|
||||
for (const dependency of meta.dependencies) {
|
||||
result.add(dependency)
|
||||
|
||||
// Recursively add transitive dependencies
|
||||
const transitive = this.getTransitiveDependencies(
|
||||
dependency,
|
||||
metas,
|
||||
cache,
|
||||
new Set(visited),
|
||||
)
|
||||
for (const t of transitive) {
|
||||
result.add(t)
|
||||
}
|
||||
}
|
||||
|
||||
// Only cache top-level results (not intermediate results during recursion)
|
||||
if (isTopLevel) {
|
||||
cache.set(filePath, result)
|
||||
}
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ export const NodeType = {
|
||||
CLASS_DECLARATION: "class_declaration",
|
||||
INTERFACE_DECLARATION: "interface_declaration",
|
||||
TYPE_ALIAS_DECLARATION: "type_alias_declaration",
|
||||
ENUM_DECLARATION: "enum_declaration",
|
||||
|
||||
// Clauses
|
||||
IMPORT_CLAUSE: "import_clause",
|
||||
@@ -37,6 +38,11 @@ export const NodeType = {
|
||||
FIELD_DEFINITION: "field_definition",
|
||||
PROPERTY_SIGNATURE: "property_signature",
|
||||
|
||||
// Enum members
|
||||
ENUM_BODY: "enum_body",
|
||||
ENUM_ASSIGNMENT: "enum_assignment",
|
||||
PROPERTY_IDENTIFIER: "property_identifier",
|
||||
|
||||
// Parameters
|
||||
REQUIRED_PARAMETER: "required_parameter",
|
||||
OPTIONAL_PARAMETER: "optional_parameter",
|
||||
@@ -57,6 +63,9 @@ export const NodeType = {
|
||||
DEFAULT: "default",
|
||||
ACCESSIBILITY_MODIFIER: "accessibility_modifier",
|
||||
READONLY: "readonly",
|
||||
|
||||
// Decorators
|
||||
DECORATOR: "decorator",
|
||||
} as const
|
||||
|
||||
export type NodeTypeValue = (typeof NodeType)[keyof typeof NodeType]
|
||||
|
||||
@@ -11,6 +11,17 @@ export interface ProjectStructure {
|
||||
directories: string[]
|
||||
}
|
||||
|
||||
/**
|
||||
* Options for building initial context.
|
||||
*/
|
||||
export interface BuildContextOptions {
|
||||
includeSignatures?: boolean
|
||||
includeDepsGraph?: boolean
|
||||
includeCircularDeps?: boolean
|
||||
includeHighImpactFiles?: boolean
|
||||
circularDeps?: string[][]
|
||||
}
|
||||
|
||||
/**
|
||||
* System prompt for the ipuaro AI agent.
|
||||
*/
|
||||
@@ -116,12 +127,38 @@ export function buildInitialContext(
|
||||
structure: ProjectStructure,
|
||||
asts: Map<string, FileAST>,
|
||||
metas?: Map<string, FileMeta>,
|
||||
options?: BuildContextOptions,
|
||||
): string {
|
||||
const sections: string[] = []
|
||||
const includeSignatures = options?.includeSignatures ?? true
|
||||
const includeDepsGraph = options?.includeDepsGraph ?? true
|
||||
const includeCircularDeps = options?.includeCircularDeps ?? true
|
||||
const includeHighImpactFiles = options?.includeHighImpactFiles ?? true
|
||||
|
||||
sections.push(formatProjectHeader(structure))
|
||||
sections.push(formatDirectoryTree(structure))
|
||||
sections.push(formatFileOverview(asts, metas))
|
||||
sections.push(formatFileOverview(asts, metas, includeSignatures))
|
||||
|
||||
if (includeDepsGraph && metas && metas.size > 0) {
|
||||
const depsGraph = formatDependencyGraph(metas)
|
||||
if (depsGraph) {
|
||||
sections.push(depsGraph)
|
||||
}
|
||||
}
|
||||
|
||||
if (includeHighImpactFiles && metas && metas.size > 0) {
|
||||
const highImpactSection = formatHighImpactFiles(metas)
|
||||
if (highImpactSection) {
|
||||
sections.push(highImpactSection)
|
||||
}
|
||||
}
|
||||
|
||||
if (includeCircularDeps && options?.circularDeps && options.circularDeps.length > 0) {
|
||||
const circularDepsSection = formatCircularDeps(options.circularDeps)
|
||||
if (circularDepsSection) {
|
||||
sections.push(circularDepsSection)
|
||||
}
|
||||
}
|
||||
|
||||
return sections.join("\n\n")
|
||||
}
|
||||
@@ -157,7 +194,11 @@ function formatDirectoryTree(structure: ProjectStructure): string {
|
||||
/**
|
||||
* 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 sortedPaths = [...asts.keys()].sort()
|
||||
@@ -168,16 +209,183 @@ function formatFileOverview(asts: Map<string, FileAST>, metas?: Map<string, File
|
||||
}
|
||||
|
||||
const meta = metas?.get(path)
|
||||
lines.push(formatFileSummary(path, ast, meta))
|
||||
lines.push(formatFileSummary(path, ast, meta, includeSignatures))
|
||||
}
|
||||
|
||||
return lines.join("\n")
|
||||
}
|
||||
|
||||
/**
|
||||
* Format a single file's AST summary.
|
||||
* Format decorators as a prefix string.
|
||||
* Example: "@Get(':id') @Auth() "
|
||||
*/
|
||||
function formatFileSummary(path: string, ast: FileAST, meta?: FileMeta): string {
|
||||
function formatDecoratorsPrefix(decorators: string[] | undefined): string {
|
||||
if (!decorators || decorators.length === 0) {
|
||||
return ""
|
||||
}
|
||||
return `${decorators.join(" ")} `
|
||||
}
|
||||
|
||||
/**
|
||||
* Format a function signature.
|
||||
*/
|
||||
function formatFunctionSignature(fn: FileAST["functions"][0]): string {
|
||||
const decoratorsPrefix = formatDecoratorsPrefix(fn.decorators)
|
||||
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 `${decoratorsPrefix}${asyncPrefix}${fn.name}(${params})${returnType}`
|
||||
}
|
||||
|
||||
/**
|
||||
* Format an interface signature with fields.
|
||||
* Example: "interface User extends Base { id: string, name: string, email?: string }"
|
||||
*/
|
||||
function formatInterfaceSignature(iface: FileAST["interfaces"][0]): string {
|
||||
const extList = iface.extends ?? []
|
||||
const ext = extList.length > 0 ? ` extends ${extList.join(", ")}` : ""
|
||||
|
||||
if (iface.properties.length === 0) {
|
||||
return `interface ${iface.name}${ext}`
|
||||
}
|
||||
|
||||
const fields = iface.properties
|
||||
.map((p) => {
|
||||
const readonly = p.isReadonly ? "readonly " : ""
|
||||
const optional = p.name.endsWith("?") ? "" : ""
|
||||
const type = p.type ? `: ${p.type}` : ""
|
||||
return `${readonly}${p.name}${optional}${type}`
|
||||
})
|
||||
.join(", ")
|
||||
|
||||
return `interface ${iface.name}${ext} { ${fields} }`
|
||||
}
|
||||
|
||||
/**
|
||||
* Format a type alias signature with definition.
|
||||
* Example: "type UserId = string" or "type Handler = (event: Event) => void"
|
||||
*/
|
||||
function formatTypeAliasSignature(type: FileAST["typeAliases"][0]): string {
|
||||
if (!type.definition) {
|
||||
return `type ${type.name}`
|
||||
}
|
||||
|
||||
const definition = truncateDefinition(type.definition, 80)
|
||||
return `type ${type.name} = ${definition}`
|
||||
}
|
||||
|
||||
/**
|
||||
* Format an enum signature with members and values.
|
||||
* Example: "enum Status { Active=1, Inactive=0, Pending=2 }"
|
||||
* Example: "const enum Role { Admin="admin", User="user" }"
|
||||
*/
|
||||
function formatEnumSignature(enumInfo: FileAST["enums"][0]): string {
|
||||
const constPrefix = enumInfo.isConst ? "const " : ""
|
||||
|
||||
if (enumInfo.members.length === 0) {
|
||||
return `${constPrefix}enum ${enumInfo.name}`
|
||||
}
|
||||
|
||||
const membersStr = enumInfo.members
|
||||
.map((m) => {
|
||||
if (m.value === undefined) {
|
||||
return m.name
|
||||
}
|
||||
const valueStr = typeof m.value === "string" ? `"${m.value}"` : String(m.value)
|
||||
return `${m.name}=${valueStr}`
|
||||
})
|
||||
.join(", ")
|
||||
|
||||
const result = `${constPrefix}enum ${enumInfo.name} { ${membersStr} }`
|
||||
|
||||
if (result.length > 100) {
|
||||
return truncateDefinition(result, 100)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* Truncate long type definitions for display.
|
||||
*/
|
||||
function truncateDefinition(definition: string, maxLength: number): string {
|
||||
const normalized = definition.replace(/\s+/g, " ").trim()
|
||||
if (normalized.length <= maxLength) {
|
||||
return normalized
|
||||
}
|
||||
return `${normalized.slice(0, maxLength - 3)}...`
|
||||
}
|
||||
|
||||
/**
|
||||
* Format a single file's AST summary.
|
||||
* When includeSignatures is true, shows full function signatures.
|
||||
* 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 decoratorsPrefix = formatDecoratorsPrefix(cls.decorators)
|
||||
const ext = cls.extends ? ` extends ${cls.extends}` : ""
|
||||
const impl = cls.implements.length > 0 ? ` implements ${cls.implements.join(", ")}` : ""
|
||||
lines.push(`- ${decoratorsPrefix}class ${cls.name}${ext}${impl}`)
|
||||
}
|
||||
}
|
||||
|
||||
if (ast.interfaces.length > 0) {
|
||||
for (const iface of ast.interfaces) {
|
||||
lines.push(`- ${formatInterfaceSignature(iface)}`)
|
||||
}
|
||||
}
|
||||
|
||||
if (ast.typeAliases.length > 0) {
|
||||
for (const type of ast.typeAliases) {
|
||||
lines.push(`- ${formatTypeAliasSignature(type)}`)
|
||||
}
|
||||
}
|
||||
|
||||
if (ast.enums && ast.enums.length > 0) {
|
||||
for (const enumInfo of ast.enums) {
|
||||
lines.push(`- ${formatEnumSignature(enumInfo)}`)
|
||||
}
|
||||
}
|
||||
|
||||
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[] = []
|
||||
|
||||
if (ast.functions.length > 0) {
|
||||
@@ -200,9 +408,12 @@ function formatFileSummary(path: string, ast: FileAST, meta?: FileMeta): string
|
||||
parts.push(`type: ${names}`)
|
||||
}
|
||||
|
||||
const summary = parts.length > 0 ? ` [${parts.join(" | ")}]` : ""
|
||||
const flags = formatFileFlags(meta)
|
||||
if (ast.enums && ast.enums.length > 0) {
|
||||
const names = ast.enums.map((e) => e.name).join(", ")
|
||||
parts.push(`enum: ${names}`)
|
||||
}
|
||||
|
||||
const summary = parts.length > 0 ? ` [${parts.join(" | ")}]` : ""
|
||||
return `- ${path}${summary}${flags}`
|
||||
}
|
||||
|
||||
@@ -231,6 +442,220 @@ function formatFileFlags(meta?: FileMeta): string {
|
||||
return flags.length > 0 ? ` (${flags.join(", ")})` : ""
|
||||
}
|
||||
|
||||
/**
|
||||
* Shorten a file path for display in dependency graph.
|
||||
* Removes common prefixes like "src/" and file extensions.
|
||||
*/
|
||||
function shortenPath(path: string): string {
|
||||
let short = path
|
||||
if (short.startsWith("src/")) {
|
||||
short = short.slice(4)
|
||||
}
|
||||
// Remove common extensions
|
||||
short = short.replace(/\.(ts|tsx|js|jsx)$/, "")
|
||||
// Remove /index suffix
|
||||
short = short.replace(/\/index$/, "")
|
||||
return short
|
||||
}
|
||||
|
||||
/**
|
||||
* Format a single dependency graph entry.
|
||||
* Format: "path: → dep1, dep2 ← dependent1, dependent2"
|
||||
*/
|
||||
function formatDepsEntry(path: string, dependencies: string[], dependents: string[]): string {
|
||||
const parts: string[] = []
|
||||
const shortPath = shortenPath(path)
|
||||
|
||||
if (dependencies.length > 0) {
|
||||
const deps = dependencies.map(shortenPath).join(", ")
|
||||
parts.push(`→ ${deps}`)
|
||||
}
|
||||
|
||||
if (dependents.length > 0) {
|
||||
const deps = dependents.map(shortenPath).join(", ")
|
||||
parts.push(`← ${deps}`)
|
||||
}
|
||||
|
||||
if (parts.length === 0) {
|
||||
return ""
|
||||
}
|
||||
|
||||
return `${shortPath}: ${parts.join(" ")}`
|
||||
}
|
||||
|
||||
/**
|
||||
* Format dependency graph for all files.
|
||||
* Shows hub files first, then files with dependencies/dependents.
|
||||
*
|
||||
* Format:
|
||||
* ## Dependency Graph
|
||||
* services/user: → types/user, utils/validation ← controllers/user
|
||||
* services/auth: → services/user, utils/jwt ← controllers/auth
|
||||
*/
|
||||
export function formatDependencyGraph(metas: Map<string, FileMeta>): string | null {
|
||||
if (metas.size === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
const entries: { path: string; deps: string[]; dependents: string[]; isHub: boolean }[] = []
|
||||
|
||||
for (const [path, meta] of metas) {
|
||||
// Only include files that have connections
|
||||
if (meta.dependencies.length > 0 || meta.dependents.length > 0) {
|
||||
entries.push({
|
||||
path,
|
||||
deps: meta.dependencies,
|
||||
dependents: meta.dependents,
|
||||
isHub: meta.isHub,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if (entries.length === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
// Sort: hubs first, then by total connections (desc), then by path
|
||||
entries.sort((a, b) => {
|
||||
if (a.isHub !== b.isHub) {
|
||||
return a.isHub ? -1 : 1
|
||||
}
|
||||
const aTotal = a.deps.length + a.dependents.length
|
||||
const bTotal = b.deps.length + b.dependents.length
|
||||
if (aTotal !== bTotal) {
|
||||
return bTotal - aTotal
|
||||
}
|
||||
return a.path.localeCompare(b.path)
|
||||
})
|
||||
|
||||
const lines: string[] = ["## Dependency Graph", ""]
|
||||
|
||||
for (const entry of entries) {
|
||||
const line = formatDepsEntry(entry.path, entry.deps, entry.dependents)
|
||||
if (line) {
|
||||
lines.push(line)
|
||||
}
|
||||
}
|
||||
|
||||
// Return null if only header (no actual entries)
|
||||
if (lines.length <= 2) {
|
||||
return null
|
||||
}
|
||||
|
||||
return lines.join("\n")
|
||||
}
|
||||
|
||||
/**
|
||||
* Format circular dependencies for display in context.
|
||||
* Shows warning section with cycle chains.
|
||||
*
|
||||
* Format:
|
||||
* ## ⚠️ Circular Dependencies
|
||||
* - services/user → services/auth → services/user
|
||||
* - utils/a → utils/b → utils/c → utils/a
|
||||
*/
|
||||
export function formatCircularDeps(cycles: string[][]): string | null {
|
||||
if (!cycles || cycles.length === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
const lines: string[] = ["## ⚠️ Circular Dependencies", ""]
|
||||
|
||||
for (const cycle of cycles) {
|
||||
if (cycle.length === 0) {
|
||||
continue
|
||||
}
|
||||
const formattedCycle = cycle.map(shortenPath).join(" → ")
|
||||
lines.push(`- ${formattedCycle}`)
|
||||
}
|
||||
|
||||
// Return null if only header (no actual cycles)
|
||||
if (lines.length <= 2) {
|
||||
return null
|
||||
}
|
||||
|
||||
return lines.join("\n")
|
||||
}
|
||||
|
||||
/**
|
||||
* Format high impact files table for display in context.
|
||||
* Shows files with highest impact scores (most dependents).
|
||||
* Includes both direct and transitive dependent counts.
|
||||
*
|
||||
* Format:
|
||||
* ## High Impact Files
|
||||
* | File | Impact | Direct | Transitive |
|
||||
* |------|--------|--------|------------|
|
||||
* | src/utils/validation.ts | 67% | 12 | 24 |
|
||||
*
|
||||
* @param metas - Map of file paths to their metadata
|
||||
* @param limit - Maximum number of files to show (default: 10)
|
||||
* @param minImpact - Minimum impact score to include (default: 5)
|
||||
*/
|
||||
export function formatHighImpactFiles(
|
||||
metas: Map<string, FileMeta>,
|
||||
limit = 10,
|
||||
minImpact = 5,
|
||||
): string | null {
|
||||
if (metas.size === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
// Collect files with impact score >= minImpact
|
||||
const impactFiles: {
|
||||
path: string
|
||||
impact: number
|
||||
dependents: number
|
||||
transitive: number
|
||||
}[] = []
|
||||
|
||||
for (const [path, meta] of metas) {
|
||||
if (meta.impactScore >= minImpact) {
|
||||
impactFiles.push({
|
||||
path,
|
||||
impact: meta.impactScore,
|
||||
dependents: meta.dependents.length,
|
||||
transitive: meta.transitiveDepCount,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if (impactFiles.length === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
// Sort by transitive count descending, then by impact, then by path
|
||||
impactFiles.sort((a, b) => {
|
||||
if (a.transitive !== b.transitive) {
|
||||
return b.transitive - a.transitive
|
||||
}
|
||||
if (a.impact !== b.impact) {
|
||||
return b.impact - a.impact
|
||||
}
|
||||
return a.path.localeCompare(b.path)
|
||||
})
|
||||
|
||||
// Take top N files
|
||||
const topFiles = impactFiles.slice(0, limit)
|
||||
|
||||
const lines: string[] = [
|
||||
"## High Impact Files",
|
||||
"",
|
||||
"| File | Impact | Direct | Transitive |",
|
||||
"|------|--------|--------|------------|",
|
||||
]
|
||||
|
||||
for (const file of topFiles) {
|
||||
const shortPath = shortenPath(file.path)
|
||||
const impact = `${String(file.impact)}%`
|
||||
const direct = String(file.dependents)
|
||||
const transitive = String(file.transitive)
|
||||
lines.push(`| ${shortPath} | ${impact} | ${direct} | ${transitive} |`)
|
||||
}
|
||||
|
||||
return lines.join("\n")
|
||||
}
|
||||
|
||||
/**
|
||||
* Format line range for display.
|
||||
*/
|
||||
|
||||
@@ -114,6 +114,10 @@ export const ContextConfigSchema = z.object({
|
||||
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),
|
||||
includeDepsGraph: z.boolean().default(true),
|
||||
includeCircularDeps: z.boolean().default(true),
|
||||
includeHighImpactFiles: z.boolean().default(true),
|
||||
})
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import { describe, it, expect } from "vitest"
|
||||
import { createFileMeta, isHubFile } from "../../../../src/domain/value-objects/FileMeta.js"
|
||||
import {
|
||||
calculateImpactScore,
|
||||
createFileMeta,
|
||||
isHubFile,
|
||||
} from "../../../../src/domain/value-objects/FileMeta.js"
|
||||
|
||||
describe("FileMeta", () => {
|
||||
describe("createFileMeta", () => {
|
||||
@@ -15,6 +19,7 @@ describe("FileMeta", () => {
|
||||
expect(meta.isHub).toBe(false)
|
||||
expect(meta.isEntryPoint).toBe(false)
|
||||
expect(meta.fileType).toBe("unknown")
|
||||
expect(meta.impactScore).toBe(0)
|
||||
})
|
||||
|
||||
it("should merge partial values", () => {
|
||||
@@ -42,4 +47,51 @@ describe("FileMeta", () => {
|
||||
expect(isHubFile(0)).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe("calculateImpactScore", () => {
|
||||
it("should return 0 for file with 0 dependents", () => {
|
||||
expect(calculateImpactScore(0, 10)).toBe(0)
|
||||
})
|
||||
|
||||
it("should return 0 when totalFiles is 0", () => {
|
||||
expect(calculateImpactScore(5, 0)).toBe(0)
|
||||
})
|
||||
|
||||
it("should return 0 when totalFiles is 1", () => {
|
||||
expect(calculateImpactScore(0, 1)).toBe(0)
|
||||
})
|
||||
|
||||
it("should calculate correct percentage", () => {
|
||||
// 5 dependents out of 10 files (excluding itself = 9 possible)
|
||||
// 5/9 * 100 = 55.56 → rounded to 56
|
||||
expect(calculateImpactScore(5, 10)).toBe(56)
|
||||
})
|
||||
|
||||
it("should return 100 when all other files depend on it", () => {
|
||||
// 9 dependents out of 10 files (9 possible dependents)
|
||||
expect(calculateImpactScore(9, 10)).toBe(100)
|
||||
})
|
||||
|
||||
it("should cap at 100", () => {
|
||||
// Edge case: more dependents than possible (shouldn't happen normally)
|
||||
expect(calculateImpactScore(20, 10)).toBe(100)
|
||||
})
|
||||
|
||||
it("should round the percentage", () => {
|
||||
// 1 dependent out of 3 files (2 possible)
|
||||
// 1/2 * 100 = 50
|
||||
expect(calculateImpactScore(1, 3)).toBe(50)
|
||||
})
|
||||
|
||||
it("should calculate impact for small projects", () => {
|
||||
// 1 dependent out of 2 files (1 possible)
|
||||
expect(calculateImpactScore(1, 2)).toBe(100)
|
||||
})
|
||||
|
||||
it("should calculate impact for larger projects", () => {
|
||||
// 50 dependents out of 100 files (99 possible)
|
||||
// 50/99 * 100 = 50.51 → rounded to 51
|
||||
expect(calculateImpactScore(50, 100)).toBe(51)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -224,6 +224,62 @@ describe("ASTParser", () => {
|
||||
const ast = parser.parse(code, "ts")
|
||||
expect(ast.typeAliases[0].isExported).toBe(true)
|
||||
})
|
||||
|
||||
it("should extract type alias definition (simple)", () => {
|
||||
const code = `type UserId = string`
|
||||
const ast = parser.parse(code, "ts")
|
||||
expect(ast.typeAliases).toHaveLength(1)
|
||||
expect(ast.typeAliases[0].definition).toBe("string")
|
||||
})
|
||||
|
||||
it("should extract type alias definition (union)", () => {
|
||||
const code = `type Status = "pending" | "active" | "done"`
|
||||
const ast = parser.parse(code, "ts")
|
||||
expect(ast.typeAliases).toHaveLength(1)
|
||||
expect(ast.typeAliases[0].definition).toBe('"pending" | "active" | "done"')
|
||||
})
|
||||
|
||||
it("should extract type alias definition (intersection)", () => {
|
||||
const code = `type AdminUser = User & Admin`
|
||||
const ast = parser.parse(code, "ts")
|
||||
expect(ast.typeAliases).toHaveLength(1)
|
||||
expect(ast.typeAliases[0].definition).toBe("User & Admin")
|
||||
})
|
||||
|
||||
it("should extract type alias definition (object type)", () => {
|
||||
const code = `type Point = { x: number; y: number }`
|
||||
const ast = parser.parse(code, "ts")
|
||||
expect(ast.typeAliases).toHaveLength(1)
|
||||
expect(ast.typeAliases[0].definition).toBe("{ x: number; y: number }")
|
||||
})
|
||||
|
||||
it("should extract type alias definition (function type)", () => {
|
||||
const code = `type Handler = (event: Event) => void`
|
||||
const ast = parser.parse(code, "ts")
|
||||
expect(ast.typeAliases).toHaveLength(1)
|
||||
expect(ast.typeAliases[0].definition).toBe("(event: Event) => void")
|
||||
})
|
||||
|
||||
it("should extract type alias definition (generic)", () => {
|
||||
const code = `type Result<T> = { success: boolean; data: T }`
|
||||
const ast = parser.parse(code, "ts")
|
||||
expect(ast.typeAliases).toHaveLength(1)
|
||||
expect(ast.typeAliases[0].definition).toBe("{ success: boolean; data: T }")
|
||||
})
|
||||
|
||||
it("should extract type alias definition (array)", () => {
|
||||
const code = `type UserIds = string[]`
|
||||
const ast = parser.parse(code, "ts")
|
||||
expect(ast.typeAliases).toHaveLength(1)
|
||||
expect(ast.typeAliases[0].definition).toBe("string[]")
|
||||
})
|
||||
|
||||
it("should extract type alias definition (tuple)", () => {
|
||||
const code = `type Pair = [string, number]`
|
||||
const ast = parser.parse(code, "ts")
|
||||
expect(ast.typeAliases).toHaveLength(1)
|
||||
expect(ast.typeAliases[0].definition).toBe("[string, number]")
|
||||
})
|
||||
})
|
||||
|
||||
describe("exports", () => {
|
||||
@@ -404,4 +460,376 @@ function mix(
|
||||
expect(ast.exports.length).toBeGreaterThanOrEqual(4)
|
||||
})
|
||||
})
|
||||
|
||||
describe("JSON parsing", () => {
|
||||
it("should extract top-level keys from JSON object", () => {
|
||||
const json = `{
|
||||
"name": "test",
|
||||
"version": "1.0.0",
|
||||
"dependencies": {},
|
||||
"scripts": {}
|
||||
}`
|
||||
const ast = parser.parse(json, "json")
|
||||
|
||||
expect(ast.parseError).toBe(false)
|
||||
expect(ast.exports).toHaveLength(4)
|
||||
expect(ast.exports.map((e) => e.name)).toEqual([
|
||||
"name",
|
||||
"version",
|
||||
"dependencies",
|
||||
"scripts",
|
||||
])
|
||||
expect(ast.exports.every((e) => e.kind === "variable")).toBe(true)
|
||||
})
|
||||
|
||||
it("should handle empty JSON object", () => {
|
||||
const json = `{}`
|
||||
const ast = parser.parse(json, "json")
|
||||
|
||||
expect(ast.parseError).toBe(false)
|
||||
expect(ast.exports).toHaveLength(0)
|
||||
})
|
||||
})
|
||||
|
||||
describe("YAML parsing", () => {
|
||||
it("should extract top-level keys from YAML", () => {
|
||||
const yaml = `name: test
|
||||
version: 1.0.0
|
||||
dependencies:
|
||||
foo: ^1.0.0
|
||||
scripts:
|
||||
test: vitest`
|
||||
|
||||
const ast = parser.parse(yaml, "yaml")
|
||||
|
||||
expect(ast.parseError).toBe(false)
|
||||
expect(ast.exports.length).toBeGreaterThanOrEqual(4)
|
||||
expect(ast.exports.map((e) => e.name)).toContain("name")
|
||||
expect(ast.exports.map((e) => e.name)).toContain("version")
|
||||
expect(ast.exports.every((e) => e.kind === "variable")).toBe(true)
|
||||
})
|
||||
|
||||
it("should handle YAML array at root", () => {
|
||||
const yaml = `- item1
|
||||
- item2
|
||||
- item3`
|
||||
|
||||
const ast = parser.parse(yaml, "yaml")
|
||||
|
||||
expect(ast.parseError).toBe(false)
|
||||
expect(ast.exports).toHaveLength(1)
|
||||
expect(ast.exports[0].name).toBe("(array)")
|
||||
})
|
||||
|
||||
it("should handle empty YAML", () => {
|
||||
const yaml = ``
|
||||
|
||||
const ast = parser.parse(yaml, "yaml")
|
||||
|
||||
expect(ast.parseError).toBe(false)
|
||||
expect(ast.exports).toHaveLength(0)
|
||||
})
|
||||
|
||||
it("should handle YAML with null content", () => {
|
||||
const yaml = `null`
|
||||
|
||||
const ast = parser.parse(yaml, "yaml")
|
||||
|
||||
expect(ast.parseError).toBe(false)
|
||||
expect(ast.exports).toHaveLength(0)
|
||||
})
|
||||
|
||||
it("should handle invalid YAML with parse error", () => {
|
||||
const yaml = `{invalid: yaml: syntax: [}`
|
||||
|
||||
const ast = parser.parse(yaml, "yaml")
|
||||
|
||||
expect(ast.parseError).toBe(true)
|
||||
expect(ast.parseErrorMessage).toBeDefined()
|
||||
})
|
||||
|
||||
it("should track correct line numbers for YAML keys", () => {
|
||||
const yaml = `first: value1
|
||||
second: value2
|
||||
third: value3`
|
||||
|
||||
const ast = parser.parse(yaml, "yaml")
|
||||
|
||||
expect(ast.parseError).toBe(false)
|
||||
expect(ast.exports).toHaveLength(3)
|
||||
expect(ast.exports[0].line).toBe(1)
|
||||
expect(ast.exports[1].line).toBe(2)
|
||||
expect(ast.exports[2].line).toBe(3)
|
||||
})
|
||||
})
|
||||
|
||||
describe("enums (0.24.3)", () => {
|
||||
it("should extract enum with numeric values", () => {
|
||||
const code = `enum Status {
|
||||
Active = 1,
|
||||
Inactive = 0,
|
||||
Pending = 2
|
||||
}`
|
||||
const ast = parser.parse(code, "ts")
|
||||
|
||||
expect(ast.enums).toHaveLength(1)
|
||||
expect(ast.enums[0]).toMatchObject({
|
||||
name: "Status",
|
||||
isExported: false,
|
||||
isConst: false,
|
||||
})
|
||||
expect(ast.enums[0].members).toHaveLength(3)
|
||||
expect(ast.enums[0].members[0]).toMatchObject({ name: "Active", value: 1 })
|
||||
expect(ast.enums[0].members[1]).toMatchObject({ name: "Inactive", value: 0 })
|
||||
expect(ast.enums[0].members[2]).toMatchObject({ name: "Pending", value: 2 })
|
||||
})
|
||||
|
||||
it("should extract enum with string values", () => {
|
||||
const code = `enum Role {
|
||||
Admin = "admin",
|
||||
User = "user",
|
||||
Guest = "guest"
|
||||
}`
|
||||
const ast = parser.parse(code, "ts")
|
||||
|
||||
expect(ast.enums).toHaveLength(1)
|
||||
expect(ast.enums[0].members).toHaveLength(3)
|
||||
expect(ast.enums[0].members[0]).toMatchObject({ name: "Admin", value: "admin" })
|
||||
expect(ast.enums[0].members[1]).toMatchObject({ name: "User", value: "user" })
|
||||
expect(ast.enums[0].members[2]).toMatchObject({ name: "Guest", value: "guest" })
|
||||
})
|
||||
|
||||
it("should extract enum without explicit values", () => {
|
||||
const code = `enum Direction {
|
||||
Up,
|
||||
Down,
|
||||
Left,
|
||||
Right
|
||||
}`
|
||||
const ast = parser.parse(code, "ts")
|
||||
|
||||
expect(ast.enums).toHaveLength(1)
|
||||
expect(ast.enums[0].members).toHaveLength(4)
|
||||
expect(ast.enums[0].members[0]).toMatchObject({ name: "Up", value: undefined })
|
||||
expect(ast.enums[0].members[1]).toMatchObject({ name: "Down", value: undefined })
|
||||
})
|
||||
|
||||
it("should extract exported enum", () => {
|
||||
const code = `export enum Color {
|
||||
Red = "#FF0000",
|
||||
Green = "#00FF00",
|
||||
Blue = "#0000FF"
|
||||
}`
|
||||
const ast = parser.parse(code, "ts")
|
||||
|
||||
expect(ast.enums).toHaveLength(1)
|
||||
expect(ast.enums[0].isExported).toBe(true)
|
||||
expect(ast.exports).toHaveLength(1)
|
||||
expect(ast.exports[0].kind).toBe("type")
|
||||
})
|
||||
|
||||
it("should extract const enum", () => {
|
||||
const code = `const enum HttpStatus {
|
||||
OK = 200,
|
||||
NotFound = 404,
|
||||
InternalError = 500
|
||||
}`
|
||||
const ast = parser.parse(code, "ts")
|
||||
|
||||
expect(ast.enums).toHaveLength(1)
|
||||
expect(ast.enums[0].isConst).toBe(true)
|
||||
expect(ast.enums[0].members[0]).toMatchObject({ name: "OK", value: 200 })
|
||||
})
|
||||
|
||||
it("should extract exported const enum", () => {
|
||||
const code = `export const enum LogLevel {
|
||||
Debug = 0,
|
||||
Info = 1,
|
||||
Warn = 2,
|
||||
Error = 3
|
||||
}`
|
||||
const ast = parser.parse(code, "ts")
|
||||
|
||||
expect(ast.enums).toHaveLength(1)
|
||||
expect(ast.enums[0].isExported).toBe(true)
|
||||
expect(ast.enums[0].isConst).toBe(true)
|
||||
})
|
||||
|
||||
it("should extract line range for enum", () => {
|
||||
const code = `enum Test {
|
||||
A = 1,
|
||||
B = 2
|
||||
}`
|
||||
const ast = parser.parse(code, "ts")
|
||||
|
||||
expect(ast.enums[0].lineStart).toBe(1)
|
||||
expect(ast.enums[0].lineEnd).toBe(4)
|
||||
})
|
||||
|
||||
it("should handle enum with negative values", () => {
|
||||
const code = `enum Temperature {
|
||||
Cold = -10,
|
||||
Freezing = -20,
|
||||
Hot = 40
|
||||
}`
|
||||
const ast = parser.parse(code, "ts")
|
||||
|
||||
expect(ast.enums).toHaveLength(1)
|
||||
expect(ast.enums[0].members[0]).toMatchObject({ name: "Cold", value: -10 })
|
||||
expect(ast.enums[0].members[1]).toMatchObject({ name: "Freezing", value: -20 })
|
||||
expect(ast.enums[0].members[2]).toMatchObject({ name: "Hot", value: 40 })
|
||||
})
|
||||
|
||||
it("should handle empty enum", () => {
|
||||
const code = `enum Empty {}`
|
||||
const ast = parser.parse(code, "ts")
|
||||
|
||||
expect(ast.enums).toHaveLength(1)
|
||||
expect(ast.enums[0].name).toBe("Empty")
|
||||
expect(ast.enums[0].members).toHaveLength(0)
|
||||
})
|
||||
|
||||
it("should not extract enum from JavaScript", () => {
|
||||
const code = `enum Status { Active = 1 }`
|
||||
const ast = parser.parse(code, "js")
|
||||
|
||||
expect(ast.enums).toHaveLength(0)
|
||||
})
|
||||
})
|
||||
|
||||
describe("decorators (0.24.4)", () => {
|
||||
it("should extract class decorator", () => {
|
||||
const code = `@Controller('users')
|
||||
class UserController {}`
|
||||
const ast = parser.parse(code, "ts")
|
||||
|
||||
expect(ast.classes).toHaveLength(1)
|
||||
expect(ast.classes[0].decorators).toHaveLength(1)
|
||||
expect(ast.classes[0].decorators[0]).toBe("@Controller('users')")
|
||||
})
|
||||
|
||||
it("should extract multiple class decorators", () => {
|
||||
const code = `@Controller('api')
|
||||
@Injectable()
|
||||
@UseGuards(AuthGuard)
|
||||
class ApiController {}`
|
||||
const ast = parser.parse(code, "ts")
|
||||
|
||||
expect(ast.classes).toHaveLength(1)
|
||||
expect(ast.classes[0].decorators).toHaveLength(3)
|
||||
expect(ast.classes[0].decorators[0]).toBe("@Controller('api')")
|
||||
expect(ast.classes[0].decorators[1]).toBe("@Injectable()")
|
||||
expect(ast.classes[0].decorators[2]).toBe("@UseGuards(AuthGuard)")
|
||||
})
|
||||
|
||||
it("should extract method decorators", () => {
|
||||
const code = `class UserController {
|
||||
@Get(':id')
|
||||
@Auth()
|
||||
async getUser() {}
|
||||
}`
|
||||
const ast = parser.parse(code, "ts")
|
||||
|
||||
expect(ast.classes).toHaveLength(1)
|
||||
expect(ast.classes[0].methods).toHaveLength(1)
|
||||
expect(ast.classes[0].methods[0].decorators).toHaveLength(2)
|
||||
expect(ast.classes[0].methods[0].decorators[0]).toBe("@Get(':id')")
|
||||
expect(ast.classes[0].methods[0].decorators[1]).toBe("@Auth()")
|
||||
})
|
||||
|
||||
it("should extract exported decorated class", () => {
|
||||
const code = `@Injectable()
|
||||
export class UserService {}`
|
||||
const ast = parser.parse(code, "ts")
|
||||
|
||||
expect(ast.classes).toHaveLength(1)
|
||||
expect(ast.classes[0].isExported).toBe(true)
|
||||
expect(ast.classes[0].decorators).toHaveLength(1)
|
||||
expect(ast.classes[0].decorators[0]).toBe("@Injectable()")
|
||||
})
|
||||
|
||||
it("should extract decorator with complex arguments", () => {
|
||||
const code = `@Module({
|
||||
imports: [UserModule],
|
||||
controllers: [AppController],
|
||||
providers: [AppService]
|
||||
})
|
||||
class AppModule {}`
|
||||
const ast = parser.parse(code, "ts")
|
||||
|
||||
expect(ast.classes).toHaveLength(1)
|
||||
expect(ast.classes[0].decorators).toHaveLength(1)
|
||||
expect(ast.classes[0].decorators[0]).toContain("@Module")
|
||||
expect(ast.classes[0].decorators[0]).toContain("imports")
|
||||
})
|
||||
|
||||
it("should extract decorated class with extends", () => {
|
||||
const code = `@Entity()
|
||||
class User extends BaseEntity {}`
|
||||
const ast = parser.parse(code, "ts")
|
||||
|
||||
expect(ast.classes).toHaveLength(1)
|
||||
expect(ast.classes[0].extends).toBe("BaseEntity")
|
||||
expect(ast.classes[0].decorators).toHaveLength(1)
|
||||
expect(ast.classes[0].decorators![0]).toBe("@Entity()")
|
||||
})
|
||||
|
||||
it("should handle class without decorators", () => {
|
||||
const code = `class SimpleClass {}`
|
||||
const ast = parser.parse(code, "ts")
|
||||
|
||||
expect(ast.classes).toHaveLength(1)
|
||||
expect(ast.classes[0].decorators).toHaveLength(0)
|
||||
})
|
||||
|
||||
it("should handle method without decorators", () => {
|
||||
const code = `class SimpleClass {
|
||||
simpleMethod() {}
|
||||
}`
|
||||
const ast = parser.parse(code, "ts")
|
||||
|
||||
expect(ast.classes).toHaveLength(1)
|
||||
expect(ast.classes[0].methods).toHaveLength(1)
|
||||
expect(ast.classes[0].methods[0].decorators).toHaveLength(0)
|
||||
})
|
||||
|
||||
it("should handle function without decorators", () => {
|
||||
const code = `function simpleFunc() {}`
|
||||
const ast = parser.parse(code, "ts")
|
||||
|
||||
expect(ast.functions).toHaveLength(1)
|
||||
expect(ast.functions[0].decorators).toHaveLength(0)
|
||||
})
|
||||
|
||||
it("should handle arrow function without decorators", () => {
|
||||
const code = `const arrowFn = () => {}`
|
||||
const ast = parser.parse(code, "ts")
|
||||
|
||||
expect(ast.functions).toHaveLength(1)
|
||||
expect(ast.functions[0].decorators).toHaveLength(0)
|
||||
})
|
||||
|
||||
it("should extract NestJS controller pattern", () => {
|
||||
const code = `@Controller('users')
|
||||
export class UserController {
|
||||
@Get()
|
||||
findAll() {}
|
||||
|
||||
@Get(':id')
|
||||
findOne() {}
|
||||
|
||||
@Post()
|
||||
@Body()
|
||||
create() {}
|
||||
}`
|
||||
const ast = parser.parse(code, "ts")
|
||||
|
||||
expect(ast.classes).toHaveLength(1)
|
||||
expect(ast.classes[0].decorators).toContain("@Controller('users')")
|
||||
expect(ast.classes[0].methods).toHaveLength(3)
|
||||
expect(ast.classes[0].methods[0].decorators).toContain("@Get()")
|
||||
expect(ast.classes[0].methods[1].decorators).toContain("@Get(':id')")
|
||||
expect(ast.classes[0].methods[2].decorators).toContain("@Post()")
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -3,6 +3,7 @@ import { MetaAnalyzer } from "../../../../src/infrastructure/indexer/MetaAnalyze
|
||||
import { ASTParser } from "../../../../src/infrastructure/indexer/ASTParser.js"
|
||||
import type { FileAST } from "../../../../src/domain/value-objects/FileAST.js"
|
||||
import { createEmptyFileAST } from "../../../../src/domain/value-objects/FileAST.js"
|
||||
import { createFileMeta, type FileMeta } from "../../../../src/domain/value-objects/FileMeta.js"
|
||||
|
||||
describe("MetaAnalyzer", () => {
|
||||
let analyzer: MetaAnalyzer
|
||||
@@ -737,4 +738,368 @@ export function createComponent(): MyComponent {
|
||||
expect(meta.fileType).toBe("source")
|
||||
})
|
||||
})
|
||||
|
||||
describe("computeTransitiveCounts", () => {
|
||||
it("should compute transitive dependents for a simple chain", () => {
|
||||
// A -> B -> C (A depends on B, B depends on C)
|
||||
// So C has transitive dependents: B, A
|
||||
const metas = new Map<string, FileMeta>()
|
||||
metas.set(
|
||||
"/project/a.ts",
|
||||
createFileMeta({
|
||||
dependencies: ["/project/b.ts"],
|
||||
dependents: [],
|
||||
}),
|
||||
)
|
||||
metas.set(
|
||||
"/project/b.ts",
|
||||
createFileMeta({
|
||||
dependencies: ["/project/c.ts"],
|
||||
dependents: ["/project/a.ts"],
|
||||
}),
|
||||
)
|
||||
metas.set(
|
||||
"/project/c.ts",
|
||||
createFileMeta({
|
||||
dependencies: [],
|
||||
dependents: ["/project/b.ts"],
|
||||
}),
|
||||
)
|
||||
|
||||
analyzer.computeTransitiveCounts(metas)
|
||||
|
||||
expect(metas.get("/project/c.ts")!.transitiveDepCount).toBe(2) // B and A
|
||||
expect(metas.get("/project/b.ts")!.transitiveDepCount).toBe(1) // A
|
||||
expect(metas.get("/project/a.ts")!.transitiveDepCount).toBe(0) // none
|
||||
})
|
||||
|
||||
it("should compute transitive dependencies for a simple chain", () => {
|
||||
// A -> B -> C (A depends on B, B depends on C)
|
||||
// So A has transitive dependencies: B, C
|
||||
const metas = new Map<string, FileMeta>()
|
||||
metas.set(
|
||||
"/project/a.ts",
|
||||
createFileMeta({
|
||||
dependencies: ["/project/b.ts"],
|
||||
dependents: [],
|
||||
}),
|
||||
)
|
||||
metas.set(
|
||||
"/project/b.ts",
|
||||
createFileMeta({
|
||||
dependencies: ["/project/c.ts"],
|
||||
dependents: ["/project/a.ts"],
|
||||
}),
|
||||
)
|
||||
metas.set(
|
||||
"/project/c.ts",
|
||||
createFileMeta({
|
||||
dependencies: [],
|
||||
dependents: ["/project/b.ts"],
|
||||
}),
|
||||
)
|
||||
|
||||
analyzer.computeTransitiveCounts(metas)
|
||||
|
||||
expect(metas.get("/project/a.ts")!.transitiveDepByCount).toBe(2) // B and C
|
||||
expect(metas.get("/project/b.ts")!.transitiveDepByCount).toBe(1) // C
|
||||
expect(metas.get("/project/c.ts")!.transitiveDepByCount).toBe(0) // none
|
||||
})
|
||||
|
||||
it("should handle diamond dependency pattern", () => {
|
||||
// A
|
||||
// / \
|
||||
// B C
|
||||
// \ /
|
||||
// D
|
||||
// A depends on B and C, both depend on D
|
||||
const metas = new Map<string, FileMeta>()
|
||||
metas.set(
|
||||
"/project/a.ts",
|
||||
createFileMeta({
|
||||
dependencies: ["/project/b.ts", "/project/c.ts"],
|
||||
dependents: [],
|
||||
}),
|
||||
)
|
||||
metas.set(
|
||||
"/project/b.ts",
|
||||
createFileMeta({
|
||||
dependencies: ["/project/d.ts"],
|
||||
dependents: ["/project/a.ts"],
|
||||
}),
|
||||
)
|
||||
metas.set(
|
||||
"/project/c.ts",
|
||||
createFileMeta({
|
||||
dependencies: ["/project/d.ts"],
|
||||
dependents: ["/project/a.ts"],
|
||||
}),
|
||||
)
|
||||
metas.set(
|
||||
"/project/d.ts",
|
||||
createFileMeta({
|
||||
dependencies: [],
|
||||
dependents: ["/project/b.ts", "/project/c.ts"],
|
||||
}),
|
||||
)
|
||||
|
||||
analyzer.computeTransitiveCounts(metas)
|
||||
|
||||
// D is depended on by B, C, and transitively by A
|
||||
expect(metas.get("/project/d.ts")!.transitiveDepCount).toBe(3)
|
||||
// A depends on B, C, and transitively on D
|
||||
expect(metas.get("/project/a.ts")!.transitiveDepByCount).toBe(3)
|
||||
})
|
||||
|
||||
it("should handle circular dependencies gracefully", () => {
|
||||
// A -> B -> C -> A (circular)
|
||||
const metas = new Map<string, FileMeta>()
|
||||
metas.set(
|
||||
"/project/a.ts",
|
||||
createFileMeta({
|
||||
dependencies: ["/project/b.ts"],
|
||||
dependents: ["/project/c.ts"],
|
||||
}),
|
||||
)
|
||||
metas.set(
|
||||
"/project/b.ts",
|
||||
createFileMeta({
|
||||
dependencies: ["/project/c.ts"],
|
||||
dependents: ["/project/a.ts"],
|
||||
}),
|
||||
)
|
||||
metas.set(
|
||||
"/project/c.ts",
|
||||
createFileMeta({
|
||||
dependencies: ["/project/a.ts"],
|
||||
dependents: ["/project/b.ts"],
|
||||
}),
|
||||
)
|
||||
|
||||
// Should not throw, should handle cycles
|
||||
analyzer.computeTransitiveCounts(metas)
|
||||
|
||||
// Each file has the other 2 as transitive dependents
|
||||
expect(metas.get("/project/a.ts")!.transitiveDepCount).toBe(2)
|
||||
expect(metas.get("/project/b.ts")!.transitiveDepCount).toBe(2)
|
||||
expect(metas.get("/project/c.ts")!.transitiveDepCount).toBe(2)
|
||||
})
|
||||
|
||||
it("should return 0 for files with no dependencies", () => {
|
||||
const metas = new Map<string, FileMeta>()
|
||||
metas.set(
|
||||
"/project/a.ts",
|
||||
createFileMeta({
|
||||
dependencies: [],
|
||||
dependents: [],
|
||||
}),
|
||||
)
|
||||
|
||||
analyzer.computeTransitiveCounts(metas)
|
||||
|
||||
expect(metas.get("/project/a.ts")!.transitiveDepCount).toBe(0)
|
||||
expect(metas.get("/project/a.ts")!.transitiveDepByCount).toBe(0)
|
||||
})
|
||||
|
||||
it("should handle empty metas map", () => {
|
||||
const metas = new Map<string, FileMeta>()
|
||||
// Should not throw
|
||||
expect(() => analyzer.computeTransitiveCounts(metas)).not.toThrow()
|
||||
})
|
||||
|
||||
it("should handle single file", () => {
|
||||
const metas = new Map<string, FileMeta>()
|
||||
metas.set(
|
||||
"/project/a.ts",
|
||||
createFileMeta({
|
||||
dependencies: [],
|
||||
dependents: [],
|
||||
}),
|
||||
)
|
||||
|
||||
analyzer.computeTransitiveCounts(metas)
|
||||
|
||||
expect(metas.get("/project/a.ts")!.transitiveDepCount).toBe(0)
|
||||
expect(metas.get("/project/a.ts")!.transitiveDepByCount).toBe(0)
|
||||
})
|
||||
|
||||
it("should handle multiple roots depending on same leaf", () => {
|
||||
// A -> C, B -> C
|
||||
const metas = new Map<string, FileMeta>()
|
||||
metas.set(
|
||||
"/project/a.ts",
|
||||
createFileMeta({
|
||||
dependencies: ["/project/c.ts"],
|
||||
dependents: [],
|
||||
}),
|
||||
)
|
||||
metas.set(
|
||||
"/project/b.ts",
|
||||
createFileMeta({
|
||||
dependencies: ["/project/c.ts"],
|
||||
dependents: [],
|
||||
}),
|
||||
)
|
||||
metas.set(
|
||||
"/project/c.ts",
|
||||
createFileMeta({
|
||||
dependencies: [],
|
||||
dependents: ["/project/a.ts", "/project/b.ts"],
|
||||
}),
|
||||
)
|
||||
|
||||
analyzer.computeTransitiveCounts(metas)
|
||||
|
||||
expect(metas.get("/project/c.ts")!.transitiveDepCount).toBe(2) // A and B
|
||||
expect(metas.get("/project/a.ts")!.transitiveDepByCount).toBe(1) // C
|
||||
expect(metas.get("/project/b.ts")!.transitiveDepByCount).toBe(1) // C
|
||||
})
|
||||
|
||||
it("should handle deep dependency chains", () => {
|
||||
// A -> B -> C -> D -> E
|
||||
const metas = new Map<string, FileMeta>()
|
||||
metas.set(
|
||||
"/project/a.ts",
|
||||
createFileMeta({
|
||||
dependencies: ["/project/b.ts"],
|
||||
dependents: [],
|
||||
}),
|
||||
)
|
||||
metas.set(
|
||||
"/project/b.ts",
|
||||
createFileMeta({
|
||||
dependencies: ["/project/c.ts"],
|
||||
dependents: ["/project/a.ts"],
|
||||
}),
|
||||
)
|
||||
metas.set(
|
||||
"/project/c.ts",
|
||||
createFileMeta({
|
||||
dependencies: ["/project/d.ts"],
|
||||
dependents: ["/project/b.ts"],
|
||||
}),
|
||||
)
|
||||
metas.set(
|
||||
"/project/d.ts",
|
||||
createFileMeta({
|
||||
dependencies: ["/project/e.ts"],
|
||||
dependents: ["/project/c.ts"],
|
||||
}),
|
||||
)
|
||||
metas.set(
|
||||
"/project/e.ts",
|
||||
createFileMeta({
|
||||
dependencies: [],
|
||||
dependents: ["/project/d.ts"],
|
||||
}),
|
||||
)
|
||||
|
||||
analyzer.computeTransitiveCounts(metas)
|
||||
|
||||
// E has transitive dependents: D, C, B, A
|
||||
expect(metas.get("/project/e.ts")!.transitiveDepCount).toBe(4)
|
||||
// A has transitive dependencies: B, C, D, E
|
||||
expect(metas.get("/project/a.ts")!.transitiveDepByCount).toBe(4)
|
||||
})
|
||||
})
|
||||
|
||||
describe("getTransitiveDependents", () => {
|
||||
it("should return empty set for file not in metas", () => {
|
||||
const metas = new Map<string, FileMeta>()
|
||||
const cache = new Map<string, Set<string>>()
|
||||
|
||||
const result = analyzer.getTransitiveDependents("/project/unknown.ts", metas, cache)
|
||||
|
||||
expect(result.size).toBe(0)
|
||||
})
|
||||
|
||||
it("should use cache for repeated calls", () => {
|
||||
const metas = new Map<string, FileMeta>()
|
||||
metas.set(
|
||||
"/project/a.ts",
|
||||
createFileMeta({
|
||||
dependencies: [],
|
||||
dependents: ["/project/b.ts"],
|
||||
}),
|
||||
)
|
||||
metas.set(
|
||||
"/project/b.ts",
|
||||
createFileMeta({
|
||||
dependencies: ["/project/a.ts"],
|
||||
dependents: [],
|
||||
}),
|
||||
)
|
||||
|
||||
const cache = new Map<string, Set<string>>()
|
||||
const result1 = analyzer.getTransitiveDependents("/project/a.ts", metas, cache)
|
||||
const result2 = analyzer.getTransitiveDependents("/project/a.ts", metas, cache)
|
||||
|
||||
// Should return same instance from cache
|
||||
expect(result1).toBe(result2)
|
||||
expect(result1.size).toBe(1)
|
||||
})
|
||||
})
|
||||
|
||||
describe("getTransitiveDependencies", () => {
|
||||
it("should return empty set for file not in metas", () => {
|
||||
const metas = new Map<string, FileMeta>()
|
||||
const cache = new Map<string, Set<string>>()
|
||||
|
||||
const result = analyzer.getTransitiveDependencies("/project/unknown.ts", metas, cache)
|
||||
|
||||
expect(result.size).toBe(0)
|
||||
})
|
||||
|
||||
it("should use cache for repeated calls", () => {
|
||||
const metas = new Map<string, FileMeta>()
|
||||
metas.set(
|
||||
"/project/a.ts",
|
||||
createFileMeta({
|
||||
dependencies: ["/project/b.ts"],
|
||||
dependents: [],
|
||||
}),
|
||||
)
|
||||
metas.set(
|
||||
"/project/b.ts",
|
||||
createFileMeta({
|
||||
dependencies: [],
|
||||
dependents: ["/project/a.ts"],
|
||||
}),
|
||||
)
|
||||
|
||||
const cache = new Map<string, Set<string>>()
|
||||
const result1 = analyzer.getTransitiveDependencies("/project/a.ts", metas, cache)
|
||||
const result2 = analyzer.getTransitiveDependencies("/project/a.ts", metas, cache)
|
||||
|
||||
// Should return same instance from cache
|
||||
expect(result1).toBe(result2)
|
||||
expect(result1.size).toBe(1)
|
||||
})
|
||||
})
|
||||
|
||||
describe("analyzeAll with transitive counts", () => {
|
||||
it("should compute transitive counts in analyzeAll", () => {
|
||||
const files = new Map<string, { ast: FileAST; content: string }>()
|
||||
|
||||
// A imports B, B imports C
|
||||
const aContent = `import { b } from "./b"`
|
||||
const aAST = parser.parse(aContent, "ts")
|
||||
files.set("/project/src/a.ts", { ast: aAST, content: aContent })
|
||||
|
||||
const bContent = `import { c } from "./c"\nexport const b = () => c()`
|
||||
const bAST = parser.parse(bContent, "ts")
|
||||
files.set("/project/src/b.ts", { ast: bAST, content: bContent })
|
||||
|
||||
const cContent = `export const c = () => 42`
|
||||
const cAST = parser.parse(cContent, "ts")
|
||||
files.set("/project/src/c.ts", { ast: cAST, content: cContent })
|
||||
|
||||
const results = analyzer.analyzeAll(files)
|
||||
|
||||
// C has transitive dependents: B and A
|
||||
expect(results.get("/project/src/c.ts")!.transitiveDepCount).toBe(2)
|
||||
// A has transitive dependencies: B and C
|
||||
expect(results.get("/project/src/a.ts")!.transitiveDepByCount).toBe(2)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -15,6 +15,10 @@ describe("ContextConfigSchema", () => {
|
||||
maxContextUsage: 0.8,
|
||||
autoCompressAt: 0.8,
|
||||
compressionMethod: "llm-summary",
|
||||
includeSignatures: true,
|
||||
includeDepsGraph: true,
|
||||
includeCircularDeps: true,
|
||||
includeHighImpactFiles: true,
|
||||
})
|
||||
})
|
||||
|
||||
@@ -26,6 +30,10 @@ describe("ContextConfigSchema", () => {
|
||||
maxContextUsage: 0.8,
|
||||
autoCompressAt: 0.8,
|
||||
compressionMethod: "llm-summary",
|
||||
includeSignatures: true,
|
||||
includeDepsGraph: true,
|
||||
includeCircularDeps: true,
|
||||
includeHighImpactFiles: true,
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -162,6 +170,10 @@ describe("ContextConfigSchema", () => {
|
||||
maxContextUsage: 0.8,
|
||||
autoCompressAt: 0.8,
|
||||
compressionMethod: "llm-summary",
|
||||
includeSignatures: true,
|
||||
includeDepsGraph: true,
|
||||
includeCircularDeps: true,
|
||||
includeHighImpactFiles: true,
|
||||
})
|
||||
})
|
||||
|
||||
@@ -175,6 +187,10 @@ describe("ContextConfigSchema", () => {
|
||||
maxContextUsage: 0.8,
|
||||
autoCompressAt: 0.9,
|
||||
compressionMethod: "llm-summary",
|
||||
includeSignatures: true,
|
||||
includeDepsGraph: true,
|
||||
includeCircularDeps: true,
|
||||
includeHighImpactFiles: true,
|
||||
})
|
||||
})
|
||||
|
||||
@@ -189,6 +205,10 @@ describe("ContextConfigSchema", () => {
|
||||
maxContextUsage: 0.7,
|
||||
autoCompressAt: 0.8,
|
||||
compressionMethod: "truncate",
|
||||
includeSignatures: true,
|
||||
includeDepsGraph: true,
|
||||
includeCircularDeps: true,
|
||||
includeHighImpactFiles: true,
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -200,6 +220,10 @@ describe("ContextConfigSchema", () => {
|
||||
maxContextUsage: 0.9,
|
||||
autoCompressAt: 0.85,
|
||||
compressionMethod: "truncate" as const,
|
||||
includeSignatures: false,
|
||||
includeDepsGraph: false,
|
||||
includeCircularDeps: false,
|
||||
includeHighImpactFiles: false,
|
||||
}
|
||||
|
||||
const result = ContextConfigSchema.parse(config)
|
||||
@@ -212,10 +236,114 @@ describe("ContextConfigSchema", () => {
|
||||
maxContextUsage: 0.8,
|
||||
autoCompressAt: 0.8,
|
||||
compressionMethod: "llm-summary" as const,
|
||||
includeSignatures: true,
|
||||
includeDepsGraph: true,
|
||||
includeCircularDeps: true,
|
||||
includeHighImpactFiles: 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()
|
||||
})
|
||||
})
|
||||
|
||||
describe("includeDepsGraph", () => {
|
||||
it("should accept true", () => {
|
||||
const result = ContextConfigSchema.parse({ includeDepsGraph: true })
|
||||
expect(result.includeDepsGraph).toBe(true)
|
||||
})
|
||||
|
||||
it("should accept false", () => {
|
||||
const result = ContextConfigSchema.parse({ includeDepsGraph: false })
|
||||
expect(result.includeDepsGraph).toBe(false)
|
||||
})
|
||||
|
||||
it("should default to true", () => {
|
||||
const result = ContextConfigSchema.parse({})
|
||||
expect(result.includeDepsGraph).toBe(true)
|
||||
})
|
||||
|
||||
it("should reject non-boolean", () => {
|
||||
expect(() => ContextConfigSchema.parse({ includeDepsGraph: "true" })).toThrow()
|
||||
})
|
||||
|
||||
it("should reject number", () => {
|
||||
expect(() => ContextConfigSchema.parse({ includeDepsGraph: 1 })).toThrow()
|
||||
})
|
||||
})
|
||||
|
||||
describe("includeCircularDeps", () => {
|
||||
it("should accept true", () => {
|
||||
const result = ContextConfigSchema.parse({ includeCircularDeps: true })
|
||||
expect(result.includeCircularDeps).toBe(true)
|
||||
})
|
||||
|
||||
it("should accept false", () => {
|
||||
const result = ContextConfigSchema.parse({ includeCircularDeps: false })
|
||||
expect(result.includeCircularDeps).toBe(false)
|
||||
})
|
||||
|
||||
it("should default to true", () => {
|
||||
const result = ContextConfigSchema.parse({})
|
||||
expect(result.includeCircularDeps).toBe(true)
|
||||
})
|
||||
|
||||
it("should reject non-boolean", () => {
|
||||
expect(() => ContextConfigSchema.parse({ includeCircularDeps: "true" })).toThrow()
|
||||
})
|
||||
|
||||
it("should reject number", () => {
|
||||
expect(() => ContextConfigSchema.parse({ includeCircularDeps: 1 })).toThrow()
|
||||
})
|
||||
})
|
||||
|
||||
describe("includeHighImpactFiles", () => {
|
||||
it("should accept true", () => {
|
||||
const result = ContextConfigSchema.parse({ includeHighImpactFiles: true })
|
||||
expect(result.includeHighImpactFiles).toBe(true)
|
||||
})
|
||||
|
||||
it("should accept false", () => {
|
||||
const result = ContextConfigSchema.parse({ includeHighImpactFiles: false })
|
||||
expect(result.includeHighImpactFiles).toBe(false)
|
||||
})
|
||||
|
||||
it("should default to true", () => {
|
||||
const result = ContextConfigSchema.parse({})
|
||||
expect(result.includeHighImpactFiles).toBe(true)
|
||||
})
|
||||
|
||||
it("should reject non-boolean", () => {
|
||||
expect(() => ContextConfigSchema.parse({ includeHighImpactFiles: "true" })).toThrow()
|
||||
})
|
||||
|
||||
it("should reject number", () => {
|
||||
expect(() => ContextConfigSchema.parse({ includeHighImpactFiles: 1 })).toThrow()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -24,7 +24,7 @@ export default defineConfig({
|
||||
thresholds: {
|
||||
lines: 95,
|
||||
functions: 95,
|
||||
branches: 91.3,
|
||||
branches: 91,
|
||||
statements: 95,
|
||||
},
|
||||
},
|
||||
|
||||
56
pnpm-lock.yaml
generated
56
pnpm-lock.yaml
generated
@@ -131,7 +131,7 @@ importers:
|
||||
version: 5.9.3
|
||||
vitest:
|
||||
specifier: ^4.0.10
|
||||
version: 4.0.13(@types/node@22.19.1)(@vitest/ui@4.0.13)(jsdom@27.2.0)(terser@5.44.1)(tsx@4.20.6)
|
||||
version: 4.0.13(@types/node@22.19.1)(@vitest/ui@4.0.13)(jsdom@27.2.0)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.2)
|
||||
|
||||
packages/ipuaro:
|
||||
dependencies:
|
||||
@@ -168,9 +168,15 @@ importers:
|
||||
tree-sitter-javascript:
|
||||
specifier: ^0.21.0
|
||||
version: 0.21.4(tree-sitter@0.21.1)
|
||||
tree-sitter-json:
|
||||
specifier: ^0.24.8
|
||||
version: 0.24.8(tree-sitter@0.21.1)
|
||||
tree-sitter-typescript:
|
||||
specifier: ^0.21.2
|
||||
version: 0.21.2(tree-sitter@0.21.1)
|
||||
yaml:
|
||||
specifier: ^2.8.2
|
||||
version: 2.8.2
|
||||
zod:
|
||||
specifier: ^3.23.8
|
||||
version: 3.25.76
|
||||
@@ -201,7 +207,7 @@ importers:
|
||||
version: 18.3.1(react@18.3.1)
|
||||
tsup:
|
||||
specifier: ^8.3.5
|
||||
version: 8.5.1(postcss@8.5.6)(tsx@4.20.6)(typescript@5.9.3)
|
||||
version: 8.5.1(postcss@8.5.6)(tsx@4.20.6)(typescript@5.9.3)(yaml@2.8.2)
|
||||
typescript:
|
||||
specifier: ^5.7.2
|
||||
version: 5.9.3
|
||||
@@ -4172,6 +4178,14 @@ packages:
|
||||
tree-sitter:
|
||||
optional: true
|
||||
|
||||
tree-sitter-json@0.24.8:
|
||||
resolution: {integrity: sha512-Tc9ZZYwHyWZ3Tt1VEw7Pa2scu1YO7/d2BCBbKTx5hXwig3UfdQjsOPkPyLpDJOn/m1UBEWYAtSdGAwCSyagBqQ==}
|
||||
peerDependencies:
|
||||
tree-sitter: ^0.21.1
|
||||
peerDependenciesMeta:
|
||||
tree-sitter:
|
||||
optional: true
|
||||
|
||||
tree-sitter-typescript@0.21.2:
|
||||
resolution: {integrity: sha512-/RyNK41ZpkA8PuPZimR6pGLvNR1p0ibRUJwwQn4qAjyyLEIQD/BNlwS3NSxWtGsAWZe9gZ44VK1mWx2+eQVldg==}
|
||||
peerDependencies:
|
||||
@@ -4636,6 +4650,11 @@ packages:
|
||||
yallist@3.1.1:
|
||||
resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==}
|
||||
|
||||
yaml@2.8.2:
|
||||
resolution: {integrity: sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==}
|
||||
engines: {node: '>= 14.6'}
|
||||
hasBin: true
|
||||
|
||||
yargs-parser@21.1.1:
|
||||
resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==}
|
||||
engines: {node: '>=12'}
|
||||
@@ -6325,7 +6344,7 @@ snapshots:
|
||||
magicast: 0.5.1
|
||||
std-env: 3.10.0
|
||||
tinyrainbow: 3.0.3
|
||||
vitest: 4.0.13(@types/node@22.19.1)(@vitest/ui@4.0.13)(jsdom@27.2.0)(terser@5.44.1)(tsx@4.20.6)
|
||||
vitest: 4.0.13(@types/node@22.19.1)(@vitest/ui@4.0.13)(jsdom@27.2.0)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.2)
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
@@ -6344,13 +6363,13 @@ snapshots:
|
||||
chai: 6.2.1
|
||||
tinyrainbow: 3.0.3
|
||||
|
||||
'@vitest/mocker@4.0.13(vite@7.2.4(@types/node@22.19.1)(terser@5.44.1)(tsx@4.20.6))':
|
||||
'@vitest/mocker@4.0.13(vite@7.2.4(@types/node@22.19.1)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.2))':
|
||||
dependencies:
|
||||
'@vitest/spy': 4.0.13
|
||||
estree-walker: 3.0.3
|
||||
magic-string: 0.30.21
|
||||
optionalDependencies:
|
||||
vite: 7.2.4(@types/node@22.19.1)(terser@5.44.1)(tsx@4.20.6)
|
||||
vite: 7.2.4(@types/node@22.19.1)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.2)
|
||||
|
||||
'@vitest/pretty-format@4.0.13':
|
||||
dependencies:
|
||||
@@ -6405,7 +6424,7 @@ snapshots:
|
||||
sirv: 3.0.2
|
||||
tinyglobby: 0.2.15
|
||||
tinyrainbow: 3.0.3
|
||||
vitest: 4.0.13(@types/node@22.19.1)(@vitest/ui@4.0.13)(jsdom@27.2.0)(terser@5.44.1)(tsx@4.20.6)
|
||||
vitest: 4.0.13(@types/node@22.19.1)(@vitest/ui@4.0.13)(jsdom@27.2.0)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.2)
|
||||
|
||||
'@vitest/utils@1.6.1':
|
||||
dependencies:
|
||||
@@ -8392,12 +8411,13 @@ snapshots:
|
||||
|
||||
pluralize@8.0.0: {}
|
||||
|
||||
postcss-load-config@6.0.1(postcss@8.5.6)(tsx@4.20.6):
|
||||
postcss-load-config@6.0.1(postcss@8.5.6)(tsx@4.20.6)(yaml@2.8.2):
|
||||
dependencies:
|
||||
lilconfig: 3.1.3
|
||||
optionalDependencies:
|
||||
postcss: 8.5.6
|
||||
tsx: 4.20.6
|
||||
yaml: 2.8.2
|
||||
|
||||
postcss@8.5.6:
|
||||
dependencies:
|
||||
@@ -8911,6 +8931,13 @@ snapshots:
|
||||
optionalDependencies:
|
||||
tree-sitter: 0.21.1
|
||||
|
||||
tree-sitter-json@0.24.8(tree-sitter@0.21.1):
|
||||
dependencies:
|
||||
node-addon-api: 8.5.0
|
||||
node-gyp-build: 4.8.4
|
||||
optionalDependencies:
|
||||
tree-sitter: 0.21.1
|
||||
|
||||
tree-sitter-typescript@0.21.2(tree-sitter@0.21.1):
|
||||
dependencies:
|
||||
node-addon-api: 8.5.0
|
||||
@@ -8999,7 +9026,7 @@ snapshots:
|
||||
|
||||
tslib@2.8.1: {}
|
||||
|
||||
tsup@8.5.1(postcss@8.5.6)(tsx@4.20.6)(typescript@5.9.3):
|
||||
tsup@8.5.1(postcss@8.5.6)(tsx@4.20.6)(typescript@5.9.3)(yaml@2.8.2):
|
||||
dependencies:
|
||||
bundle-require: 5.1.0(esbuild@0.27.0)
|
||||
cac: 6.7.14
|
||||
@@ -9010,7 +9037,7 @@ snapshots:
|
||||
fix-dts-default-cjs-exports: 1.0.1
|
||||
joycon: 3.1.1
|
||||
picocolors: 1.1.1
|
||||
postcss-load-config: 6.0.1(postcss@8.5.6)(tsx@4.20.6)
|
||||
postcss-load-config: 6.0.1(postcss@8.5.6)(tsx@4.20.6)(yaml@2.8.2)
|
||||
resolve-from: 5.0.0
|
||||
rollup: 4.53.3
|
||||
source-map: 0.7.6
|
||||
@@ -9156,7 +9183,7 @@ snapshots:
|
||||
fsevents: 2.3.3
|
||||
terser: 5.44.1
|
||||
|
||||
vite@7.2.4(@types/node@22.19.1)(terser@5.44.1)(tsx@4.20.6):
|
||||
vite@7.2.4(@types/node@22.19.1)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.2):
|
||||
dependencies:
|
||||
esbuild: 0.25.12
|
||||
fdir: 6.5.0(picomatch@4.0.3)
|
||||
@@ -9169,6 +9196,7 @@ snapshots:
|
||||
fsevents: 2.3.3
|
||||
terser: 5.44.1
|
||||
tsx: 4.20.6
|
||||
yaml: 2.8.2
|
||||
|
||||
vitest@1.6.1(@types/node@22.19.1)(@vitest/ui@1.6.1)(jsdom@27.2.0)(terser@5.44.1):
|
||||
dependencies:
|
||||
@@ -9206,10 +9234,10 @@ snapshots:
|
||||
- supports-color
|
||||
- terser
|
||||
|
||||
vitest@4.0.13(@types/node@22.19.1)(@vitest/ui@4.0.13)(jsdom@27.2.0)(terser@5.44.1)(tsx@4.20.6):
|
||||
vitest@4.0.13(@types/node@22.19.1)(@vitest/ui@4.0.13)(jsdom@27.2.0)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.2):
|
||||
dependencies:
|
||||
'@vitest/expect': 4.0.13
|
||||
'@vitest/mocker': 4.0.13(vite@7.2.4(@types/node@22.19.1)(terser@5.44.1)(tsx@4.20.6))
|
||||
'@vitest/mocker': 4.0.13(vite@7.2.4(@types/node@22.19.1)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.2))
|
||||
'@vitest/pretty-format': 4.0.13
|
||||
'@vitest/runner': 4.0.13
|
||||
'@vitest/snapshot': 4.0.13
|
||||
@@ -9226,7 +9254,7 @@ snapshots:
|
||||
tinyexec: 0.3.2
|
||||
tinyglobby: 0.2.15
|
||||
tinyrainbow: 3.0.3
|
||||
vite: 7.2.4(@types/node@22.19.1)(terser@5.44.1)(tsx@4.20.6)
|
||||
vite: 7.2.4(@types/node@22.19.1)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.2)
|
||||
why-is-node-running: 2.3.0
|
||||
optionalDependencies:
|
||||
'@types/node': 22.19.1
|
||||
@@ -9366,6 +9394,8 @@ snapshots:
|
||||
|
||||
yallist@3.1.1: {}
|
||||
|
||||
yaml@2.8.2: {}
|
||||
|
||||
yargs-parser@21.1.1: {}
|
||||
|
||||
yargs@17.7.2:
|
||||
|
||||
Reference in New Issue
Block a user