Skip to content

Commit f442c75

Browse files
authored
Merge pull request #28 from solutionforest/claude/review-and-docs-ncvzh
2 parents 55b16f8 + 4f54059 commit f442c75

42 files changed

Lines changed: 2404 additions & 408 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

AGENTS.md

Lines changed: 259 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,259 @@
1+
# AGENTS.md
2+
3+
Instructions for AI agents working on this codebase. Read CLAUDE.md first for project context.
4+
5+
---
6+
7+
## General Rules
8+
9+
1. **Read before you write.** Never modify a file you haven't read. Understand the existing patterns.
10+
2. **Zero production dependencies.** This is a framework-agnostic library. Never add runtime `require` entries to composer.json. Dev dependencies are fine.
11+
3. **PHP 8.3+ only.** Use readonly properties, backed enums, match expressions, named arguments, constructor promotion. No legacy patterns.
12+
4. **Run the full quality pipeline** after any change: `composer ci` (pint:test + analyze + test).
13+
5. **PHPStan level 6.** All new code must pass. Don't suppress errors unless truly unavoidable—and document why.
14+
6. **Pest PHP for tests.** Use `test()` syntax, `expect()` assertions, `describe()` blocks. No raw PHPUnit `$this->assert*()`.
15+
7. **Laravel Pint formatting.** Run `composer pint` before committing. The CI will reject improperly formatted code.
16+
17+
---
18+
19+
## Agent: Code Review
20+
21+
When reviewing PRs or code changes for this project:
22+
23+
### What to Check
24+
25+
- **Contract compliance**: Does new code implement `WorkflowAction`, `StorageAdapter`, `EventDispatcher`, or `Logger` correctly?
26+
- **Immutability**: `WorkflowContext`, `ActionResult`, `WorkflowDefinition` are value objects. Don't add setters or mutation methods.
27+
- **State transitions**: Any code that calls `setState()` must respect the transition rules in `WorkflowState::canTransitionTo()`. Invalid transitions are bugs.
28+
- **Action instantiation**: Actions are created with `new $actionClass($config, $logger)`. If you change the `WorkflowAction` interface or `BaseAction` constructor, you must update `Executor::executeAction()`.
29+
- **No framework coupling**: This package must not `use` any Laravel, Symfony, or other framework classes in `src/`. The only exception is `data_get()`/`data_set()` helpers (which need to be replaced—see Known Issues).
30+
- **Exception hierarchy**: All exceptions must extend `WorkflowException`. Use the static factory methods (e.g., `InvalidWorkflowDefinitionException::invalidName()`), not raw `new` constructors.
31+
- **Event consistency**: Every state-changing operation should dispatch the appropriate event. Check that `WorkflowStarted`, `WorkflowCompleted`, `WorkflowFailed`, `WorkflowCancelled`, `StepCompleted`, `StepFailed` are dispatched at the right moments.
32+
33+
### What to Reject
34+
35+
- Adding `declare(strict_types=1)` piecemeal—if we add it, it goes in every file at once.
36+
- Magic string config keys without constants or typed config objects.
37+
- Suppressing PHPStan errors without a clear comment explaining why.
38+
- Tests that use `assertTrue(true)` or other vacuous assertions.
39+
- New public API methods without PHPDoc `@param`, `@return`, and `@throws` tags.
40+
41+
---
42+
43+
## Agent: Testing
44+
45+
### Test Structure
46+
47+
```
48+
tests/
49+
├── Unit/ # Isolated class tests (no I/O, no storage)
50+
├── Integration/ # Multi-step workflow execution through the engine
51+
├── RealWorld/ # Complex production-like scenarios
52+
├── Actions/ECommerce/ # Custom action fixtures used by RealWorld tests
53+
├── Support/ # Test helpers (InMemoryStorage)
54+
├── TestCase.php # Base class: provides $this->engine + $this->storage
55+
├── Pest.php # Pest config
56+
├── ArchTest.php # Architecture constraints
57+
└── ExampleTest.php # Sanity check
58+
```
59+
60+
### Writing Tests
61+
62+
```php
63+
// Good: descriptive, focused, uses Pest syntax
64+
test('workflow transitions to failed state when action throws', function () {
65+
$definition = [
66+
'name' => 'failing-workflow',
67+
'steps' => [
68+
['id' => 'bad_step', 'action' => NonExistentAction::class],
69+
],
70+
];
71+
72+
expect(fn () => $this->engine->start('test-fail', $definition))
73+
->toThrow(ActionNotFoundException::class);
74+
});
75+
76+
// Good: grouped with describe
77+
describe('WorkflowBuilder', function () {
78+
test('validates step IDs', function () { ... });
79+
test('rejects empty workflow names', function () { ... });
80+
});
81+
```
82+
83+
### Test Gaps to Fill
84+
85+
These areas currently lack test coverage. Prioritize them when writing new tests:
86+
87+
1. **Event dispatch verification** — No tests confirm events are actually dispatched. Create a `SpyEventDispatcher` that records dispatched events and assert against it.
88+
2. **Retry logic**`Step` supports `retryAttempts` but the `Executor` never actually retries. When retry is implemented, add tests for 0, 1, and max retries.
89+
3. **HTTP/Email actions**`HttpAction` and `EmailAction` have no unit tests. Mock the underlying operations and test config validation, error handling, and result mapping.
90+
4. **Storage adapter edge cases** — Only `InMemoryStorage` is tested. Add tests for: loading a non-existent instance, concurrent saves, findInstances with various filter combinations.
91+
5. **Condition evaluation** — The regex-based condition parser in `Step::evaluateCondition()` silently returns `true` for unparseable conditions. Test edge cases: nested properties, numeric comparisons, boolean values, empty strings.
92+
6. **Compensation actions**`Step` supports `compensationAction` but nothing executes it. When implemented, test rollback sequences.
93+
7. **Pause/resume cycles** — Test multiple pause → resume → pause sequences and verify state consistency.
94+
95+
---
96+
97+
## Agent: Implementation
98+
99+
### Before Writing Code
100+
101+
1. Check if the feature touches any contract interface. Interface changes require updates to all implementations (including `InMemoryStorage`, `NullLogger`, `NullEventDispatcher`).
102+
2. Check if the change affects the builder API. Builder changes should maintain backward compatibility—add new methods, don't change existing signatures.
103+
3. Check if new exceptions are needed. Use the existing hierarchy and static factory pattern.
104+
105+
### Patterns to Follow
106+
107+
**Creating a new Action:**
108+
```php
109+
class MyAction extends BaseAction
110+
{
111+
protected function doExecute(WorkflowContext $context): ActionResult
112+
{
113+
$value = $this->getConfig('key');
114+
// ... business logic ...
115+
return ActionResult::success(['output' => $result]);
116+
}
117+
118+
public function getName(): string
119+
{
120+
return 'My Action';
121+
}
122+
123+
public function getDescription(): string
124+
{
125+
return 'Does something specific';
126+
}
127+
}
128+
```
129+
130+
**Creating a new Exception:**
131+
```php
132+
class MyException extends WorkflowException
133+
{
134+
public static function specificCase(string $id): self
135+
{
136+
return new self(
137+
message: "Technical: thing failed for {$id}",
138+
userMessage: "The operation could not be completed.",
139+
suggestions: ['Check the configuration', 'Verify the ID exists'],
140+
context: ['id' => $id]
141+
);
142+
}
143+
}
144+
```
145+
146+
**Creating a new Event:**
147+
```php
148+
class MyEvent
149+
{
150+
public function __construct(
151+
public readonly string $workflowId,
152+
public readonly string $detail,
153+
public readonly \DateTimeInterface $occurredAt = new \DateTime(),
154+
) {}
155+
}
156+
```
157+
158+
---
159+
160+
## Known Issues and Improvement Roadmap
161+
162+
These are real issues found through code review, ordered by impact. This is not a wishlist—these are bugs, missing implementations, and design problems that need fixing before v1.0.
163+
164+
### Critical — Blocks Production Use
165+
166+
**1. Timeout is configured but never enforced**
167+
`Step` accepts a `timeout` parameter. `WorkflowBuilder` validates it. But `Executor::executeStep()` never checks it. A misbehaving action can hang the entire process indefinitely. The TODO comment at `Executor.php:230` confirms this is known.
168+
169+
*Fix:* Implement timeout enforcement in `Executor::executeStep()` using `pcntl_alarm()` or a wrapper that throws `StepExecutionException` on timeout. Add a `TimeoutException` to the exception hierarchy.
170+
171+
**2. Retry logic is declared but never executed**
172+
`Step::getRetryAttempts()` returns a value, but `Executor` catches exceptions and immediately fails. No retry loop exists anywhere in the execution path.
173+
174+
*Fix:* Add a retry loop in `Executor::executeStep()` with exponential backoff. Dispatch `StepRetryEvent` on each retry attempt. Track attempt count on the `WorkflowInstance`.
175+
176+
**3. Compensation actions are defined but never called**
177+
`Step` supports `compensationAction` and `hasCompensation()`. Nothing in the codebase ever calls a compensation action. The Saga pattern is advertised but not implemented.
178+
179+
*Fix:* When a step fails, walk backward through completed steps and execute their compensation actions. Add a `CompensationExecutedEvent`. This is a significant feature — design it before implementing.
180+
181+
**4. `data_get()` / `data_set()` Laravel helper dependency**
182+
`Step::evaluateCondition()` and `WorkflowDefinition::evaluateCondition()` call `data_get()`, a Laravel helper. This function doesn't exist in non-Laravel environments. The package advertises itself as framework-agnostic but will throw a fatal error without Laravel's helpers.
183+
184+
*Fix:* Implement a simple `Support\Arr::get()` utility that handles dot-notation access, or inline the logic. Remove the PHPStan suppression for `data_get`/`data_set`.
185+
186+
### High — Correctness Issues
187+
188+
**5. Duplicate condition evaluation logic**
189+
`Step::evaluateCondition()` (line 221) and `WorkflowDefinition::evaluateCondition()` contain identical regex-based condition parsing. This violates DRY and means bug fixes must be applied in two places.
190+
191+
*Fix:* Extract to a `Support\ConditionEvaluator` class. Both `Step` and `WorkflowDefinition` should delegate to it.
192+
193+
**6. Silent condition parsing failures**
194+
`Step::evaluateCondition()` returns `true` when it can't parse a condition (line 244). This means a typo in a condition expression (`user.plan = "premium"` instead of `===`) will silently pass, executing steps that should be skipped.
195+
196+
*Fix:* Throw `InvalidWorkflowDefinitionException` for unparseable conditions, or at minimum log a warning. Never silently succeed.
197+
198+
**7. Inconsistent event class naming**
199+
Some events end with `Event` (`StepCompletedEvent`, `WorkflowCompletedEvent`, `WorkflowFailedEvent`) and some don't (`WorkflowStarted`, `WorkflowCancelled`). Pick one convention and stick with it.
200+
201+
*Fix:* Rename all to `*Event` suffix for consistency: `WorkflowStartedEvent`, `WorkflowCancelledEvent`.
202+
203+
**8. Duplicate API methods on WorkflowEngine**
204+
`getInstance()` and `getWorkflow()` do the same thing (lines 203 and 265). `getInstances()` and `listWorkflows()` do the same thing (lines 238 and 294). This confuses consumers and doubles the API surface.
205+
206+
*Fix:* Deprecate `getWorkflow()` and `listWorkflows()`. Keep `getInstance()` and `getInstances()` as the canonical API. Remove the deprecated methods before v1.0.
207+
208+
### Medium — Design Improvements
209+
210+
**9. Action constructor not enforced by contract**
211+
`Executor::executeAction()` (line 300) does `new $actionClass($config, $logger)`. The `WorkflowAction` interface doesn't define a constructor, so custom actions with different constructors will fail at runtime with a cryptic error.
212+
213+
*Fix:* Either document the constructor contract clearly, add a static factory method to the interface (`WorkflowAction::make($config, $logger)`), or use an `ActionFactory` that can be overridden for DI containers.
214+
215+
**10. `WorkflowInstance` does too much**
216+
`WorkflowInstance` handles state tracking, progress calculation, step management, data merging, serialization, and error recording. It's a god object.
217+
218+
*Fix:* Extract `WorkflowProgress` (progress calculation, step completion tracking) and `WorkflowSerializer` (toArray/fromArray) into separate concerns. Keep `WorkflowInstance` focused on identity and state.
219+
220+
**11. No middleware/pipeline for cross-cutting concerns**
221+
Retry, timeout, logging, and metrics all need to wrap step execution. Currently there's no clean way to add these behaviors without modifying `Executor` directly.
222+
223+
*Fix:* Implement a `StepMiddleware` interface and a pipeline in `Executor` that chains middleware around action execution. Ship `RetryMiddleware`, `TimeoutMiddleware`, and `LoggingMiddleware` as built-in implementations.
224+
225+
**12. Condition evaluator is too limited**
226+
The regex-based condition parser only supports simple binary comparisons. No AND/OR, no grouping, no function calls. This limits real-world usefulness significantly.
227+
228+
*Fix:* Consider a simple expression parser or adopt a lightweight expression language. At minimum, support AND (`&&`) and OR (`||`) operators.
229+
230+
### Low — Polish
231+
232+
**13. Missing `declare(strict_types=1)`**
233+
No source file declares strict types. This allows implicit type coercion which can hide bugs.
234+
235+
**14. PHPStan could be stricter**
236+
Level 6 is good but not maximum. Several errors are suppressed in `phpstan.neon.dist`. Work toward level 8 by fixing the underlying type issues rather than suppressing them.
237+
238+
**15. Excessive inline documentation**
239+
Some classes (e.g., `WorkflowContext`) have 600+ lines with extensive code examples in PHPDoc. This makes files hard to navigate. Move examples to a `docs/` directory or the README.
240+
241+
**16. No integration test for event dispatching**
242+
The event system is a core feature but no test verifies that events are dispatched. Add a `SpyEventDispatcher` to the test support classes and use it in integration tests.
243+
244+
---
245+
246+
## PR Checklist
247+
248+
Before approving any PR, verify:
249+
250+
- [ ] `composer ci` passes (pint:test + phpstan + pest)
251+
- [ ] New public methods have `@param`, `@return`, and `@throws` PHPDoc
252+
- [ ] No new PHPStan suppressions without justification
253+
- [ ] No framework-specific imports in `src/`
254+
- [ ] State transitions respect `WorkflowState::canTransitionTo()`
255+
- [ ] New features have corresponding tests in the appropriate directory
256+
- [ ] Exception messages are helpful (use static factory methods with context)
257+
- [ ] Events are dispatched for state-changing operations
258+
- [ ] No `dd()`, `dump()`, `var_dump()`, or `ray()` calls (ArchTest enforces this)
259+
- [ ] Builder API changes are backward-compatible

0 commit comments

Comments
 (0)