diff --git a/MILESTONES.md b/MILESTONES.md
new file mode 100644
index 0000000..2718bde
--- /dev/null
+++ b/MILESTONES.md
@@ -0,0 +1,42 @@
+# Beacon Milestones
+
+## Phase 3: Planning
+
+1. **Project Infrastructure & Core Dispatcher**
+ - [x] Initialize `composer.json` with `getphred/beacon-contracts` and PHP 8.2 requirements.
+ - [x] Configure PHPUnit and test structure.
+ - [x] Implement `EventDispatcher` (PSR-14 compliant).
+ - [x] Implement basic `ListenerProvider`.
+ - [x] Implement `CanStopPropagation` trait for stoppable events.
+ - [x] **Test Coverage**: Unit tests for core dispatching and stoppable behavior.
+
+2. **Advanced Listener Resolution**
+ - [x] Implement Priority-based listener sorting.
+ - [x] Implement Inheritance Resolution (Event class, Parents, Interfaces).
+ - [x] Implement Performance Toggle for Interface scanning.
+ - [x] Implement Wildcard pattern support (`*`) and internal regex caching.
+ - [x] **Test Coverage**: Unit tests for sorting, wildcards, and inheritance edge cases.
+
+3. **Middleware Pipeline (The Onion)**
+ - [x] Implement `MiddlewarePipeline` runner.
+ - [x] Integrate Middleware with `EventDispatcher`.
+ - [x] Implement basic Telemetry (timing/memory) within the pipeline.
+ - [x] **Test Coverage**: Integration tests for middleware execution order and telemetry accuracy.
+
+4. **Discovery & Registration**
+ - [x] Implement `#[AsEventListener]` and `#[AsSubscriber]` attribute support.
+ - [x] Implement `ListenerDiscovery` service for scanning directories.
+ - [x] Integrate PSR-16 cache for discovery results.
+ - [x] Implement PSR-11 container integration for lazy loading.
+ - [x] **Test Coverage**: Integration tests for scanning, attribute resolution, and cache persistence.
+
+5. **Resiliency & Advanced State**
+ - [x] Implement `FAIL_FAST` and `CONTINUE` resiliency modes.
+ - [x] Implement `DispatchFailed` internal event for `CONTINUE` mode.
+ - [x] Implement `EventEnvelope` for carrying immutable context.
+ - [x] **Test Coverage**: Unit tests for error handling and context mapping.
+
+6. **Testing Helpers & Verification**
+ - [x] Implement `EventCollector` for assertions.
+ - [x] Create `EventAssertions` trait for PHPUnit.
+ - [x] **Final Verification**: Run full suite; zero failures, zero deprecations.
diff --git a/SPECS.md b/SPECS.md
new file mode 100644
index 0000000..970337b
--- /dev/null
+++ b/SPECS.md
@@ -0,0 +1,74 @@
+# Beacon Specification (SPECS)
+
+## Overview
+Beacon is a high-performance, PSR-14 compliant event management system. It is designed to be decoupled, utilizing `BeaconContracts` for interfaces and providing an enterprise-grade engine with middleware support, attribute-based discovery, and telemetry.
+
+---
+
+## 1. Core Interfaces (BeaconContracts)
+The foundation of Beacon must be defined in `getphred/beacon-contracts`.
+
+- **EventDispatcherInterface**: Extends `Psr\EventDispatcher\EventDispatcherInterface`.
+- **ListenerProviderInterface**: Extends `Psr\EventDispatcher\ListenerProviderInterface`.
+- **SubscriberInterface**: Defines `getSubscribedEvents(): array`.
+- **StoppableEventInterface**: Extends `Psr\EventDispatcher\StoppableEventInterface`.
+- **DispatchMiddlewareInterface**: Defines `handle(object $event, callable $next): object`.
+
+---
+
+## 2. Event Dispatcher (The Engine)
+The primary implementation of the dispatcher.
+
+- **PSR-14 Compliance**: Must correctly handle event objects and return them.
+- **Middleware Support**: Implements an "Onion" style pipeline.
+ - Global middleware can intercept every dispatch.
+ - Middleware can modify the event or stop propagation.
+- **Resiliency Modes**:
+ - `FAIL_FAST`: Stops execution on the first listener exception.
+ - `CONTINUE`: Logs the error, dispatches a `DispatchFailed` internal event, and moves to the next listener.
+- **Telemetry**: Internal tracking of listener execution time and memory usage.
+
+---
+
+## 3. Listener Provider & Resolution
+The logic for finding who cares about an event.
+
+- **Priority Handling**: Listeners are sorted by priority (higher integer runs first).
+- **Inheritance Resolution**:
+ - Automatically resolves listeners for the event class, its parents, and its interfaces.
+ - Must include a performance toggle to disable interface scanning.
+- **Wildcard Support**:
+ - Support dot-notation wildcards (e.g., `user.*`, `*.created`).
+ - Pattern matching converted to optimized regex with internal caching.
+- **Lazy Loading**: Integration with a PSR-11 container to instantiate listeners only when they are about to be called.
+
+---
+
+## 4. Discovery & Registration
+Mechanisms to register listeners without manual configuration.
+
+- **Attribute Discovery**:
+ - Support `#[AsEventListener(event: string, priority: int)]`.
+ - Support `#[AsSubscriber]`.
+- **Listener Discovery Service**:
+ - Scans directories for classes with Beacon attributes.
+ - Integrates with PSR-16 cache to avoid scanning on every request.
+- **Manual Registration**:
+ - `addListener(string $event, callable $listener, int $priority = 0)`
+ - `addSubscriber(SubscriberInterface $subscriber)`
+
+---
+
+## 5. Event Objects & Context
+Enhancing the standard event pattern.
+
+- **Event Envelope**:
+ - A wrapper to carry immutable metadata (e.g., `request_id`, `user_id`, `timestamp`).
+ - Uses a `ContextMap` for type-safe metadata access.
+- **Stoppable Trait**: Provide `CanStopPropagation` trait for easy implementation of `StoppableEventInterface`.
+
+---
+
+## 6. Testing Utilities
+- **EventCollector**: A listener that gathers all dispatched events for assertions.
+- **PHPUnit Assertions**: Traits for `assertEventDispatched`, `assertEventNotDispatched`, and `assertListenerCalled`.
diff --git a/composer.json b/composer.json
new file mode 100644
index 0000000..8eea3c5
--- /dev/null
+++ b/composer.json
@@ -0,0 +1,39 @@
+{
+ "name": "getphred/beacon",
+ "description": "Enterprise-grade PSR-14 event management engine for the Phred Framework.",
+ "license": "MIT",
+ "type": "library",
+ "authors": [
+ {
+ "name": "Phred Team",
+ "email": "team@getphred.com"
+ }
+ ],
+ "require": {
+ "php": "^8.2",
+ "getphred/beacon-contracts": "dev-master",
+ "psr/simple-cache": "^3.0",
+ "psr/container": "^2.0"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^10.0"
+ },
+ "autoload": {
+ "psr-4": {
+ "Phred\\Beacon\\": "src/"
+ }
+ },
+ "autoload-dev": {
+ "psr-4": {
+ "Phred\\Beacon\\Tests\\": "tests/"
+ }
+ },
+ "repositories": [
+ {
+ "type": "path",
+ "url": "../BeaconContracts"
+ }
+ ],
+ "minimum-stability": "dev",
+ "prefer-stable": true
+}
diff --git a/phpunit.xml b/phpunit.xml
new file mode 100644
index 0000000..e71ab58
--- /dev/null
+++ b/phpunit.xml
@@ -0,0 +1,20 @@
+
+
+
+
+ tests/Unit
+
+
+ tests/Integration
+
+
+
+
+ src
+
+
+
diff --git a/src/CanStopPropagation.php b/src/CanStopPropagation.php
new file mode 100644
index 0000000..9a4c0d2
--- /dev/null
+++ b/src/CanStopPropagation.php
@@ -0,0 +1,39 @@
+isPropagationStopped = true;
+ }
+
+ /**
+ * Returns true if propagation was stopped.
+ *
+ * @return bool
+ */
+ public function isPropagationStopped(): bool
+ {
+ return $this->isPropagationStopped;
+ }
+}
diff --git a/src/EventDispatcher.php b/src/EventDispatcher.php
new file mode 100644
index 0000000..1b8a153
--- /dev/null
+++ b/src/EventDispatcher.php
@@ -0,0 +1,97 @@
+pipeline = $pipeline ?? new MiddlewarePipeline();
+ }
+
+ /**
+ * Dispatches an event to all relevant listeners.
+ *
+ * @template T of object
+ * @param T $event The object to process.
+ * @return T The event object after listeners have processed it.
+ */
+ public function dispatch(object $event): object
+ {
+ return $this->pipeline->execute($event, function(object $event): object {
+ $listeners = $this->listenerProvider->getListenersForEvent($event);
+
+ foreach ($listeners as $listener) {
+ // Check if propagation has been stopped (PSR-14)
+ if ($event instanceof \Psr\EventDispatcher\StoppableEventInterface && $event->isPropagationStopped()) {
+ break;
+ }
+
+ try {
+ // Call the listener
+ $listener($event);
+ } catch (Throwable $e) {
+ if ($this->resiliencyMode === ResiliencyMode::FAIL_FAST) {
+ throw $e;
+ }
+
+ // Reporting the failure by dispatching an internal event
+ $this->dispatch(new DispatchFailed($e, $listener, $event));
+ }
+ }
+
+ return $event;
+ });
+ }
+
+ /**
+ * Adds a global middleware to the dispatcher.
+ *
+ * @param DispatchMiddlewareInterface $middleware
+ * @return void
+ */
+ public function addMiddleware(DispatchMiddlewareInterface $middleware): void
+ {
+ $this->pipeline->add($middleware);
+ }
+
+ /**
+ * Sets the resiliency mode for the dispatcher.
+ *
+ * @param ResiliencyMode $mode
+ * @return void
+ */
+ public function setResiliencyMode(ResiliencyMode $mode): void
+ {
+ $this->resiliencyMode = $mode;
+ }
+}
diff --git a/src/ListenerProvider.php b/src/ListenerProvider.php
new file mode 100644
index 0000000..d0f0f21
--- /dev/null
+++ b/src/ListenerProvider.php
@@ -0,0 +1,212 @@
+> List of listeners.
+ */
+ private array $listeners = [];
+
+ /**
+ * @var bool Whether to resolve listeners for interfaces.
+ */
+ private bool $resolveInterfaces = true;
+
+ /**
+ * @var array Cache for compiled wildcard regexes.
+ */
+ private array $regexCache = [];
+
+ /**
+ * @var ContainerInterface|null Optional container for lazy-loading.
+ */
+ private ?ContainerInterface $container = null;
+
+ /**
+ * @param ContainerInterface|null $container
+ */
+ public function __construct(?ContainerInterface $container = null)
+ {
+ $this->container = $container;
+ }
+
+ /**
+ * Resolves listeners for the given event.
+ *
+ * @param object $event The event object.
+ * @return iterable An iterable of callables that should handle the event.
+ */
+ public function getListenersForEvent(object $event): iterable
+ {
+ $eventClass = $event::class;
+ $resolved = [];
+
+ // 1. Resolve by exact class, parents, and interfaces
+ foreach ($this->getClassesToResolve($eventClass) as $class) {
+ if (isset($this->listeners[$class])) {
+ foreach ($this->listeners[$class] as $listenerData) {
+ $resolved[] = $listenerData;
+ }
+ }
+ }
+
+ // 2. Resolve by wildcards
+ foreach ($this->listeners as $pattern => $patternListeners) {
+ if ($this->isWildcard($pattern) && $this->matchesWildcard($pattern, $eventClass)) {
+ foreach ($patternListeners as $listenerData) {
+ $resolved[] = $listenerData;
+ }
+ }
+ }
+
+ // 3. Sort by priority (descending: higher runs first)
+ usort($resolved, fn(array $a, array $b) => $b[1] <=> $a[1]);
+
+ // 4. Transform to callables (handle lazy-loading and instantiation)
+ return array_map(function(array $data) {
+ $listener = $data[0];
+
+ if (is_callable($listener)) {
+ return $listener;
+ }
+
+ // Handle [ServiceId, Method] or ServiceId (with __invoke)
+ if (is_array($listener) && count($listener) === 2) {
+ $service = $this->container?->get($listener[0]) ?? (class_exists($listener[0]) ? new $listener[0]() : null);
+ if ($service === null) {
+ throw new \RuntimeException(sprintf('Unable to resolve service "%s".', $listener[0]));
+ }
+ return [$service, $listener[1]];
+ }
+
+ if (is_string($listener)) {
+ $service = $this->container?->get($listener) ?? (class_exists($listener) ? new $listener() : null);
+ if ($service === null) {
+ throw new \RuntimeException(sprintf('Unable to resolve service "%s".', $listener));
+ }
+ return is_callable($service) ? $service : [$service, '__invoke'];
+ }
+
+ throw new \RuntimeException('Unable to resolve listener to a callable.');
+ }, $resolved);
+ }
+
+ /**
+ * Manually registers a listener for an event class or pattern.
+ *
+ * @param string $eventClassOrPattern The FQCN or a wildcard pattern (e.g., 'App.User.*').
+ * @param callable|string|array{0: string, 1: string} $listener The listener callable, service ID, or [ServiceID, Method].
+ * @param int $priority The priority (higher runs first).
+ * @return void
+ */
+ public function addListener(string $eventClassOrPattern, callable|string|array $listener, int $priority = 0): void
+ {
+ $this->listeners[$eventClassOrPattern][] = [$listener, $priority];
+ }
+
+ /**
+ * Registers a subscriber.
+ *
+ * @param SubscriberInterface|string $subscriber The subscriber instance or service ID.
+ * @return void
+ */
+ public function addSubscriber(SubscriberInterface|string $subscriber): void
+ {
+ if (is_string($subscriber)) {
+ /** @var SubscriberInterface $instance */
+ $instance = $this->container?->get($subscriber) ?? (class_exists($subscriber) ? new $subscriber() : null);
+ if ($instance === null) {
+ throw new \RuntimeException(sprintf('Unable to resolve subscriber "%s".', $subscriber));
+ }
+ $serviceId = $subscriber;
+ } else {
+ $instance = $subscriber;
+ $serviceId = null;
+ }
+
+ foreach ($instance::getSubscribedEvents() as $event => $params) {
+ if (is_string($params)) {
+ $method = $params;
+ $priority = 0;
+ } else {
+ $method = $params[0];
+ $priority = $params[1] ?? 0;
+ }
+
+ $listener = $serviceId !== null ? [$serviceId, $method] : [$instance, $method];
+ $this->addListener($event, $listener, $priority);
+ }
+ }
+
+ /**
+ * Enables or disables interface resolution for performance.
+ *
+ * @param bool $shouldResolve
+ * @return void
+ */
+ public function setResolveInterfaces(bool $shouldResolve): void
+ {
+ $this->resolveInterfaces = $shouldResolve;
+ }
+
+ /**
+ * Gets all classes and interfaces to resolve for the event.
+ *
+ * @param string $eventClass
+ * @return array
+ */
+ private function getClassesToResolve(string $eventClass): array
+ {
+ $classes = class_parents($eventClass);
+ $classes[] = $eventClass;
+
+ if ($this->resolveInterfaces) {
+ $classes = array_merge($classes, class_implements($eventClass));
+ }
+
+ return array_unique($classes);
+ }
+
+ /**
+ * Checks if a string is a wildcard pattern.
+ *
+ * @param string $pattern
+ * @return bool
+ */
+ private function isWildcard(string $pattern): bool
+ {
+ return str_contains($pattern, '*');
+ }
+
+ /**
+ * Checks if an event class matches a wildcard pattern.
+ *
+ * @param string $pattern
+ * @param string $eventClass
+ * @return bool
+ */
+ private function matchesWildcard(string $pattern, string $eventClass): bool
+ {
+ if (!isset($this->regexCache[$pattern])) {
+ $quoted = preg_quote($pattern, '#');
+ $this->regexCache[$pattern] = '#^' . str_replace('\\*', '.*', $quoted) . '$#i';
+ }
+
+ return (bool) preg_match($this->regexCache[$pattern], $eventClass);
+ }
+}
diff --git a/tests/Unit/CoreDispatcherTest.php b/tests/Unit/CoreDispatcherTest.php
new file mode 100644
index 0000000..8613cad
--- /dev/null
+++ b/tests/Unit/CoreDispatcherTest.php
@@ -0,0 +1,67 @@
+addListener($event::class, function($e) {
+ $e->count++;
+ });
+
+ $dispatchedEvent = $dispatcher->dispatch($event);
+
+ $this->assertSame($event, $dispatchedEvent);
+ $this->assertEquals(1, $event->count);
+ }
+
+ public function test_it_respects_stoppable_events(): void
+ {
+ $provider = new ListenerProvider();
+ $dispatcher = new EventDispatcher($provider);
+
+ $event = new class implements StoppableEventInterface {
+ use CanStopPropagation;
+ public int $count = 0;
+ };
+
+ $provider->addListener($event::class, function($e) {
+ $e->count++;
+ $e->stopPropagation();
+ });
+
+ $provider->addListener($event::class, function($e) {
+ $e->count++; // Should not run
+ });
+
+ $dispatcher->dispatch($event);
+
+ $this->assertEquals(1, $event->count);
+ $this->assertTrue($event->isPropagationStopped());
+ }
+
+ public function test_it_returns_the_event_object(): void
+ {
+ $provider = new ListenerProvider();
+ $dispatcher = new EventDispatcher($provider);
+ $event = new \stdClass();
+
+ $result = $dispatcher->dispatch($event);
+
+ $this->assertSame($event, $result);
+ }
+}