enrichment schema

This commit is contained in:
2026-06-12 22:59:02 +02:00
parent f87d41223e
commit 8641c7266f
20 changed files with 986 additions and 12 deletions

View File

@@ -0,0 +1,35 @@
# Execution Plan for Extended Analysis
## Phase 1: Foundation (Current Topic)
- [ ] Define the `TriggerPoint` and `TriggerContext` models.
- [ ] Implement `TriggerDetector` interface.
- [ ] Create `TriggerAggregator` to coordinate multiple detectors.
## Phase 2: Direct Trigger Detection
- [ ] Implement `GenericEventDetector` using `ASTVisitor` to find `sendEvent` calls.
- [ ] Enhance `CodebaseContext` to provide mapping from `MethodDeclaration` to its callers (within the same codebase).
## Phase 3: Entry Point Mapping
- [ ] Implement `SpringMvcDetector` for REST endpoints.
- [ ] Implement `MessageListenerDetector` for Kafka/JMS.
- [ ] Implement `ConfigurableTriggerDetector` for user-defined patterns.
## Phase 4: Call Graph Integration
- [ ] Develop a simple call graph builder to link `Entry Points` to `Trigger Points` via intermediate service calls.
- [ ] Support inheritance in call graph resolution (e.g., calling a method defined in an interface/base class).
## Phase 5: Correlation and Visualization
- [ ] Update `ExportService` to include trigger information.
- [ ] Update DOT exporter to show "external" entry points as boxes pointing to transitions.
- [ ] Update SCXML exporter to include trigger metadata in `<transition>` tags.
## Phase 6: Validation
- [ ] Create a "Complex Sample Project" with REST controllers, services, and multiple state machines.
- [ ] Add unit tests for each detector and the aggregator.
- [ ] Verify that inheritance in both state machine config and controllers is correctly handled.
## Phase 7: Advanced Value Resolution
- [ ] Implement `PropertyResolver` to scan `application.properties/yml`.
- [ ] Add support for `@Value` tracking in fields and constructors.
- [ ] Implement constant resolution for cross-class references in annotations.
- [ ] Integrate value propagation into the call graph analysis (linking injected values to `sendEvent` arguments).

View File

@@ -0,0 +1,68 @@
# Extended State Machine Analysis: Trigger Point Mapping
## Overview
The goal of this extension is to bridge the gap between state machine definitions and their real-world usage in application code. By identifying where events are triggered (e.g., REST endpoints, message listeners), we can provide a more holistic view of the system's behavior.
## Key Concepts
- **Trigger Point**: A location in the source code where an event is sent to a state machine (e.g., `stateMachine.sendEvent(E)`).
- **Entry Point**: A high-level application component (e.g., a REST Controller or Message Listener) that ultimately leads to a Trigger Point.
- **Event Linkage**: The process of connecting an Entry Point to one or more state machine transitions via the event that triggers them.
## Proposed Architecture
### 1. TriggerDetector
A generic interface for identifying trigger points and extracting context.
```java
public interface TriggerDetector {
List<TriggerPoint> detect(CompilationUnit cu, CodebaseContext context);
}
```
### 2. Predefined Detectors
- **SpringMvcDetector**: Looks for `@RestController` / `@Controller` and mappings like `@PostMapping`.
- **SpringWebFluxDetector**:
- **Annotation-based**: Similar to MVC but handles `Mono`/`Flux` return types.
- **Functional-based**: Scans for `RouterFunction` beans and extracts routes/handlers.
- **MessageListenerDetector**:
- **Kafka**: `@KafkaListener`.
- **RabbitMQ**: `@RabbitListener`.
- **JMS**: `@JmsListener`.
- **GenericEventDetector**: Looks for any `sendEvent` call and tries to find its context.
### 3. TriggerPoint Model
```java
public record TriggerPoint(
String className,
String methodName,
String event,
TriggerContext context
) {}
public record TriggerContext(
String type, // "REST", "KAFKA", "JMS", "GENERIC"
Map<String, String> metadata // path="/api/submit", topic="orders", etc.
) {}
```
### 4. Analysis Flow
1. **Scan Phase**: Existing `CodebaseContext.scan(root)`.
2. **Configuration Phase**: Existing `StateMachineAggregator` finds states and transitions.
3. **Trigger Discovery Phase**: New `TriggerAggregator` runs various `TriggerDetector`s over the scanned codebase.
4. **Correlation Phase**: Match `TriggerPoint.event` with `Transition.event`.
5. **Export Phase**: Enhance DOT/SCXML or generate a new "System Flow Map".
## Challenges
- **Multiple State Machines**: How to know which `StateMachine` instance is being used?
- Initial heuristic: If there's only one, assume it's that one.
- If multiple, check generic types `StateMachine<S, E>` or variable names.
- **Indirect Calls**: Method `A` (Controller) calls `B` (Service), and `B` calls `sendEvent`.
- Static analysis might need to follow call graphs.
- Use JDT's cross-reference capabilities if possible, or build a simple call graph.
- **Inheritance**:
- Controllers might extend base classes with common mappings.
- State machine configurations already handle inheritance; Trigger detection should too.
## Next Steps
1. Create a PoC `TriggerDetector` for Spring MVC.
2. Integrate `TriggerAggregator` into the main analysis pipeline.
3. Update the `Exporter` to visualize these links.

View File

