super-kernel/annotator scans package-scoped class maps supplied through
SuperKernel\Contract\PackageManagerInterface, reflects eligible class-like symbols through
SuperKernel\Contract\ReflectorInterface, and collects native PHP Attributes declared on classes, methods, and
properties. The vendor directory and reflection implementations are injected through shared Contract interfaces.
Isolated callback execution is injected through SuperKernel\ScanIsolation\Contract\ScanProcessorInterface, provided by the
direct super-kernel/scan-isolation dependency. This package does not select a concrete scan processor.
Production classes are organized by their role:
| Namespace | Classes | Responsibility |
|---|---|---|
SuperKernel\Annotator\Scan |
AnnotationScanner, ClassAnnotationScanner |
Package traversal and native class/member Attribute extraction. |
SuperKernel\Annotator\Scan |
AnnotationScanWorker, AnnotationScanSession |
Isolated callback entry and the scan-to-publication lifecycle. |
SuperKernel\Annotator\Cache |
AnnotationCache, SerializedPackageAnnotations, SerializedPackageReferences |
Cache files, serialized records, and restoration validation. |
SuperKernel\Annotator |
AnnotationCollector |
Exact and assignable method discovery through the shared collector contract. |
ClassAnnotationScanner remains the shared extraction entry for DI bootstrap. AnnotationScanWorker remains a
provider callback implementation; its placement under Scan does not change who owns process execution.
Model continues to hold the collected metadata values, and Provider continues to own the DI factory.
Persisted class names and object layouts must match the installed code. Rebuild annotation snapshots through a normal scan before restore-only deployment after model changes; scanning preserves unrelated files.
Invoking AnnotationCollectorProvider performs the complete lifecycle synchronously:
- require the supplied Scan Processor's
support()probe to returntrue; - create a static anonymous callback over the supplied Package Manager, Reflector, and vendor directory;
- execute that callback through the supplied Scan Processor;
- let the completed child-side scan session publish its data from its destructor; and
- restore the published package records into an immutable
AnnotationCollectorin the calling process.
Cache operations use .cache/ beneath the injected VendorDirInterface directory and create it on demand.
An enclosing DI bootstrap supplies PhaseScanProcessor: its false phase runs the worker in the existing isolated
process, and its true phase restores the published records. Native extraction is shared with DI through
Scan\ClassAnnotationScanner, so the finite bootstrap graph and complete package scans use the same Attribute semantics.
use SuperKernel\Annotator\Provider\AnnotationCollectorProvider;
/** @var SuperKernel\Contract\PackageManagerInterface $packages */
/** @var SuperKernel\ScanIsolation\Contract\ScanProcessorInterface $processor */
/** @var SuperKernel\Contract\ReflectorInterface $reflector */
/** @var SuperKernel\Contract\VendorDirInterface $vendorDir */
$provider = new AnnotationCollectorProvider();
$collector = $provider($packages, $processor, $reflector, $vendorDir);
$controllers = $collector->getClassesWithAttribute(App\Attribute\Controller::class);
$exceptionHandlers = $collector->getClassesWithAttributeAssignableTo(
SuperKernel\ExceptionDispatcher\Attribute\ExceptionHandler::class,
);
$classAttributes = $collector->getClassAttributes(App\Controller\HomeController::class);
$methodAttributes = $collector->getMethodAttributes(App\Controller\HomeController::class, 'index');
$propertyAttributes = $collector->getPropertyAttributes(App\Service\Clock::class, 'timezone');getClassesWithAttribute() returns an insertion-ordered map from every class carrying the exact Attribute name to only
the matching AttributeMetadata entries. Consumers performing discovery should use this indexed lookup instead of
repeatedly traversing every package and every unrelated class Attribute. Every entry contains an already constructed,
trusted instance of the exact queried Attribute. Native target and repeatability errors fail during construction and
therefore never appear in a published record; valid repeatable declarations retain their source order.
getClassesWithAttributeAssignableTo() returns the same shape but includes declarations whose concrete Attribute
class extends the requested class or implements the requested interface. Assignability is captured during isolated
scanning and restored from metadata, so consumers do not need to repeat reflective family discovery in the application
process. Exact queries remain exact and are unaffected by this additional index.
AnnotationCollectorInterface includes getMethodsWithAttribute() and
getMethodsWithAttributeAssignableTo(). Both return maps keyed by declaring class and exact method name, with an
ordered non-empty list of matching trusted metadata for each method. The concrete collector builds both indexes from
package records during construction or restoration. Queries do not rescan, reconstruct Attributes, or load targets.
The first package owning a class's method records wins before filtering, including an empty first map. Inherited
methods remain under their declaring class; repeatable declarations retain order and instance identity.
use SuperKernel\Attribute\Provision;
use SuperKernel\Contract\AnnotationCollectorInterface;
/** @var AnnotationCollectorInterface $collector */
$methods = $collector->getMethodsWithAttribute(Provision::class);Consumers call the injected collector directly and retain their own target validation, return-type interpretation, service definitions, and registration rules. Method indexes are derived from restored records, not serialized separately.
The provider throws AnnotationScanException before allocating cache files when the injected processor is unsupported.
Otherwise, it returns only after the child exits and every cache record has been restored and validated. Supplying a
NullScanProcessor explicitly skips the callback and restores existing data only; missing, stale, or malformed data is
rejected. If source inspection, reflection, Attribute construction, or serializable snapshot creation fails for a
class-like symbol, the worker records a structured diagnostic, discards all partial metadata for that class, and
continues with the remaining class map. The published collector contains the successful subset and every skipped-class
diagnostic. Successful worker scans do not print warnings; DI reports from its restored collector as described below. Before reflection, source files without a native PHP
Attribute token are skipped, so loading unrelated optional integrations cannot terminate collection because one of
their runtime dependencies is absent. A missing per-package class map remains a structural scan failure.
AnnotationCollector and Model\PackageAnnotations additionally implement
SuperKernel\Contract\ScanDiagnosticsInterface::getDiagnostics(): array. This capability is not separately bound in DI.
A package exposes its subset; the collector combines diagnostic-capable packages in scan order. Independent package implementations
contribute diagnostics only when they implement that capability.
Every row has exactly these fields:
| Field | Value |
|---|---|
sequence |
Positive integer, globally contiguous from 1 through N for one complete scan. |
package |
Normalized non-empty Composer package name, equal to the containing record's owner. |
class |
Non-empty captured class-name string; not an autoload request. |
exception_type |
Non-empty concrete throwable-name string; anonymous names may contain NUL. |
message |
Original string bytes, including empty messages, control bytes, and non-UTF-8 content. |
No Throwable, trace, resource, timestamp, logger, or output-state flag is serialized. Diagnostic strings can contain paths or application data; neither caching nor structured logging redacts them. Retrieval never reflects or loads these captured names.
use SuperKernel\Annotator\AnnotationCollector;
use SuperKernel\Annotator\Model\PackageAnnotations;
use SuperKernel\Contract\ScanDiagnosticsInterface;
// Hand-built metadata without skipped-class diagnostics.
$record = new PackageAnnotations('app/example', null, [], [], []);
// Optional sixth argument carries the package's diagnostics.
$package = new PackageAnnotations('app/example', null, [], [], [], [
[
'sequence' => 1,
'package' => 'app/example',
'class' => 'App\\Unavailable',
'exception_type' => 'Error',
'message' => 'Optional parent class is unavailable',
],
]);
$collector = new AnnotationCollector([$package]);
if ($collector instanceof ScanDiagnosticsInterface) {
$diagnostics = $collector->getDiagnostics();
}The normal DI bootstrap publishes and reads the complete collector in the scanning child, constructs a container
from it, and resolves the application's actual StdoutLoggerInterface. It reports one WARNING summary and one INFO
per skipped class. The normal parent does not resolve a logger for these diagnostics. A restore-only Phar has no child:
it reports the cached build diagnostics in its current process each time a new root is created. Future binary hosts
must adapt their bootstrap separately; this change does not implement binary execution.
With the default Logger INFO threshold, summary and details are visible. For example, excluding the logger timestamp:
[WARNING] Annotation scan skipped 1 class.
[INFO] Annotation scan skipped App\Unavailable: Error: Optional parent class is unavailable
Plural summary: Annotation scan skipped %d classes. Zero skipped classes produce no records and do not resolve a
logger. INFO messages contain all three diagnostic fields because ConsoleFormatter does not render context. The
message's display copy escapes ASCII controls (\r, \n, \t, \x00, \x1B, \x7F); context and caches retain
raw bytes. Ordinary namespace backslashes remain unchanged. See DI reporting
for context keys, failure behavior, and process streams, and Logger configuration
for a complete application Provider example.
AnnotationScanner accepts Package Manager, Reflector, and an optional diagnosticReporter Closure. The reporter
receives one structured diagnostic row for each skipped class. Without a reporter, DiagnosticOutput immediately
writes the yellow warning to STDERR. Supplying a reporter gives the caller responsibility for output; rows are still
retained in the resulting package records.
use SuperKernel\Annotator\Scan\AnnotationScanner;
$scanner = new AnnotationScanner($packages, $reflector);
$records = $scanner->scan();
$diagnostics = [];
$scanner = new AnnotationScanner(
$packages,
$reflector,
diagnosticReporter: static function (array $diagnostic) use (&$diagnostics): void {
$diagnostics[] = $diagnostic;
},
);
$records = $scanner->scan();Diagnostic\DiagnosticOutput::formatWarning(array $diagnostic): string returns the emergency ANSI warning without its
terminal newline; writeToStderr(array $diagnostics): void writes those lines in order and throws if writing fails.
These stateless emergency helpers have no DI binding. A custom reporter owns output for its rows.
Direct AnnotationCollectorProvider users receive persisted
diagnostics without successful-scan output and must explicitly consume this capability or use DI's normal bootstrap.
Package records now use schema 4. Diagnostics share each package's existing atomic .ser publication; the
packages.ser reference index stays version-free. Write and read validate exact row keys and scalar types, package
ownership, ordered positive sequences, globally unique contiguous sequences, no repeated failed class within a package,
and no partial metadata for that class. The outer allow-list stays exactly SerializedPackageAnnotations,
PackageAnnotations, and AttributeMetadata; diagnostic names never expand it. Nested Attribute snapshot validation
is unchanged. Invalid rows reject restoration rather than being dropped.
Schema 3 is rejected even if no warning was previously observed. Rebuild by a normal source scan before packaging or restoring. Do not share a cache between old and new runtime versions; rebuild matching artifacts when rolling back. Phar deployments must carry compatible caches and paths, and replay their build diagnostics on each startup without rescanning or clearing them. Existing per-file atomicity is not a transaction across packages; serialize initialization.
A structural scan failure flushes already collected diagnostics in the yellow STDERR emergency format before rethrowing the original failure. Publication failure is covered by the same worker guard, including actual Session destruction. Incomplete results are invalidated; cleanup or emergency-output failures cannot replace the primary exception. A later package restoration failure flushes only previously validated diagnostics before rethrowing. No logger is created on these failed scan/read paths. Lost writes to a broken STDERR cannot be guaranteed, but the operation does not become successful because fallback failed.
SuperKernel\Annotator\Provider\AnnotationCollectorProvider exposes the collector through DI
FactoryProvider metadata:
#[SuperKernel\Attribute\FactoryProvider(
SuperKernel\Contract\AnnotationCollectorInterface::class,
)]The provider requires a Package Manager, Reflector, and vendor directory through their SuperKernel\Contract
interfaces, plus a SuperKernel\ScanIsolation\Contract\ScanProcessorInterface. Selecting and constructing those
implementations is the responsibility of the DI integration. The same provider can be invoked manually, as shown
above, when those four dependencies are available. The fourth argument is VendorDirInterface.
The provider passes only a static anonymous Closure to the Scan Processor. Annotator's scan worker remains an
internal callback implementation, while any object used to wrap and terminate that callback belongs entirely to Scan
Isolation and is not exposed through this package's constructor or public contracts.
Each __invoke() call performs a new complete synchronous scan and restore lifecycle and returns that invocation's
collector. The provider does not cache its product. Callers that need reuse must retain the returned collector, while
a DI container may retain the mapped entry according to its own lifecycle. Hosts must continue to serialize competing
invocations.
The FactoryProvider Attribute comes from the implementation-independent super-kernel/attribute package rather
than the official DI implementation. Annotator depends directly on Scan Isolation for its processor interface and has
no production dependency on Composer, Reflector, or DI. Executing this provider requires an integration that
supplies the declared interfaces.
Model\AttributeMetadata<T> implements SuperKernel\Contract\AttributeMetadataInterface<T>, and
Model\PackageAnnotations implements SuperKernel\Contract\PackageAnnotationsInterface. Shared collector results
use these read interfaces, allowing custom collectors to supply independent metadata implementations. Business
consumers depend on the shared interfaces while retaining their own registration and validation rules.
AnnotationCollector accepts PackageAnnotationsInterface values, stores shared metadata interfaces in its indexes,
and declares the same ?PackageAnnotationsInterface return type as the shared collector contract. All array lookup
types are inherited from the shared interfaces. It retains the supplied package and metadata instances directly,
including independent implementations. Scanning and persistence continue to own the concrete Annotator models.
Arbitrary shared-interface implementations do not become cache-eligible. Serialization remains an Annotator implementation responsibility. Schema 3 records, including those predating these interfaces, now require rebuilding because they contain no skipped-class diagnostics.
Each AttributeMetadata<T> contains a trusted T instance plus the concrete Attribute class name and captured
assignable type names. attribute() returns that instance directly, so consumers do not reconstruct it or repeat
constructor validation. The instance is also captured as a serialized snapshot when metadata is created; an Attribute
whose object graph cannot be serialized and restored, or whose snapshot traversal exceeds 64 nested levels, is
excluded through the same per-class warning path.
Exact queries guarantee an instance of the requested class. Assignable queries guarantee an instance compatible with
the requested base class or interface. Consumers remain responsible for relationships in their own domain, such as a
marked command extending Symfony Command or a mapped middleware implementing PSR-15, but should not revalidate the
collector's query match or reconstruct the Attribute.
Only members declared by the reflected class are stored. Inherited methods and properties are collected with their own declaring classes instead of being duplicated under every descendant.
The injected VendorDirInterface determines the cache root, including custom vendor directories:
<vendor>/.cache/
├── packages.ser
└── annotations/
├── super-kernel/composer.ser
├── super-kernel/reflector.ser
└── vendor/package.ser
Each package file is the raw serialization of one validated package annotation record. packages.ser is published
last and is the raw serialized stub mapping every current package name to the exact nullable reference() value from
the injected package metadata. The outer record uses a narrow Annotator-model allow-list. Each metadata value keeps its
Attribute snapshot as a nested serialized payload, preflights it with ordinary object classes disabled, derives its
exact object-class set, and then restores only those classes. Parent restoration therefore loads Attribute value
classes, including enum classes required by PHP serialization, while annotated application classes remain outside the
cache object graph. These files are trusted local runtime artifacts and must not be populated by an untrusted writer.
The files are disposable derived data. The provider rescans on every invocation; package references validate that the restored files belong to the scan that just completed. The reference stub has no schema version: restoration validates its structure and exact package references. A failed scan invalidates the previous serialized results and cannot publish an earlier result from the same session. Deserialization or validation failures reject the stored data; the next normal scan rebuilds it. Incompatible reference stubs require a normal rescan. Each file is published atomically through a temporary file in the same directory. The host must still serialize competing provider invocations; the cache provides no process lock.
Invalidation removes only .cache/packages.ser and .cache/annotations/; Composer's sibling .cache/classes/
directory is preserved. Only the documented cache paths are read or written. A normal
scan populates this layout; restore-only execution requires those files to have been prepared beforehand.
composer84 install
composer84 validate --strict
composer84 test
composer84 format:check
composer84 format