|
| 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