@@ -0,0 +1,92 @@
# Generic and Extensible Endpoint Finder
## The Need for Genericity
Applications use various frameworks and patterns to trigger state machine events. While Spring MVC and Kafka are common, others like Micronuat, Jakarta EE, or custom internal event buses might be used.
## Configuration-Driven Detection
We can allow users to define "Entry Point" patterns via a configuration file (e.g., `analysis-config.json`).
```json
{
"entryPoints": [
{
"name": "SpringMVC",
"classAnnotations": ["RestController", "Controller"],
"methodAnnotations": ["PostMapping", "GetMapping", "PutMapping", "DeleteMapping", "RequestMapping"],
"metadataExtractors": {
"path": ["value", "path"]
}
},
{
"name": "Kafka",
"methodAnnotations": ["KafkaListener"],
"metadataExtractors": {
"topic": ["topics", "topicPartitions"]
}
}
]
}
```
## Abstract EntryPoint Detector
A generic detector that uses the above configuration to identify entry points.
```java
public class ConfigurableTriggerDetector implements TriggerDetector {
private final AnalysisConfig config;
@Override
public List<TriggerPoint> detect(CompilationUnit cu, CodebaseContext context) {
// 1. Find all classes
// 2. If class has any of config.classAnnotations
// 3. Scan methods for any of config.methodAnnotations
// 4. If found, look for sendEvent calls inside the method or its call tree
}
}
```
## Indirect Flow Detection (Call Tree Analysis)
Oftentimes, the `sendEvent` call is not directly in the controller method but in a service called by the controller.
### Simple Call Tree Analysis
1. Find all methods that call `sendEvent(E)`. Let's call them "Direct Triggers".
2. Find all methods that call "Direct Triggers".
3. Recursively build this "Trigger Path".
4. If a "Trigger Path" starts at an "Entry Point" (Controller/Listener), we have a complete flow.
Example:
`OrderController.submitOrder()` -> `OrderService.processOrder()` -> `StateMachine.sendEvent(SUBMIT)`
Analysis:
1. `OrderService.processOrder()` is a Direct Trigger for `SUBMIT`.
2. `OrderController.submitOrder()` calls `OrderService.processOrder()`.
3. `OrderController.submitOrder()` is an Entry Point (annotated with `@PostMapping`).
4. Result: `POST /order` -> `SUBMIT` transition.
## Handling Reactive Chains Statically
In WebFlux/Project Reactor, the logic is decoupled into a pipeline. Static analysis must "connect the dots" across operators.
**Strategy**:
1. When analyzing a method, identify if it returns a reactive type (`Mono`, `Flux`).
2. If so, inspect all lambdas within that method.
3. If a lambda calls `sendEvent`, treat the method as a "Trigger Point".
4. If the method is also an "Entry Point" (via annotations or `RouterFunction`), we have a direct reactive flow.
## Supporting Inheritance in Listeners
Often, base classes or interfaces define generic listeners:
```java
public interface BaseConsumer<T> {
@RabbitListener(queues = "${app.queue}")
void handle(T payload);
}
```
The `CodebaseContext` must resolve the `${app.queue}` if possible (via `application.properties` scanning) or at least note that it's a parameterized listener.
## Advanced Call Graph: The "Data Flow"
For a truly detailed analysis, we should track NOT just method calls, but also how the "Event" object is passed around.
- Is the event hardcoded? `sm.sendEvent(Events.CREATE)`
- Is it derived from a payload field? `sm.sendEvent(payload.getEvent())`
- Is it mapped via a `switch` or `Map`?
**Static Mapping Inference**:
If we see a `switch` statement on a payload field that leads to different `sendEvent` calls, we can report multiple possible flows from the same endpoint.

View File

@@ -0,0 +1,102 @@
# Implementation Details: Trigger Detection with JDT
## 1. Finding `sendEvent` Calls
We can use an `ASTVisitor` to find all `MethodInvocation` nodes.
```java
public class SendEventVisitor extends ASTVisitor {
@Override
public boolean visit(MethodInvocation node) {
if ("sendEvent".equals(node.getName().getIdentifier())) {
// Found a call!
// 1. Extract event (first argument)
// 2. Identify the enclosing method and class
}
return super.visit(node);
}
}
```
### Challenges in Event Extraction
Events can be:
- Enum constants: `Events.SUBMIT`
- Strings: `"SUBMIT"`
- Variables: `sm.sendEvent(eventFromPayload)` (Hard to resolve statically)
We should reuse `CodebaseContext.resolveState` logic (which is basically resolving an expression to a value/fqn).
## 2. Identifying Enclosing Context
Once a `sendEvent` is found, we can traverse up the AST to find the `MethodDeclaration` and `TypeDeclaration`.
```java
ASTNode parent = node.getParent();
while (parent != null && !(parent instanceof MethodDeclaration)) {
parent = parent.getParent();
}
// parent is now the MethodDeclaration
```
### 2. Extracting Annotations
From `MethodDeclaration`, we can check for mappings:
- `@PostMapping`, `@GetMapping`, etc.
- `@KafkaListener`: Extract `topics`, `groupId`.
- `@RabbitListener`: Extract `queues`, `bindings` (Exchange/RoutingKey).
From `TypeDeclaration`, we can check for:
- `@RestController`, `@Controller`
- `@RequestMapping` at class level (to get base path)
## 3. Specialized WebFlux Analysis
### Annotation-based WebFlux
This is largely identical to Spring MVC. The challenge is if the `sendEvent` is wrapped in a reactive operator.
```java
public Mono<Void> submit(Order order) {
return service.save(order)
.doOnNext(o -> stateMachine.sendEvent(Events.SUBMIT))
.then();
}
```
**Static Strategy**: We need to look inside `LambdaExpression` nodes passed to reactive operators (`doOnNext`, `flatMap`, `map`, `subscribe`). The `ASTVisitor` should traverse into these lambdas.
### Functional WebFlux (RouterFunctions)
Functional endpoints are often defined as Beans returning `RouterFunction`.
```java
@Bean
public RouterFunction<ServerResponse> route(OrderHandler handler) {
return RouterFunctions.route(POST("/orders"), handler::submitOrder);
}
```
**Static Strategy**:
1. Find methods returning `RouterFunction`.
2. Analyze the `MethodInvocation` chain (`route`, `andRoute`, `nest`).
3. Extract the URI pattern and the `HandlerFunction` reference.
4. If the handler is a method reference (`handler::submitOrder`), link it to the corresponding `MethodDeclaration`.
## 4. Specialized RabbitMQ Analysis
`@RabbitListener` can be complex:
```java
@RabbitListener(bindings = @QueueBinding(
value = @Queue(value = "orderQueue", durable = "true"),
exchange = @Exchange(value = "orderExchange"),
key = "order.created"
))
public void onOrder(Order order) { ... }
```
**Static Strategy**:
1. Find `@RabbitListener`.
2. If it has `bindings`, drill down into `@QueueBinding`, `@Queue`, `@Exchange` to extract the topology.
3. If it only has `queues`, resolve the queue name (might be a SpEL expression or property placeholder, which we can try to resolve or just keep as-is).
## 5. Indirect Flow Detection (The "Service Link")
If the project has multiple state machines, we need to know which one is being targeted.
Usually, this is done via:
- Autowiring by type: `StateMachine<States, Events> sm;`
- Autowiring by name: `@Qualifier("mySm") StateMachine sm;`
We can look at the fields of the class where `sendEvent` is called.
## 6. Output Enhancement
The `Exporter` should be modified to:
- In DOT: Add nodes for Endpoints/Listeners and link them to the Events/Transitions.
- In SCXML: Add metadata to transitions.

View File

@@ -0,0 +1,71 @@
# Spring Property and Value Resolution Strategy
## 1. The Challenge of "Injected" Metadata
In Spring, metadata (like Queue names, Topic names, or even Event names) is often not hardcoded but injected using various mechanisms. To provide an accurate map, the analyzer must resolve these values.
## 2. Supported Injection Patterns
### Field Injection
```java
@Value("${app.order-queue}")
private String orderQueue;
```
**Static Strategy**:
1. Scan all fields for `@Value`.
2. Map `FieldName` -> `Placeholder/Value`.
3. If `${...}` is found, resolve it via the `PropertyResolver`.
### Constructor and Setter Injection
```java
public OrderService(@Value("${app.event.submit}") String submitEvent) {
this.submitEvent = submitEvent;
}
```
**Static Strategy**:
1. Scan constructor/method parameters for `@Value`.
2. Trace the assignment to a class field (`this.submitEvent = ...`).
3. Store the mapping for that field.
### @ConfigurationProperties
```java
@ConfigurationProperties(prefix = "app.messaging")
public class AppProps {
private String queueName;
}
```
**Static Strategy**:
1. Identify `@ConfigurationProperties` classes.
2. Index their fields with the prefix (e.g., `app.messaging.queueName`).
3. When these beans are injected into a service, link the service's usage to the resolved property value.
## 3. Property Resolver (The "Config Scanner")
A component dedicated to building a global map of available properties.
1. **File Scanning**: Parse `application.properties`, `application.yml`, and `bootstrap.yml`.
2. **Profile Support**: Handle `application-{profile}.yml` if a profile is active.
3. **Property Map**: Create a `Map<String, String>` of all key-value pairs.
## 4. Value Propagation (Data Flow)
Once a value is resolved or its placeholder is identified, we track its usage.
**Example Trace**:
1. `application.yml` -> `app.queue: "orders"`
2. `OrderService` -> `@Value("${app.queue}") String q;` -> `this.queue = q;`
3. `OrderService.send()` -> `sm.sendEvent(this.queue);`
4. **Resolution**: The "Event" for this trigger is "orders".
## 5. SpEL (Spring Expression Language) Lite
Full SpEL support is hard for static analysis, but we can support "Common Patterns":
- Simple bean property access: `#{myBean.name}`
- System properties: `#{systemProperties['user.dir']}`
- Ternary operators: `${app.enabled ? 'active' : 'inactive'}`
## 6. Constant Resolution
If an annotation uses a reference to a constant:
```java
@RabbitListener(queues = MyConstants.ORDER_QUEUE)
```
**Static Strategy**:
1. Use JDT to resolve the `QualifiedName` (`MyConstants.ORDER_QUEUE`).
2. Fetch the `TypeDeclaration` for `MyConstants`.
3. Find the `VariableDeclarationFragment` for `ORDER_QUEUE` and extract its literal initializer.

View File

@@ -0,0 +1,41 @@
# Execution Plan for Extended Analysis
## Phase 0: Surgical Refactoring (The Enrichment Hook)
- [ ] Create `AnalysisResult` and `CodebaseMetadata` models.
- [ ] Update `ExportService.generateOutputs` to accept `AnalysisResult`.
- [ ] Implement `EnrichmentService` and `AnalysisEnricher` interface.
- [ ] Wrap current parser outputs in `AnalysisResult` within `ExportService`.
## Phase 1: Foundation
- [ ] Implement `JdtIntelligenceProvider` as the primary semantic analyzer.
- [ ] Implement `RegexIntelligenceProvider` for fast, lightweight metadata extraction.
...
## Phase 2: Direct Trigger Detection
- [ ] Implement `GenericEventDetector` using `ASTVisitor` to find `sendEvent` calls.
- [ ] Enhance `CodebaseContext` to provide mapping from `MethodDeclaration` to its callers (within the same codebase).
## Phase 3: Entry Point Mapping
- [ ] Implement `SpringMvcDetector` for REST endpoints.
- [ ] Implement `MessageListenerDetector` for Kafka/JMS.
- [ ] Implement `ConfigurableTriggerDetector` for user-defined patterns.
## Phase 4: Call Graph Integration
- [ ] Develop a simple call graph builder to link `Entry Points` to `Trigger Points` via intermediate service calls.
- [ ] Support inheritance in call graph resolution (e.g., calling a method defined in an interface/base class).
## Phase 5: Correlation and Visualization
- [ ] Update `ExportService` to include trigger information.
- [ ] Update DOT exporter to show "external" entry points as boxes pointing to transitions.
- [ ] Update SCXML exporter to include trigger metadata in `<transition>` tags.
## Phase 6: Validation
- [ ] Create a "Complex Sample Project" with REST controllers, services, and multiple state machines.
- [ ] Add unit tests for each detector and the aggregator.
- [ ] Verify that inheritance in both state machine config and controllers is correctly handled.
## Phase 7: Advanced Value Resolution
- [ ] Implement `PropertyResolver` to scan `application.properties/yml`.
- [ ] Add support for `@Value` tracking in fields and constructors.
- [ ] Implement constant resolution for cross-class references in annotations.
- [ ] Integrate value propagation into the call graph analysis (linking injected values to `sendEvent` arguments).

View File

@@ -0,0 +1,56 @@
# Decoupled Analysis Architecture: The "Modular Provider" Approach
## Core Philosophy
We decouple the **State Machine Definition** (what the machine IS) from the **Codebase Intelligence** (how the machine IS USED). This allows for swappable analysis tools and prevents the state machine parser from becoming a monolithic "all-knowing" beast.
## 1. The Intelligence Interface (The Bridge)
We introduce an abstraction layer that provides information about the code without tying us to a specific implementation (like JDT).
```java
public interface CodebaseIntelligenceProvider {
// Event Triggers
List<TriggerPoint> findTriggerPoints();
// Entry Points
List<EntryPoint> findEntryPoints();
// Call Graphs
List<CallChain> traceCallChain(MethodReference target);
// Resolution
String resolveValue(ExpressionReference ref);
}
```
## 2. Swappable Implementations
This allows us to offer different "tiers" of analysis or even different tools:
- **JdtProvider (Current)**: High-precision, understands hierarchy, but heavy.
- **RegexProvider**: Lightweight, fast, good for simple projects or quick scans.
- **ExternalLspProvider (Future)**: Could query a running Language Server (like Eclipse JDT.LS) for ultra-accurate cross-project resolution.
## 3. The "State Machine Model" is the Sink
The State Machine Exporter now becomes a "Consumer" of intelligence.
1. **SM Parser**: Extracts the core states and transitions.
2. **Intelligence Service**: Discovers where events are triggered.
3. **Correlation Engine**: Merges the two models based on Event IDs.
## 4. Benefits
- **Testability**: We can mock the `CodebaseIntelligenceProvider` to test state machine rendering without needing a full Java project on disk.
- **Maintainability**: Adding support for a new framework (e.g., Micronaut) only requires updating the `IntelligenceProvider`, not touching the core state machine logic.
- **Performance**: We can run the SM Parser and the Intelligence Scanner in parallel since they are now independent.
## Challenges
- **Multiple State Machines**: How to know which `StateMachine` instance is being used?
- Initial heuristic: If there's only one, assume it's that one.
- If multiple, check generic types `StateMachine<S, E>` or variable names.
- **Indirect Calls**: Method `A` (Controller) calls `B` (Service), and `B` calls `sendEvent`.
- Static analysis might need to follow call graphs.
- Use JDT's cross-reference capabilities if possible, or build a simple call graph.
- **Inheritance**:
- Controllers might extend base classes with common mappings.
- State machine configurations already handle inheritance; Trigger detection should too.
## Next Steps
1. Create a PoC `TriggerDetector` for Spring MVC.
2. Integrate `TriggerAggregator` into the main analysis pipeline.
3. Update the `Exporter` to visualize these links.

View File

@@ -0,0 +1,40 @@
# The Analysis Contract: Decoupling Metadata from Implementation
## 1. Unified Model
Instead of each parser having its own internal model, we move to a "Context-Rich Model".
```java
public record CodebaseMetadata(
List<TriggerPoint> triggers,
List<EntryPoint> entryPoints,
Map<String, String> properties,
CallGraph callGraph
) {}
```
## 2. Intelligence Provider Lifecycle
1. **Initialize**: Provide root paths and configuration.
2. **Scan**: The provider builds its internal index (e.g., JDT AST, Regex indices).
3. **Extract**: The provider returns a `CodebaseMetadata` object.
4. **Dispose**: Cleanup resources.
## 3. Tool-Specific Strengths
### JDT Provider (Advanced)
- **Strengths**: True semantic understanding, inheritance resolution, precise constant lookups.
- **Usage**: Deep analysis where we need to know that `BaseService.notify()` is actually what is being called in `ChildService`.
### Regex / Tree-Sitter Provider (Light)
- **Strengths**: Extremely fast, works even on broken code (doesn't need full classpath), very low memory overhead.
- **Usage**: Large monorepos where full JDT analysis would take minutes.
## 4. Aggregation Logic
The `IntelligenceService` can actually combine results from multiple providers!
- Use JDT for the State Machine Config (where we need high precision).
- Use Regex for a quick scan of 1000+ controllers to find mapping patterns.
## 5. Refactoring Strategy
1. **Extract**: Move current JDT-based transition parsing into a `JdtStateMachineProvider`.
2. **Define**: Create the `CodebaseMetadata` model.
3. **Bridge**: Update the `ExportService` to accept both a `StateMachineModel` and a `CodebaseMetadata` object.
4. **Implement**: Create the `CodebaseIntelligenceProvider` interface and its first implementation (wrapping the existing JDT logic).

View File

@@ -0,0 +1,67 @@
# Low-Impact Abstraction: The Enrichment Pipeline
## 1. Goal
Support enhanced codebase analysis (Endpoints, Listeners, etc.) without a massive refactor of the existing JDT state machine parsers.
## 2. The Core Model: `AnalysisResult`
We wrap the existing output into a container that can hold optional metadata.
```java
public class AnalysisResult {
// Current Core Data
private final String name;
private final List<Transition> transitions;
private final Set<String> startStates;
private final Set<String> endStates;
// Optional Enhanced Data
private CodebaseMetadata metadata = CodebaseMetadata.empty();
public AnalysisResult(String name, List<Transition> transitions, Set<String> startStates, Set<String> endStates) {
this.name = name;
this.transitions = transitions;
this.startStates = startStates;
this.endStates = endStates;
}
public void setMetadata(CodebaseMetadata metadata) {
this.metadata = metadata;
}
}
```
## 3. The "Enricher" Abstraction
Instead of modifying the parser, we "enrich" the result after it's produced.
```java
public interface AnalysisEnricher {
/**
* Examines the core analysis result and the codebase context
* to add extra metadata (triggers, endpoints, etc.).
*/
void enrich(AnalysisResult result, CodebaseContext context);
}
```
## 4. Implementation Tiers
### Tier 1: Current State (No-Op)
The `ExportService` calls `enrich()`, but since no enrichers are registered, it just returns the core data. This is zero-impact refactoring.
### Tier 2: Enhanced Analysis
Once we implement `SpringMvcEnricher` or `RabbitMqEnricher`, we register them. They will:
1. Scan the `CodebaseContext`.
2. Find `TriggerPoints`.
3. Link them to the `transitions` already present in the `AnalysisResult`.
4. Update the `metadata` field.
## 5. Why this is "Good":
- **Non-Breaking**: `StateMachineAggregator` remains untouched.
- **Pluggable**: You can add/remove "Intelligence" without changing the core parser.
- **Future-Proof**: If you switch from JDT to another tool for code analysis, you only change the `Enricher` implementation.
## 6. Refactoring Steps
1. **Create `AnalysisResult`** and **`CodebaseMetadata`** classes.
2. **Update `ExportService.generateOutputs`** to take an `AnalysisResult` object instead of 4 separate parameters.
3. **Introduce `EnrichmentService`** that holds a list of `AnalysisEnricher`s.
4. **Modify `ExportService`** to call `enrichmentService.enrich(result, context)` before passing the result to exporters.

View File

@@ -0,0 +1,92 @@
# Generic and Extensible Endpoint Finder
## The Need for Genericity
Applications use various frameworks and patterns to trigger state machine events. While Spring MVC and Kafka are common, others like Micronuat, Jakarta EE, or custom internal event buses might be used.
## Configuration-Driven Detection
We can allow users to define "Entry Point" patterns via a configuration file (e.g., `analysis-config.json`).
```json
{
"entryPoints": [
{
"name": "SpringMVC",
"classAnnotations": ["RestController", "Controller"],
"methodAnnotations": ["PostMapping", "GetMapping", "PutMapping", "DeleteMapping", "RequestMapping"],
"metadataExtractors": {
"path": ["value", "path"]
}
},
{
"name": "Kafka",
"methodAnnotations": ["KafkaListener"],
"metadataExtractors": {
"topic": ["topics", "topicPartitions"]
}
}
]
}
```
## Abstract EntryPoint Detector
A generic detector that uses the above configuration to identify entry points.
```java
public class ConfigurableTriggerDetector implements TriggerDetector {
private final AnalysisConfig config;
@Override
public List<TriggerPoint> detect(CompilationUnit cu, CodebaseContext context) {
// 1. Find all classes
// 2. If class has any of config.classAnnotations
// 3. Scan methods for any of config.methodAnnotations
// 4. If found, look for sendEvent calls inside the method or its call tree
}
}
```
## Indirect Flow Detection (Call Tree Analysis)
Oftentimes, the `sendEvent` call is not directly in the controller method but in a service called by the controller.
### Simple Call Tree Analysis
1. Find all methods that call `sendEvent(E)`. Let's call them "Direct Triggers".
2. Find all methods that call "Direct Triggers".
3. Recursively build this "Trigger Path".
4. If a "Trigger Path" starts at an "Entry Point" (Controller/Listener), we have a complete flow.
Example:
`OrderController.submitOrder()` -> `OrderService.processOrder()` -> `StateMachine.sendEvent(SUBMIT)`
Analysis:
1. `OrderService.processOrder()` is a Direct Trigger for `SUBMIT`.
2. `OrderController.submitOrder()` calls `OrderService.processOrder()`.
3. `OrderController.submitOrder()` is an Entry Point (annotated with `@PostMapping`).
4. Result: `POST /order` -> `SUBMIT` transition.
## Handling Reactive Chains Statically
In WebFlux/Project Reactor, the logic is decoupled into a pipeline. Static analysis must "connect the dots" across operators.
**Strategy**:
1. When analyzing a method, identify if it returns a reactive type (`Mono`, `Flux`).
2. If so, inspect all lambdas within that method.
3. If a lambda calls `sendEvent`, treat the method as a "Trigger Point".
4. If the method is also an "Entry Point" (via annotations or `RouterFunction`), we have a direct reactive flow.
## Supporting Inheritance in Listeners
Often, base classes or interfaces define generic listeners:
```java
public interface BaseConsumer<T> {
@RabbitListener(queues = "${app.queue}")
void handle(T payload);
}
```
The `CodebaseContext` must resolve the `${app.queue}` if possible (via `application.properties` scanning) or at least note that it's a parameterized listener.
## Advanced Call Graph: The "Data Flow"
For a truly detailed analysis, we should track NOT just method calls, but also how the "Event" object is passed around.
- Is the event hardcoded? `sm.sendEvent(Events.CREATE)`
- Is it derived from a payload field? `sm.sendEvent(payload.getEvent())`
- Is it mapped via a `switch` or `Map`?
**Static Mapping Inference**:
If we see a `switch` statement on a payload field that leads to different `sendEvent` calls, we can report multiple possible flows from the same endpoint.

View File

@@ -0,0 +1,102 @@
# Implementation Details: Trigger Detection with JDT
## 1. Finding `sendEvent` Calls
We can use an `ASTVisitor` to find all `MethodInvocation` nodes.
```java
public class SendEventVisitor extends ASTVisitor {
@Override
public boolean visit(MethodInvocation node) {
if ("sendEvent".equals(node.getName().getIdentifier())) {
// Found a call!
// 1. Extract event (first argument)
// 2. Identify the enclosing method and class
}
return super.visit(node);
}
}
```
### Challenges in Event Extraction
Events can be:
- Enum constants: `Events.SUBMIT`
- Strings: `"SUBMIT"`
- Variables: `sm.sendEvent(eventFromPayload)` (Hard to resolve statically)
We should reuse `CodebaseContext.resolveState` logic (which is basically resolving an expression to a value/fqn).
## 2. Identifying Enclosing Context
Once a `sendEvent` is found, we can traverse up the AST to find the `MethodDeclaration` and `TypeDeclaration`.
```java
ASTNode parent = node.getParent();
while (parent != null && !(parent instanceof MethodDeclaration)) {
parent = parent.getParent();
}
// parent is now the MethodDeclaration
```
### 2. Extracting Annotations
From `MethodDeclaration`, we can check for mappings:
- `@PostMapping`, `@GetMapping`, etc.
- `@KafkaListener`: Extract `topics`, `groupId`.
- `@RabbitListener`: Extract `queues`, `bindings` (Exchange/RoutingKey).
From `TypeDeclaration`, we can check for:
- `@RestController`, `@Controller`
- `@RequestMapping` at class level (to get base path)
## 3. Specialized WebFlux Analysis
### Annotation-based WebFlux
This is largely identical to Spring MVC. The challenge is if the `sendEvent` is wrapped in a reactive operator.
```java
public Mono<Void> submit(Order order) {
return service.save(order)
.doOnNext(o -> stateMachine.sendEvent(Events.SUBMIT))
.then();
}
```
**Static Strategy**: We need to look inside `LambdaExpression` nodes passed to reactive operators (`doOnNext`, `flatMap`, `map`, `subscribe`). The `ASTVisitor` should traverse into these lambdas.
### Functional WebFlux (RouterFunctions)
Functional endpoints are often defined as Beans returning `RouterFunction`.
```java
@Bean
public RouterFunction<ServerResponse> route(OrderHandler handler) {
return RouterFunctions.route(POST("/orders"), handler::submitOrder);
}
```
**Static Strategy**:
1. Find methods returning `RouterFunction`.
2. Analyze the `MethodInvocation` chain (`route`, `andRoute`, `nest`).
3. Extract the URI pattern and the `HandlerFunction` reference.
4. If the handler is a method reference (`handler::submitOrder`), link it to the corresponding `MethodDeclaration`.
## 4. Specialized RabbitMQ Analysis
`@RabbitListener` can be complex:
```java
@RabbitListener(bindings = @QueueBinding(
value = @Queue(value = "orderQueue", durable = "true"),
exchange = @Exchange(value = "orderExchange"),
key = "order.created"
))
public void onOrder(Order order) { ... }
```
**Static Strategy**:
1. Find `@RabbitListener`.
2. If it has `bindings`, drill down into `@QueueBinding`, `@Queue`, `@Exchange` to extract the topology.
3. If it only has `queues`, resolve the queue name (might be a SpEL expression or property placeholder, which we can try to resolve or just keep as-is).
## 5. Indirect Flow Detection (The "Service Link")
If the project has multiple state machines, we need to know which one is being targeted.
Usually, this is done via:
- Autowiring by type: `StateMachine<States, Events> sm;`
- Autowiring by name: `@Qualifier("mySm") StateMachine sm;`
We can look at the fields of the class where `sendEvent` is called.
## 6. Output Enhancement
The `Exporter` should be modified to:
- In DOT: Add nodes for Endpoints/Listeners and link them to the Events/Transitions.
- In SCXML: Add metadata to transitions.

View File

@@ -0,0 +1,71 @@
# Spring Property and Value Resolution Strategy
## 1. The Challenge of "Injected" Metadata
In Spring, metadata (like Queue names, Topic names, or even Event names) is often not hardcoded but injected using various mechanisms. To provide an accurate map, the analyzer must resolve these values.
## 2. Supported Injection Patterns
### Field Injection
```java
@Value("${app.order-queue}")
private String orderQueue;
```
**Static Strategy**:
1. Scan all fields for `@Value`.
2. Map `FieldName` -> `Placeholder/Value`.
3. If `${...}` is found, resolve it via the `PropertyResolver`.
### Constructor and Setter Injection
```java
public OrderService(@Value("${app.event.submit}") String submitEvent) {
this.submitEvent = submitEvent;
}
```
**Static Strategy**:
1. Scan constructor/method parameters for `@Value`.
2. Trace the assignment to a class field (`this.submitEvent = ...`).
3. Store the mapping for that field.
### @ConfigurationProperties
```java
@ConfigurationProperties(prefix = "app.messaging")
public class AppProps {
private String queueName;
}
```
**Static Strategy**:
1. Identify `@ConfigurationProperties` classes.
2. Index their fields with the prefix (e.g., `app.messaging.queueName`).
3. When these beans are injected into a service, link the service's usage to the resolved property value.
## 3. Property Resolver (The "Config Scanner")
A component dedicated to building a global map of available properties.
1. **File Scanning**: Parse `application.properties`, `application.yml`, and `bootstrap.yml`.
2. **Profile Support**: Handle `application-{profile}.yml` if a profile is active.
3. **Property Map**: Create a `Map<String, String>` of all key-value pairs.
## 4. Value Propagation (Data Flow)
Once a value is resolved or its placeholder is identified, we track its usage.
**Example Trace**:
1. `application.yml` -> `app.queue: "orders"`
2. `OrderService` -> `@Value("${app.queue}") String q;` -> `this.queue = q;`
3. `OrderService.send()` -> `sm.sendEvent(this.queue);`
4. **Resolution**: The "Event" for this trigger is "orders".
## 5. SpEL (Spring Expression Language) Lite
Full SpEL support is hard for static analysis, but we can support "Common Patterns":
- Simple bean property access: `#{myBean.name}`
- System properties: `#{systemProperties['user.dir']}`
- Ternary operators: `${app.enabled ? 'active' : 'inactive'}`
## 6. Constant Resolution
If an annotation uses a reference to a constant:
```java
@RabbitListener(queues = MyConstants.ORDER_QUEUE)
```
**Static Strategy**:
1. Use JDT to resolve the `QualifiedName` (`MyConstants.ORDER_QUEUE`).
2. Fetch the `TypeDeclaration` for `MyConstants`.
3. Find the `VariableDeclarationFragment` for `ORDER_QUEUE` and extract its literal initializer.

View File

@@ -1,5 +1,6 @@
package click.kamil.springstatemachineexporter; package click.kamil.springstatemachineexporter;
import click.kamil.springstatemachineexporter.analysis.service.EnrichmentService;
import click.kamil.springstatemachineexporter.exporter.Dot; import click.kamil.springstatemachineexporter.exporter.Dot;
import click.kamil.springstatemachineexporter.exporter.JsonExporter; import click.kamil.springstatemachineexporter.exporter.JsonExporter;
import click.kamil.springstatemachineexporter.exporter.PlantUml; import click.kamil.springstatemachineexporter.exporter.PlantUml;
@@ -8,13 +9,15 @@ import click.kamil.springstatemachineexporter.command.ExporterCommand;
import click.kamil.springstatemachineexporter.service.ExportService; import click.kamil.springstatemachineexporter.service.ExportService;
import picocli.CommandLine; import picocli.CommandLine;
import java.util.Collections;
import java.util.List; import java.util.List;
public class Main { public class Main {
public static void main(String[] args) { public static void main(String[] args) {
// Manual DI / Wiring // Manual DI / Wiring
var exporters = List.of(new PlantUml(), new Dot(), new Scxml(), new JsonExporter()); var exporters = List.of(new PlantUml(), new Dot(), new Scxml(), new JsonExporter());
var exportService = new ExportService(exporters); var enrichmentService = new EnrichmentService(Collections.emptyList());
var exportService = new ExportService(exporters, enrichmentService);
var command = new ExporterCommand(exportService); var command = new ExporterCommand(exportService);
int exitCode = new CommandLine(command).execute(args); int exitCode = new CommandLine(command).execute(args);

View File

@@ -0,0 +1,8 @@
package click.kamil.springstatemachineexporter.analysis.enricher;
import click.kamil.springstatemachineexporter.analysis.model.AnalysisResult;
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
public interface AnalysisEnricher {
void enrich(AnalysisResult result, CodebaseContext context);
}

View File

@@ -0,0 +1,41 @@
package click.kamil.springstatemachineexporter.analysis.model;
import click.kamil.springstatemachineexporter.model.Transition;
import lombok.Builder;
import lombok.Getter;
import java.util.Collections;
import java.util.List;
import java.util.Set;
@Getter
@Builder
public class AnalysisResult {
private final String name;
private final List<Transition> transitions;
private final Set<String> startStates;
private final Set<String> endStates;
private final boolean renderChoicesAsDiamonds;
@Builder.Default
private CodebaseMetadata metadata = CodebaseMetadata.empty();
public void addMetadata(CodebaseMetadata newMetadata) {
if (newMetadata == null) return;
List<CodebaseMetadata.TriggerPoint> combinedTriggers = new java.util.ArrayList<>(this.metadata.getTriggers());
combinedTriggers.addAll(newMetadata.getTriggers());
List<CodebaseMetadata.EntryPoint> combinedEntryPoints = new java.util.ArrayList<>(this.metadata.getEntryPoints());
combinedEntryPoints.addAll(newMetadata.getEntryPoints());
java.util.Map<String, String> combinedProperties = new java.util.HashMap<>(this.metadata.getProperties());
combinedProperties.putAll(newMetadata.getProperties());
this.metadata = CodebaseMetadata.builder()
.triggers(Collections.unmodifiableList(combinedTriggers))
.entryPoints(Collections.unmodifiableList(combinedEntryPoints))
.properties(Collections.unmodifiableMap(combinedProperties))
.build();
}
}

View File

@@ -0,0 +1,28 @@
package click.kamil.springstatemachineexporter.analysis.model;
import lombok.Builder;
import lombok.Getter;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
@Getter
@Builder(toBuilder = true)
public class CodebaseMetadata {
private final List<TriggerPoint> triggers;
private final List<EntryPoint> entryPoints;
private final Map<String, String> properties;
public static CodebaseMetadata empty() {
return CodebaseMetadata.builder()
.triggers(Collections.emptyList())
.entryPoints(Collections.emptyList())
.properties(Collections.emptyMap())
.build();
}
public record TriggerPoint(String className, String methodName, String event, Map<String, String> metadata) {}
public record EntryPoint(String className, String methodName, String type, Map<String, String> metadata) {}
}

View File

@@ -0,0 +1,19 @@
package click.kamil.springstatemachineexporter.analysis.service;
import click.kamil.springstatemachineexporter.analysis.enricher.AnalysisEnricher;
import click.kamil.springstatemachineexporter.analysis.model.AnalysisResult;
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
import lombok.RequiredArgsConstructor;
import java.util.List;
@RequiredArgsConstructor
public class EnrichmentService {
private final List<AnalysisEnricher> enrichers;
public void enrich(AnalysisResult result, CodebaseContext context) {
for (AnalysisEnricher enricher : enrichers) {
enricher.enrich(result, context);
}
}
}

View File

@@ -1,5 +1,7 @@
package click.kamil.springstatemachineexporter.service; package click.kamil.springstatemachineexporter.service;
import click.kamil.springstatemachineexporter.analysis.model.AnalysisResult;
import click.kamil.springstatemachineexporter.analysis.service.EnrichmentService;
import click.kamil.springstatemachineexporter.ast.app.AstTransitionParser; import click.kamil.springstatemachineexporter.ast.app.AstTransitionParser;
import click.kamil.springstatemachineexporter.ast.app.StateMachineAggregator; import click.kamil.springstatemachineexporter.ast.app.StateMachineAggregator;
import click.kamil.springstatemachineexporter.ast.app.TransitionStateUtils; import click.kamil.springstatemachineexporter.ast.app.TransitionStateUtils;
@@ -17,6 +19,7 @@ import java.io.IOException;
import java.io.PrintWriter; import java.io.PrintWriter;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.Collections;
import java.util.HashSet; import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Set; import java.util.Set;
@@ -26,6 +29,7 @@ import java.util.Set;
public class ExportService { public class ExportService {
private final List<StateMachineExporter> exporters; private final List<StateMachineExporter> exporters;
private final EnrichmentService enrichmentService;
private static final List<String> TARGET_ANNOTATIONS = List.of("EnableStateMachineFactory", "EnableStateMachine"); private static final List<String> TARGET_ANNOTATIONS = List.of("EnableStateMachineFactory", "EnableStateMachine");
public static final Set<String> STATE_MACHINE_RETURN_TYPES = Set.of("StateMachine", "StateMachineFactory", "StateMachineModelFactory"); public static final Set<String> STATE_MACHINE_RETURN_TYPES = Set.of("StateMachine", "StateMachineFactory", "StateMachineModelFactory");
@@ -38,7 +42,16 @@ public class ExportService {
public void runJsonExporter(Path jsonFile, Path outputDir, List<String> selectedFormats) throws IOException { public void runJsonExporter(Path jsonFile, Path outputDir, List<String> selectedFormats) throws IOException {
JsonImportService jsonImportService = new JsonImportService(); JsonImportService jsonImportService = new JsonImportService();
StateMachineModel model = jsonImportService.importJson(jsonFile); StateMachineModel model = jsonImportService.importJson(jsonFile);
generateOutputs(outputDir, model.getName(), model.getTransitions(), model.getStartStates(), model.getEndStates(), selectedFormats, model.isRenderChoicesAsDiamonds());
AnalysisResult result = AnalysisResult.builder()
.name(model.getName())
.transitions(model.getTransitions())
.startStates(model.getStartStates())
.endStates(model.getEndStates())
.renderChoicesAsDiamonds(model.isRenderChoicesAsDiamonds())
.build();
generateOutputs(outputDir, result, selectedFormats);
} }
private void exportAll(CodebaseContext context, Path outputDir, List<String> selectedFormats, boolean renderChoicesAsDiamonds) throws IOException { private void exportAll(CodebaseContext context, Path outputDir, List<String> selectedFormats, boolean renderChoicesAsDiamonds) throws IOException {
@@ -84,7 +97,17 @@ public class ExportService {
Set<String> startStates = TransitionStateUtils.findStartStates(transitions, initialStatesAst); Set<String> startStates = TransitionStateUtils.findStartStates(transitions, initialStatesAst);
Set<String> endStates = TransitionStateUtils.findEndStates(transitions, endStatesAst); Set<String> endStates = TransitionStateUtils.findEndStates(transitions, endStatesAst);
generateOutputs(outputDir, className, transitions, startStates, endStates, selectedFormats, renderChoicesAsDiamonds); AnalysisResult result = AnalysisResult.builder()
.name(className)
.transitions(transitions)
.startStates(startStates)
.endStates(endStates)
.renderChoicesAsDiamonds(renderChoicesAsDiamonds)
.build();
enrichmentService.enrich(result, context);
generateOutputs(outputDir, result, selectedFormats);
} }
private void processBeanMethod(MethodDeclaration m, Path outputDir, CodebaseContext context, List<String> selectedFormats, boolean renderChoicesAsDiamonds) throws IOException { private void processBeanMethod(MethodDeclaration m, Path outputDir, CodebaseContext context, List<String> selectedFormats, boolean renderChoicesAsDiamonds) throws IOException {
@@ -100,18 +123,27 @@ public class ExportService {
Set<String> startStates = TransitionStateUtils.findStartStates(transitions); Set<String> startStates = TransitionStateUtils.findStartStates(transitions);
Set<String> endStates = TransitionStateUtils.findEndStates(transitions); Set<String> endStates = TransitionStateUtils.findEndStates(transitions);
generateOutputs(outputDir, uniqueName, transitions, startStates, endStates, selectedFormats, renderChoicesAsDiamonds); AnalysisResult result = AnalysisResult.builder()
.name(uniqueName)
.transitions(transitions)
.startStates(startStates)
.endStates(endStates)
.renderChoicesAsDiamonds(renderChoicesAsDiamonds)
.build();
enrichmentService.enrich(result, context);
generateOutputs(outputDir, result, selectedFormats);
} }
private void generateOutputs(Path outputDir, String baseName, List<Transition> transitions, private void generateOutputs(Path outputDir, AnalysisResult result, List<String> selectedFormats) throws IOException {
Set<String> startStates, Set<String> endStates, List<String> selectedFormats, boolean renderChoicesAsDiamonds) throws IOException {
Path targetDir = outputDir.resolve(baseName); Path targetDir = outputDir.resolve(result.getName());
Files.createDirectories(targetDir); Files.createDirectories(targetDir);
ExportOptions options = ExportOptions.builder() ExportOptions options = ExportOptions.builder()
.useLambdaGuards(true) .useLambdaGuards(true)
.renderChoicesAsDiamonds(renderChoicesAsDiamonds) .renderChoicesAsDiamonds(result.isRenderChoicesAsDiamonds())
.build(); .build();
for (StateMachineExporter output : exporters) { for (StateMachineExporter output : exporters) {
@@ -120,8 +152,8 @@ public class ExportService {
if (!match) continue; if (!match) continue;
} }
String content = output.export(baseName, transitions, startStates, endStates, options); String content = output.export(result.getName(), result.getTransitions(), result.getStartStates(), result.getEndStates(), options);
String fileName = baseName + output.getFileExtension(); String fileName = result.getName() + output.getFileExtension();
try (PrintWriter out = new PrintWriter(targetDir.resolve(fileName).toFile())) { try (PrintWriter out = new PrintWriter(targetDir.resolve(fileName).toFile())) {
out.println(content); out.println(content);

View File

@@ -1,5 +1,6 @@
package click.kamil.springstatemachineexporter; package click.kamil.springstatemachineexporter;
import click.kamil.springstatemachineexporter.analysis.service.EnrichmentService;
import click.kamil.springstatemachineexporter.exporter.PlantUml; import click.kamil.springstatemachineexporter.exporter.PlantUml;
import click.kamil.springstatemachineexporter.exporter.StateMachineExporter; import click.kamil.springstatemachineexporter.exporter.StateMachineExporter;
import click.kamil.springstatemachineexporter.service.ExportService; import click.kamil.springstatemachineexporter.service.ExportService;
@@ -12,6 +13,7 @@ import org.junit.jupiter.params.provider.MethodSource;
import java.io.IOException; import java.io.IOException;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.Collections;
import java.util.List; import java.util.List;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
@@ -20,7 +22,8 @@ import static org.assertj.core.api.Assertions.assertThat;
public class PlantUmlRenderTest { public class PlantUmlRenderTest {
private final List<StateMachineExporter> exporters = List.of(new PlantUml()); private final List<StateMachineExporter> exporters = List.of(new PlantUml());
private final ExportService exportService = new ExportService(exporters); private final EnrichmentService enrichmentService = new EnrichmentService(Collections.emptyList());
private final ExportService exportService = new ExportService(exporters, enrichmentService);
private static List<TestScenario> provideTestScenarios() { private static List<TestScenario> provideTestScenarios() {
return List.of( return List.of(

View File

@@ -1,5 +1,6 @@
package click.kamil.springstatemachineexporter; package click.kamil.springstatemachineexporter;
import click.kamil.springstatemachineexporter.analysis.service.EnrichmentService;
import click.kamil.springstatemachineexporter.exporter.Dot; import click.kamil.springstatemachineexporter.exporter.Dot;
import click.kamil.springstatemachineexporter.exporter.JsonExporter; import click.kamil.springstatemachineexporter.exporter.JsonExporter;
import click.kamil.springstatemachineexporter.exporter.PlantUml; import click.kamil.springstatemachineexporter.exporter.PlantUml;
@@ -13,6 +14,7 @@ import org.junit.jupiter.params.provider.MethodSource;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.Collections;
import java.util.List; import java.util.List;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
@@ -20,7 +22,8 @@ import static org.assertj.core.api.Assertions.assertThat;
public class RegressionTest { public class RegressionTest {
private final List<StateMachineExporter> exporters = List.of(new PlantUml(), new Dot(), new Scxml(), new JsonExporter()); private final List<StateMachineExporter> exporters = List.of(new PlantUml(), new Dot(), new Scxml(), new JsonExporter());
private final ExportService exportService = new ExportService(exporters); private final EnrichmentService enrichmentService = new EnrichmentService(Collections.emptyList());
private final ExportService exportService = new ExportService(exporters, enrichmentService);
private static List<TestScenario> provideTestScenarios() { private static List<TestScenario> provideTestScenarios() {
return List.of( return List.of(