# Hypervel Components Agent Guide **External contributions:** When preparing an issue or pull request as an external contributor, read `src/docs/contributions.md` in full and follow its requirements. This does not apply to maintainer-directed work. **Agent workflow:** This guide assumes two agents: an implementer and an independent peer providing second opinions and code review. Without a peer, replace each required second opinion with a concise, plain-language explanation of the issue and recommended solution to the owner, and wait for approval. Replace peer code review with a full self-review after implementation. ## Background Hypervel is a standalone Laravel-style Swoole framework. The public API should stay close to Laravel wherever possible, while the internals are adapted for long-lived Swoole workers, coroutine safety, and high performance. Laravel is the main API reference. Hyperf is a historical and architectural reference for some lower-level Swoole/coroutine packages, but Hypervel code should follow current Hypervel patterns rather than copying Hyperf structure mechanically. Most work in this repo today is framework bug fixes and enhancements, or porting Laravel packages and updates. `docs/ai/porting-hyperf.md` applies only to the rare Hyperf package or update port. This file is intentionally detailed because agents trained on Laravel will otherwise assume Laravel's request lifecycle and miss Hypervel's Swoole/coroutine constraints. ## Mental Model When working on Hypervel, start from this frame: - **Development monorepo:** Applications install individual subtree-split packages, never `hypervel/components`. Root `composer.json` and `package.json` metadata is development-only. Determine installed-application behavior from the relevant package manifests and the application's dependencies and discovery settings. - Hypervel is Laravel-shaped at the API level. - **Database connections are driver-neutral.** `ConnectionInterface` and `Connection` support custom drivers registered through `DB::extend()`, including non-PDO transports. - Hypervel is not request-per-process PHP — workers are long-lived, and PHP state does not disappear after each request. - Singletons, static properties, manager registries, callbacks, config, and cached metadata can persist for the worker lifetime. - Per-request state must live in coroutine-scoped storage (CoroutineContext), not process-global state. - Laravel source is the default parity reference, but Laravel internals often assume per-request bootstrap and are not optimized to take advantage of static caching of immutable state. - Hyperf source can be useful for Swoole/coroutine behavior, but Hyperf container/config/listener patterns are not the target architecture. - Laravel's rate limiter lives under `Illuminate\Cache`; Hypervel's canonical implementation is the dedicated `hypervel/rate-limiter` package under `Hypervel\RateLimiter`. It uses typed policies and dedicated atomic stores and has no `Hypervel\Cache` alias or primitive counter API. Use this package directly when porting rate-limited Laravel code. ## Repository Layout and Commands Always run framework commands from the repository root. This is the directory that contains this `AGENTS.md`, the root `composer.json`, `phpunit.xml.dist`, `phpstan.neon`, `src/`, and `tests/`. Key paths: | Path | Description | |------|-------------| | `src/docs/` | Hypervel documentation and the source for the `hypervel/docs` package. | | `src/framework/` | Composer metadata for `hypervel/framework`, the metapackage that installs the standard framework components. Published to its own read-only split repository. | | `src/testbench/` | Hypervel's testbench package (port of `orchestra/testbench`). Contains `TestCase`, attributes (`WithConfig`, `WithMigration`), and bootstrap logic. Part of the monorepo, not a vendor dependency. | | `src/testbench/hypervel/` | Committed Hypervel app skeleton. On bootstrap, testbench clones this to a disposable temp directory (`/tmp/hypervel-components-testbench-{token}-{pid}/`) and points `BASE_PATH` at the clone — tests that write files under `BASE_PATH` (generated providers, migrations, fixtures, etc.) hit the temp copy, not this committed path. The clone is deleted on shutdown and stale copies from crashed runs are cleaned up. Testbench also exports `TESTBENCH_BASE_PATH` so subprocesses can locate the active runtime. | | `src/testbench/workbench/` | Committed shared test fixtures (NOT cloned). Subdirs are psr-4-mapped from the monorepo root as `Workbench\App\*`, `Workbench\Database\Factories\*`, `Workbench\Database\Seeders\*` so multiple tests can reuse the same models/factories/seeders without redefining them. Not the runtime app — that's the disposable clone of `src/testbench/hypervel/`. | | `skeletons/` | Application skeletons and starter kits, each published to its own read-only split repository. `hypervel/` is the base application skeleton; `react-starter-kit/` is the React starter kit. | | `docs/ai/` | Supplementary agent guides, including `porting-hyperf.md` (Hyperf conversion mechanics). | | `docs/todo.md` | Tracked gaps and improvements worth doing. | ### Running tests **Always use `composer test:parallel`** to run the full components test suite. This runs raw ParaTest with Hypervel's parallel testing flag supplied by `phpunit.xml.dist`. **Always use `composer test:testbench`** after Testbench changes. This runs the scoped Testbench package-mode contract suite through the real `package:test` command. ParaTest defaults to the machine CPU count when no process count is specified. Redis integration runs need `REDIS_TEST_DB_MIN` / `REDIS_TEST_DB_MAX` to cover the chosen worker count, or an explicit `--processes` / `-p` value that fits the configured range. `TEST_TOKEN` is runner-owned worker identity. Tests must use the assigned value unless they explicitly exercise token-dependent behavior; such tests must restore every environment source they mutate or run in an isolated subprocess. To run a single test class: `./vendor/bin/phpunit --no-progress path/to/TestClass.php`. ## Change Workflow ### Classify the change Before editing, identify the work as one of: - A framework bug fix. - A framework enhancement. - An update from an existing package's upstream. - A new package port. This determines which upstream source and tests to compare. Incremental updates from an existing upstream follow the workflow below together with the applicable Porting Policy and rules. New package ports follow the full workflow under Porting Packages. Bug fixes and enhancements follow the workflows below. In every case, read the package README for the upstream reference and the relevant Hypervel source and tests before editing. ### Incremental upstream updates When bringing an existing upstream feature, fix, or API change into Hypervel: 1. Find the originating implementation pull request and any corresponding documentation pull request. Use them to understand the reason for the change and identify the complete set of files changed when it was introduced. 2. Inspect every file changed by those pull requests, including source, contracts, tests, fixtures, configuration, package metadata, and documentation. Search the current upstream branch for the added symbols as well, since later changes may have introduced additional consumers or coverage. 3. Treat the historical pull-request diffs as discovery and history only. Port the actual source, tests, and documentation from the current checked-out upstream default or development branch. Follow-up fixes and documentation improvements may have changed the final implementation or coverage. 4. Compare that current upstream surface with the Hypervel implementation and apply the approved Hypervel adaptations under Porting Packages. If the upstream feature has no user-facing documentation, add proportionate Hypervel documentation at its natural public surface. ### Audit changes during modification and code review Every audit must explicitly check for overengineering, Laravel-style ergonomics, and avoidable performance or scalability costs, especially repeated hot-path work, excessive database or network round trips (e.g. Redis), inefficient query or index design, unnecessary allocation or serialization, unbounded work, and worker-lifetime memory growth. Modifying code is an implicit assessment of it. Whenever you edit a method, move code, copy a file, or port from upstream, check what you touch for: - Lifetime classification for any container-buildable class you add, port, or whose mutable state you change — see Container. - Hardcoded values that should be derived (namespaces, defaults) - Defensive code that masks bugs - Conventions that diverge from how the framework actually does it - Deprecated APIs or dated patterns - Issues in the code right next to what you're changing Anything found follows When to Stop and Report — "the task didn't ask me to fix that" and "I copied it verbatim" are not reasons to stay silent. The trigger is modification or code review; files read only for context don't need a line-by-line audit. ### Framework bug fixes 1. Reproduce the bug with the smallest useful test. 2. Trace the exact failing code path and identify the root cause before changing source. 3. Compare the matching Laravel or package-upstream behavior when it defines the contract. 4. Reach consensus with your peer agent on the root cause and fix through a second-opinion loop before implementing. 5. Fix the underlying defect — never add a workaround for incorrect framework code. 6. Add a regression test for the bug and run that test file immediately. 7. Check types, API parity, coroutine isolation, and worker-lifetime state around the changed code. 8. Complete the verification workflow below. ### Framework enhancements 1. Read the related Hypervel APIs and tests. Check Laravel or the package upstream for an established public API and behavior. 2. Decide which state is local, coroutine-scoped, or worker-scoped before writing code — see Coroutine and Worker-Lifetime State. 3. Preserve Laravel API parity by default. If parity conflicts with Hypervel's architecture, preserves a verified defect or deprecated upstream API, or would require worse code or a workaround, reach consensus with your peer agent on the cleanest design through a second-opinion loop before planning or editing. Never make Hypervel code worse merely to preserve parity. 4. Test the public behavior, failure paths, coroutine isolation, and cleanup the feature needs. 5. Complete the verification workflow below. ### Verification **Run verification commands sequentially** — Never overlap formatting, static analysis or tests. Wait for each command to finish before starting another. A tool’s built-in parallelism, such as ParaTest workers, is fine. **During implementation**, run each new or changed test file immediately with `./vendor/bin/phpunit --no-progress path/to/TestClass.php`. Do not routinely run formatting or PHPStan during edits. Run targeted PHPStan earlier only when investigating a typing issue or when its feedback is needed to guide implementation. **At the end of a coherent implementation slice**, run `composer analyse` and the affected tests, in that order. Use PHPUnit for one test file or `composer test:parallel -- --processes=4 tests/PackageName` for multiple files. Use `--filter` when needed to narrow the selection. Run `composer lint:fix` before a scheduled code review or commit checkpoint, not after individual changes within a batch. Run the full test suite only when the change has framework-wide impact. In that case, use `composer fix`, which includes formatting, analysis and all test suites; do not run those checks separately first. For focused changes, include separate Testbench or dogfood checks when the changed behavior affects those workflows. After a failure or review correction, fix the cause, repeat only checks affected by the correction, and finish any outstanding verification. Do not restart the entire sequence merely because another review round occurred. ## Development Conventions The Working rules and the Avoid overengineering rules apply to all work in this repo. The Code conventions apply to newly written code. Laravel package ports preserve upstream naming, structure, and style except for the approved adaptations under Porting Packages. Hyperf ports follow `docs/ai/porting-hyperf.md`. Apply approved adaptations when porting or modifying code. Do not expand a change solely to apply style or modernization conventions elsewhere, unless that cleanup was explicitly requested. ### Working rules - **Never use subagents without explicit user consent** — Do not spawn or delegate work to subagents unless the user explicitly requests or approves their use. - **Avoid bulk modification tools** — tools like `sed` and `replace_all` often have unwanted side effects. Never use bulk modification tools without explicit user approval; prefer manual edits. When approved, run them in multiple passes that each target long, exact, case-sensitive strings to avoid accidental changes. - **One file at a time** — never work on multiple files simultaneously. This governs manual editing; package-manager and formatter runs may touch multiple files. - **Never use Write to overwrite files** — always use Edit for targeted updates. - **Always use `cp` to copy files and `mv` to move/rename** — never read → write new version → delete old version. - **Copy before splitting** — When a new file or class is primarily extracted from existing code, use `cp` to copy the primary source first and then update the copy rather than rebuilding it from individual pieces. Copy any additional blocks, including comments and docblocks, into the destination before removing them from their source. - **Grep broadly — never assume a subdir** — when searching for any symbol, class, method, or pattern, grep across the whole `src/` (or `tests/`) tree, not a specific package subdir. Assumptions about where something lives produce false negatives. - **Read the source before describing behavior** — never state how code behaves from memory or Laravel assumptions. Hypervel's coroutine runtime breaks many Laravel assumptions; if you haven't read the relevant source, read it first. - **Treat past owner decisions as context, not constraints** — Previous owner approvals and completed plans explain history but do not determine the best design today. Never retain or reject a design merely because it was previously approved; decide from current requirements, code, and evidence. - **Revert failed attempts immediately** — when a fix doesn't work, revert it before trying another approach. Don't leave experimental code in place. - **Check dependency versions before adding them** — Before adding a package dependency to the root `composer.json`, check Packagist for the latest compatible stable version. The root `composer.lock` is intentionally untracked; run `composer update` after adding or merging dependency changes, do not treat an outdated local lock as a repository defect, and never commit it. - **Declare only real dependencies** — Declare packages used directly by the code or needed for a supported installation. Do not add dependencies merely to complete the list. Add a polyfill only when neither the minimum PHP version nor an existing requirement provides the feature. - **Keep Composer metadata functional** — Declare an `ext-*` requirement only when the extension is not guaranteed by Hypervel's minimum PHP version. Add a `suggest` entry only when installing that package enables a concrete, documented feature; conditional interoperability, class-string references, tests, or metadata completeness do not qualify. ### Documentation - **Use one source of truth** — Put all user documentation in `src/docs/`. Package READMEs are intentionally minimal, not a second documentation surface, and must not duplicate user documentation. - **Write user documentation in Laravel-docs prose** — Use the simple, direct, human-friendly style of first-party Laravel documentation. Before drafting, read and match the surrounding prose; for new pages, use `src/docs/processes.md` as a style reference. Explain how to use the API in natural prose and examples. Only include implementation details when they affect how the reader must use it. Before finishing, remove repeated guidance, unnecessary caveats, and explanations already clear from the example. - **Keep the Laravel porting guide current and focused** — Whenever a framework change means existing Laravel application or package code, configuration, integrations, or data require adaptation or a compatibility check, update `src/docs/porting-from-laravel.md` in the same change. Before adding an entry, ask: “What existing Laravel usage would someone port incorrectly without this entry?” Purely additive, opt-in enhancements belong in feature documentation, not this guide. An enhancement that also changes compatibility qualifies because of that difference, not because it adds functionality. Do not document bug fixes, internal implementation differences, performance work, incidental source drift, or narrow edge cases unless they change what a porter needs to do. Briefly identify the affected usage and required action or risk, then link to the relevant canonical documentation instead of repeating its detail. The guide is a high-signal starting context for humans and LLMs, not an exhaustive framework diff or feature catalogue. Treat its context size as a design constraint, and remove stale or duplicated guidance whenever editing it. #### Package READMEs Use this order, omitting items that do not apply: 1. Package header 2. Documentation link, when the package has a meaningful user-facing documentation page (`Documentation: https://hypervel.org/docs/{documentation-slug}`) 3. `Differences From Laravel`, for deliberate differences only 4. Upstream link, when the package is a port or deliberately tracks an upstream package (`Ported from: https://github.com/{vendor}/{package}`) Do not add a documentation link merely for completeness. Omit it when there is no meaningful user-facing documentation page for the package. Do not add upstream links for inspiration or historical lineage. Omit them when the Hypervel package is maintained independently and is not expected to track upstream changes. `Differences From Laravel` is only for deliberate, lasting public contract differences that developers must account for. Bug fixes, correctness or safety fixes, performance improvements, and internal implementation differences do not qualify merely because they make Hypervel behave differently from Laravel. Omit the section when none apply. When porting upstream updates, keep the package’s differences section accurate for the code being examined: add missing public-contract differences, correct stale descriptions, and remove entries that no longer qualify. Briefly explain the affected usage, why it differs, and the Hypervel equivalent where applicable. Apply this equally to first-party and third-party upstreams; link to detailed documentation rather than repeating it. Documented differences record current decisions, not permanent requirements. Preserve supported enhancements, but reassess limitations when upstream changes or a clean, performant implementation make them unnecessary; follow the existing approval rules for API changes. ### Avoid overengineering Build complete, long-term solutions, not MVPs or local workarounds. A broad change is correct when the root cause is in shared code, but every added mechanism must solve a real problem. - **Do not add behavior to guard against unsupported API use.** Before adding defensive code, show that normal supported use actually behaves incorrectly and causes meaningful harm. Behavior that is only possible or surprising when an API is used outside its contract is not a bug. - Prefer the simplest existing Laravel or Hypervel API, PHP feature, or database constraint. Do not duplicate framework behavior with package-owned machinery. - Do not add a new mechanism merely because it sounds robust, flexible, or potentially useful — for example, a registry, retry loop, configuration option, or extension point. It must solve a verified problem, meet a clear approved requirement, support a clearly likely need whose shape is understood, or remove greater complexity elsewhere. - Do not add machinery to preserve invariants across deliberate Laravel-style escape hatches such as `withoutEvents()`, quiet methods, raw builders, raw SQL, disabled middleware, or direct transport access unless the public contract explicitly promises that behavior. - For coroutine or worker-lifetime concerns, identify the concrete shared state, a realistic interleaving, and the resulting leak or failure before adding isolation, locking, cleanup, or caching machinery. - Fix related symptoms in the lowest shared layer that owns the behavior instead of adding separate defensive paths. The size of the resulting change is not a reason to patch around the defect. - A capability may be worth adding before its first use when the need is clearly likely and its design is understood. Do not add generic flexibility for hypothetical consumers. - Avoiding overengineering never justifies an incomplete fix, weaker correctness, missing security or isolation safeguards, or deferring a worthwhile improvement. ### Code conventions - **New Hypervel-owned code and packages must be Laravel-style** — Design new packages and public surfaces as if they were first-party Laravel packages ported to and enhanced for Hypervel. APIs, naming, class responsibilities, code patterns, and directory structure must be ergonomic, intuitive, and immediately familiar to Laravel developers, while internals remain coroutine-safe and optimized for Hypervel's long-lived Swoole runtime and high-performance requirements. Apply the requirements under [Audit changes during modification and code review](#audit-changes-during-modification-and-code-review) from initial design onward. - **Reserve `assert*` methods for testing APIs** — Use prefixes such as `ensure*` or `validate*` for runtime guards. - **Modern PHP 8.4+ with full typing** — use constructor property promotion, readonly properties, enums, match expressions, named arguments, and attributes where they fit. Every file declares `strict_types=1`; parameters, return types, properties, and class constants are natively typed wherever PHP and the inherited API permit (e.g. `resource` cannot be represented as a native PHP type). PHP does not allow return types on `__construct()` or `__destruct()`. - **Contract signature dependencies are lazy** — contract signatures may natively reference types from optional split packages without a reverse Composer dependency. Do not remove these types or add cyclic dependencies solely for split-package isolation. - **Dependency access in new packages** — Inject framework service contracts through constructors or methods rather than using helpers, facades, or direct instantiation. Where injection is impractical, such as static contexts or framework-constructed objects, use `Container::getInstance()->make(...)`. Helpers are fine in route and config files. - **Dependency access within existing packages** — Follow the established style of surrounding classes with the same role, including when adding new classes to a ported package. Preserve upstream’s helper, facade and instantiation style where it keeps updates straightforward. A class being newly written is not, by itself, a reason to use a different style. Change dependency access only for a concrete correctness, lifetime, testability or maintainability benefit, and keep related code internally consistent without rewriting unrelated classes. - **Use imported short class names** — applies to new and ported code, including PHPDoc annotations. Replace fully or partially qualified class references with imported short names; use aliases for naming collisions. Classes in the current namespace need no import. Keep fully qualified names where genuinely clearer, such as middleware arrays and similar config-style identifier lists. - **Group traits in `Concerns/`** — follow the package's existing convention if it already has a `Concerns/` or `Traits/` directory; never mix both in one package. New Hypervel-original packages always use `Concerns/`; a newly ported package keeps its upstream directory name. - **Use Laravel observer conventions** — place Eloquent observers in a top-level `Observers/` directory. Register model-specific observers with `#[ObservedBy(...)]`; use `observe()` only for dynamic registration or observers supplied automatically by a reusable concern. - **Use attributed local scopes** — define local Eloquent query scopes as protected methods marked with `#[Scope]`, rather than legacy public `scopeFoo()` methods. Use separate scope classes only for genuine global scopes. - **No class docblocks unless warranted** — only add a class-level docblock if something unusual or complex needs explanation: purpose, architectural role, usage patterns. Never write one that inventories the class — trait lists, method summaries, "registers X, configures Y" — that duplicates the members' own docblocks and goes stale. Method docblocks (title only, Laravel-style, imperative mood: "Return", not "Returns") are always added, except on test methods, whether identified by a test name prefix or PHPUnit’s `#[Test]` attribute. A body can accompany the title for complex methods that need further explanation. - **Add comments only where they're genuinely useful** (required Laravel-style method title docblocks and config section comment blocks still apply) — a short WHY for logic that isn't obvious from reading the code, the reason behind a bug fix, or logic that's coupled to code in other files and hard to understand in isolation. Avoid unnecessary code comments; don't comment what the code does, and don't annotate routine casts or type normalizations. When reconciling upstream changes, add missing explanations for non-obvious Hypervel behavior being retained, so future syncs can understand why it exists. - **Explain intentional public API and behavior differences from upstream** — Where a future sync could revert or overwrite an intentional difference, add a concise comment beside the relevant code explaining why it differs. Do not annotate mechanical porting changes. Comments on internal implementation differences follow the general commenting rule. - **Don't make classes final by default** — keep classes open; add `final` only when it protects a real invariant or avoids a concrete framework/API problem, e.g. immutability, coroutine-safety, or a security guarantee. - **Place methods logically, not at the end** — group new methods with related ones (getters with getters, setters with setters). Two exceptions: preserve upstream order when merging ported code (see Porting rules), and `flushState()` has its own placement rule (see Static state and test cleanup). - **Only extract methods when justified** — extract only when the logic is complex enough to benefit from a name, it's likely to be reused, or two or more methods call it. Don't extract a simple one-liner with a single caller. - **Never abbreviate variable names** — `$attributes` not `$attrs`, `$connection` not `$conn`. - **Enum cases use PascalCase by default** — `case Pending` not `case pending`, `case OauthToken` not `case OAUTH_TOKEN`. Applies to both backed and unit enums. **Exception:** when `->name` is used as an external identifier (cache keys, cookie names, filesystem disks, rate limiter names, timezone strings) or appears in serialized output (e.g., `toArray()` returning `'name' => $this->name`), match the consuming system's convention (typically lowercase or snake_case). - **Strict comparisons by default** — Use `===` and `!==` in newly written code unless the supported API requires loose comparison. When converting an upstream comparison, verify operand types (`$value === 0.0` for a float, not `=== 0`) and preserve its behavior, including coercion and object equality. Keep loose comparisons where their semantics are intentional and needed to preserve supported behavior; do not replace them solely for consistency. - **Prefer union types over `mixed` when all types are known** — `mixed` is only for truly unconstrained values or cases that cannot be safely narrowed after control-flow analysis. - **Type decisions must be evidence-based** — check corresponding Laravel/Hyperf signatures and docblocks as a reference, then trace the real control flow through method bodies across all callers and callees to confirm the types are correct. - **Fail fast with framework and PHP exceptions** — don't add guards, wrapping, or defensive checks unless they handle a reachable invalid case or materially improve the error, and never swallow exceptions. If code would fail anyway (e.g. null passed to a typed parameter), let it fail naturally instead of adding a check that throws a custom exception — the stack trace is enough to diagnose. - **Use semantic column types in migrations** — `uuid()` / `foreignUuid()` / `uuidMorphs()` for UUIDs, `ipAddress()` / `macAddress()` for network addresses, and `jsonb()` for JSON the database queries, indexes or validates. Use `text()` for opaque serialized values that are only read back whole. PostgreSQL gets the real types (`jsonb`, `uuid`, `inet`, `macaddr`); the other supported databases fall back automatically to compatible types. - **No arbitrary string lengths** — use `->string('name')`, not `->string('name', 100)`. Don't invent limits; specify a length only when the domain or protocol defines one — exact (UUID: 36, ULID: 26, sha-256 hex token: 64) or a defined maximum (IPv6 address: 45). - **No database enums** — use string columns plus PHP enums. Adding a value to a database enum requires a migration. - **Use `dateTime` for arbitrary dates** — use `dateTime` instead of `timestamp` for scheduled dates, expiry dates, historical dates, and similar arbitrary dates. Keep `timestamp` for dates recorded when an action happens, such as `created_at`, `updated_at`, and `deleted_at`. On MySQL and MariaDB, `timestamp` only supports 1970–2038. - **Prefer `timestamp` over `timestampTz` in migrations** — store times in UTC with plain `timestamp` columns, matching the normal convention in Laravel and Hypervel first-party migrations. Reserve `timestampTz` for columns that genuinely need database-level timezone semantics, such as existing timezone-aware schemas or columns written under different session timezones. The schema API supports both — this is a column-choice convention, not an API restriction. - **Guard optional event dispatches with `hasListeners()`** — before constructing and dispatching framework events, guard them with `hasListeners()` so hot paths skip event overhead when nobody is listening. Bare `*` listeners are passive observers and do not count; targeted wildcards do. Do not guard dispatches where dispatching is the side effect, such as jobs, broadcasts, webhooks, or command bus calls. - **Use `Sleep::usleep()` / `Sleep::sleep()` for delays in source code** — `Sleep` is fakeable in tests. Use raw `sleep()` / `usleep()` only where real time must pass, such as test harnesses and external-process polling. - **Use `xxh128` for internal non-cryptographic hashing** — cache and context keys, content checksums, and change detection. It is faster than `sha256`, which is reserved for trust boundaries: stored credential digests, signatures, and anything an attacker gains by forging. Seed it when the hashed value comes from user input, as `SwooleStore` does for its physical table keys. - **Use immutable dates by default** — Hypervel defaults to `Hypervel\Support\CarbonImmutable`, including where Laravel uses mutable Carbon. Create public or application-configurable dates through the `Date` facade or date helpers, and use exact `CarbonImmutable` for framework-owned internal or held values. Type configurable Carbon boundaries as `CarbonInterface` and native or third-party boundaries as `DateTimeInterface`. Capture the return value of every date modifier whose result must persist. Use `Hypervel\Support\Carbon` only for explicit mutable opt-out or conversion behavior. - **Always use American English spelling** — E.g., "behavior" vs "behaviour", "utilize" vs "utilise". ### Configuration These rules apply to all code, including ported code. - **Laravel-style config files** — Group related settings under Laravel's standard section comment blocks, with concise, user-facing explanations. - Always use typed getters for values with one non-null type. Only use `get()` when null, union, or mixed values are meaningful. Add a test for any supported null behavior. - Cast environment-backed booleans and numbers in config files; if `null` is supported, cast only non-null values. Consumers must not repeat those casts. When a factory accepts raw configuration records, normalize types and documented optional defaults once at that boundary; never supply missing required members. - Required settings live in shipped config and must not have a code-level fallback, so missing or misspelled keys fail loudly. - Intentionally optional settings keep one owning fallback and remain discoverable in config or feature documentation. Document what omission or null means. - Framework and package defaults are shallow-merged with application config. `mergeableOptions()` is only for named groups such as connections or stores: application entries replace matching defaults, while other default entries remain. Other nested arrays are replaced as a whole. - Replaceable named and nested records must apply documented optional defaults at their owning boundary; missing required members must still fail. - A non-obvious fallback value shared by multiple source paths must have an owning constant. If user-facing config exposes the same default, keep its concrete value readable and add a focused test asserting that it matches the constant. Do not create constants for obvious source fallbacks such as `true`, `false`, `null`, `[]`, or `'default'`. - **Env var naming** — Ported config keeps upstream names. New Hypervel-specific settings should use the established prefix for the package or subsystem that owns the value (`SERVER_`, `CACHE_`, `REDIS_`, etc.). Determine ownership semantically, not from the config filename: aggregate files such as `app.php` contain multiple domains, and `APP_` is for genuinely application-wide settings. If a value mirrors another config key, reuse that key's environment variable instead of defining a duplicate. - **Use `resolve...Using` for Hypervel-owned config resolvers** — prefer this naming for callbacks that resolve config-derived values, unless an established Laravel domain convention already exists, such as `redirectUsing()`. ## Container Hypervel's container keeps Laravel's named API surface — `bind()`, `singleton()`, `scoped()`, `instance()`, aliases, contextual bindings — with resolution adapted for long-lived Swoole workers. Container ArrayAccess and dynamic service properties are intentionally unsupported. `make()` and `get()` resolve identically; `get()` is the PSR-compliant exception wrapper. `Container::getInstance()` auto-creates via `??= new static()`, so it always returns a container. ### Resolution semantics vs Laravel The critical difference: **unbound concrete classes are auto-singletoned**. In Laravel, `make()` on a class with no binding builds a fresh instance every call. In Hypervel, the first resolution caches the instance (in `$autoSingletons`) for the worker lifetime. Worker-safe services can reuse initialized state, avoiding repeated construction cost and allocation. Explicit bindings override this (bound classes follow their binding type), and `SelfBuilding` and `Transient` classes are excluded. | Registration | Laravel | Hypervel | |---|---|---| | Unbound concrete class | Fresh instance every `make()` | **Auto-singletoned** on first `make()`; cached for the worker lifetime | | `singleton()` / `#[Singleton]` | Cached in `$instances` | Same — but the worker serves many requests, so the instance is shared across all of them | | `scoped()` / `#[Scoped]` | A worker-lifetime singleton the runtime flushes between requests (`forgetScopedInstances()`) | Cached per coroutine via `CoroutineContext` — isolated between the concurrent requests in one worker | | `bind()` | Fresh instance every `make()` | Same | | `make($abstract, $params)` / `makeWith()` | Contextual build — never cached | Same — parameters bypass the singleton, scoped, and auto-singleton caches | | `build($concrete)` | Constructs the concrete directly, bypassing bindings and caches for the top-level class | Same; Hypervel adds `buildWith($concrete, $params)`. Nested constructor dependencies still resolve through the container | | `implements SelfBuilding` | Container calls the class's static `newInstance()` | Same, and it also skips auto-singletoning | | `implements Transient` | No equivalent marker | Unbound resolutions are always fresh; explicit bindings still determine the lifetime. Eloquent models inherit this marker from `Model` | Rules that follow from this: - **Ownership of mutable state decides the lifetime.** Most services, middleware, listeners, factories, and formatters are safe to auto-singleton because they are stateless or hold only worker-owned state. State a service computes and keeps for reuse — caches, registries, resolved configuration, callbacks, or connections — is intended worker-lifetime state, so those classes remain auto-singletons. State owned by a caller, request, or operation must not be stored on a worker-shared object: keep invocation state in `CoroutineContext`, use `scoped()` for one instance per coroutine/request, `bind()` for a fresh bound instance, or `Transient` only when freshness is intrinsic to the whole hierarchy. - **A class that captures request state in its constructor and is neither scoped nor execution-scoped is a coroutine-safety bug** — reach consensus on the fix through a second-opinion loop with your peer agent before implementing. Contextual attributes such as `RouteParameter` and `CurrentUser` already make their containing class execution-scoped. - **Do not infer `Transient` from mutable properties, setters, or a no-argument constructor.** Remove incidental mutation instead of marking the class as `Transient`, and verify every subclass before marking a base class. For example, `Collection` holds caller-owned values and is `Transient`, while `Manager::$drivers` is a service-owned cache and remains auto-singletoned. ### Choosing a resolution strategy - Unbound concrete service whose state is safe to share for the worker lifetime → do not bind it; auto-singletoning is the default. - Interface or canonical service key shared for the worker lifetime → `singleton()`. - Existing object intentionally shared for the worker lifetime → `instance()` during boot, or in tests for a swap. - Unbound class whose constructor uses an `ExecutionScopedAttribute` → do not bind it; the container scopes it automatically to the current execution. - One instance per coroutine / request → `scoped()`. - Fresh instance required by a specific binding → `bind()`. - Freshness intrinsic to the whole class hierarchy → `Transient`. - One resolution should ignore the implicit auto-singleton and constructor-derived execution scope while retaining aliases, bindings, extenders, and resolving callbacks → `makeTransient()`. - Class-controlled construction through `newInstance()` → `SelfBuilding`. - One contextual resolution with explicit constructor parameters → `make()` / `makeWith()` with parameters. Aliases, binding definitions, and resolving callbacks still apply, but cached lifetimes are bypassed. - Direct construction that intentionally bypasses top-level bindings and caches → `build()` / `buildWith()`. Do not use these when aliases, test swaps, or resolving callbacks must be honored. ### Binding patterns **1. Canonical string key — use a closure with `new`:** ```php // The concrete (AuthManager) is listed as an alias for 'auth', so // singleton('auth', AuthManager::class) would create a circular resolution cycle: // 'auth' -> build AuthManager -> getAlias(AuthManager) -> 'auth' -> infinite loop $this->app->singleton('auth', fn ($app) => new AuthManager($app)); ``` **2. Abstract is not in the alias table — use string concrete:** ```php // Neither FormatterInterface nor DefaultFormatter are aliases for anything, // so the container can resolve this directly without cycles. $this->app->singleton(FormatterInterface::class, DefaultFormatter::class); ``` **3. Abstract and concrete are the same class — do not bind merely to share it.** Hypervel's container auto-singletons unbound concrete classes on first resolution. An explicit `singleton(Foo::class)` is redundant unless the class declares an intrinsic fresh lifetime through `Transient` or `SelfBuilding`: ```php // Wrong: redundant; auto-singleton handles this. $this->app->singleton(BroadcastManager::class); // Correct: do not bind it. The first make(BroadcastManager::class) auto-singletons it. ``` ### Aliases and canonical keys Before binding core framework services, check `Application::registerCoreContainerAliases()`. If the abstract or concrete participates in the alias table, choose the binding key carefully. `bind()` and `singleton()` store bindings under the exact abstract key passed; `resolve()` resolves aliases before lookup. Binding an alias instead of the canonical key can orphan the binding. When adding a core alias, use the string key as the canonical abstract: ```php // Wrong: contract is canonical. \Hypervel\Contracts\Auth\Factory::class => [ 'auth', \Hypervel\Auth\AuthManager::class, ], // Correct: string key is canonical. 'auth' => [ \Hypervel\Auth\AuthManager::class, \Hypervel\Contracts\Auth\Factory::class, ], ``` For canonical string keys such as `'auth'`, `'cache'`, `'db'`, `'request'`, and similar framework services, prefer closure bindings: ```php $this->app->singleton('auth', fn ($app) => new AuthManager($app)); ``` Facades for these services should resolve the canonical container key instead of the concrete manager class. Do not add new core aliases without need. Only add an alias when Hypervel needs it as part of its public container surface. ## Providers and Listeners ### Provider registration When adding a package provider, register the provider and aliases through the package Composer metadata and the root Composer metadata, as described in the package skeleton workflow under Porting Packages. Add a provider to `DefaultProviders` only if every Hypervel application needs it at framework startup, such as auth, cache, database, session, validation, view, or low-level Swoole infrastructure. Optional packages such as Reverb, Scout, Telescope, Sentry, and Watcher should rely on package discovery instead. Before changing `registerBaseServiceProviders()`, reach consensus with your peer agent through a second-opinion loop that the service is needed during bootstrap, before normal providers are registered. Reserve this method for framework infrastructure needed by the boot process itself. It is safe to have the same provider listed in both `registerBaseServiceProviders()` and `extra.hypervel.providers` when early loading is genuinely needed. `Application::register()` deduplicates providers by class name, and the discovery entry ensures standalone installs still load the provider. ### Boot-time callbacks and resolvers For callbacks and resolvers registered during provider boot, inject worker-safe dependencies into `boot()` and capture them in the closure. Do not repeatedly resolve them from the container when the callback runs. Event listeners are the exception described below: resolve listeners at event time so application and test rebindings are honored. ### Listener registration Register listeners in the service provider's `boot()` method using closures that resolve the listener from the container: ```php public function boot(): void { $events = $this->app->make('events'); $events->listen(AfterWorkerStart::class, function (AfterWorkerStart $event) { $this->app->make(AfterWorkerStartListener::class)->handle($event); }); } ``` Resolve from the container (`$this->app->make(...)`) rather than injecting or instantiating directly — this ensures constructor dependencies are resolved and the listener benefits from auto-singleton caching. ## Coroutine and Worker-Lifetime State Decide where state lives before writing code: | State | Storage | |---|---| | Immutable metadata shared by all requests | Static property cache or worker-lifetime singleton | | Stateless service shared by all requests | `singleton()` or auto-singleton (see Container) | | Mutable state for one request, operation, or coroutine | `CoroutineContext` or a `scoped()` binding | | Fresh mutable object per resolution | `bind()`, contextual parameters, or `Transient` for an intrinsically fresh class hierarchy | - **Use `Hypervel\Context\CoroutineContext` for invocation-scoped state** — anything that must not be visible to other concurrent coroutines in the same worker. Static properties and singleton fields leak across coroutines: whatever one coroutine sets becomes visible to all others in the worker. Use the established key-naming convention: `__.` value prefix, `_CONTEXT_KEY` / `_CONTEXT_KEY_PREFIX` constant suffixes, public only when other classes or tests reference the constant. Do not use `Hypervel\Support\Facades\Context` as the low-level coroutine store; it provides Laravel-style application context instead. - **Group related coroutine state in `…State` classes when useful.** Store the object in `CoroutineContext` so callers can retrieve it once and read or update several properties without repeated context lookups or array copying. Use named properties for known fields and arrays for keyed collections. When copied child coroutines need independent state, implement `ReplicableContext` with the appropriate copying behavior. See `InertiaState` and `ContainerResolutionState`. - **Configure process-global values only during worker boot** — config is a process-global singleton, so `Config::set()` during request handling changes behavior for every concurrent request in the worker. Never mutate config for request-specific behavior; use `CoroutineContext` or middleware instead. Provider boot-time configuration is fine — it runs once per worker. - **Create Swoole tables from a `BeforeServerStart` listener, never in `register()`** — `register()` runs in every process, including ones that never use the table. Managers that create tables by name also `seal()` there. Swoole frees table memory only on `destroy()`, which only the creating process may call — never from a destructor, since forked workers run those too. - **Name static cache properties for what they store** — not with a `Cache` suffix; static properties in Swoole workers are caches by nature. Exception: matching an existing Laravel-ported pattern in the same class (e.g. `$classCastCache`, `$attributeCastCache` on `HasAttributes`). - **Bound worker-lifetime lookup caches** — any internal lookup cache retained across requests in a worker—for example, in static properties or singleton instances—must have a naturally limited set of keys or discard entries that are safe to recompute. Do not add a size limit merely to hide growth from request- or user-derived keys. This governs framework-derived caches, not application-owned cache stores such as the `worker-array` driver. - **Review worker-lifetime state explicitly** — before introducing or modifying static properties/caches, singletons or other long-lived state, assess the Swoole persistence impact (memory leaks, cross-request behavior) and reach consensus on the design through a second-opinion loop with your peer agent. - **Document worker-lifetime mutators** — when adding or touching a public method that mutates static state, singleton-held configuration, manager registries, cached drivers, global callbacks, or other worker-lifetime state, add a short warning to the method docblock if the method is intended only for boot-time configuration or tests. Use the tag-first format so humans and LLMs can recognize it quickly: - `Boot-only.` — for startup configuration methods - `Tests only.` — for test fakes, swaps, and resolver overrides - `Boot or tests only.` — for cache/registry clearing methods used during boot reconfiguration or test cleanup The second sentence should name the concrete failure mode, e.g. "The callback persists in a static property for the worker lifetime and affects every subsequent request." Do not add these warnings to methods that are genuinely safe for normal runtime/per-request use. If a method is commonly expected to be used dynamically but mutates shared worker-lifetime state, treat that as a coroutine-safety bug and reach consensus on the fix through a second-opinion loop with your peer agent. Never just document it. - **Assess static caching opportunities** — if a path repeatedly computes expensive stable metadata and worker-lifetime static caching would be a clear win, reach consensus with your peer agent through a second-opinion loop on what to cache, the expected benefit, bounded memory use, and correctness across requests before implementing. Classes that use static caching need a `flushState()` method for test cleanup — see "Static state and test cleanup" under Writing Tests. - **Release idle pooled connections before unrelated long waits** — Holding connections while waiting for external services or child coroutines can exhaust pools and limit concurrency. Release them at the appropriate shared boundary, preserving active operations and session ownership; consider reacquisition costs rather than adding release calls indiscriminately. For database sessions, reuse `ConnectionResolver::releaseIdleConnections()`, which preserves transactions and explicit pins. See `Mailer::sendSymfonyMessage()` and `ConcurrentImportRunner::wait()` for examples. ### Coroutine cancellation See [Coroutine Cancellation](src/docs/coroutines.md#coroutine-cancellation) for handling patterns and examples. - **Treat coroutine cancellation separately from errors** — Where cancellation needs different handling and the operation runs callbacks or listeners—including `releaseIdleConnections()`—use `Coroutine::causedByCancellation($exception)`. Cleanup errors can carry cancellation in their exception chain. Preserve the original exception, perform owned cleanup, and propagate cancellation instead of retrying or continuing normal work. Report genuine cleanup errors without swallowing cancellation. Add this only where cancellation can actually happen and mishandling it causes a real bug. Keep plain checks for native-only operations. - **Preserve cancellation ownership** — If shared work can safely be retried, allow another waiter to retry after its owner is canceled. Otherwise, share the failure behind `ForeignCancellationException`, or `ChildCancellationException` for an independently canceled child, preserving the original cause. The canceled owner propagates its own exception unchanged; waiters must not mistake it for their own cancellation. - **Contain cancellation in direct engine coroutines** — Callbacks passed directly to `Engine\Coroutine` must catch cancellation after cleanup so it cannot terminate the worker. The higher-level `Coroutine` class already provides this protection. - **Test cancellation through cleanup** — When adding or changing cancellation-sensitive retry, child-management or shared-result handling, extend relevant regression tests with throwing cleanup during cancellation. Verify the resulting behavior, not just the exception type. ## When to Stop and Report Resolve the technical issues below through evidence-based second-opinion loops with your peer agent, then proceed on consensus. Ask the owner only for unapproved public API departures or capability removals; database schema changes; meaningful performance regressions or weakened guarantees; consequential tradeoffs the requirements do not settle; unresolved peer disagreements; or actions requiring owner authorization. - **Investigate anything unusual** — missing dependencies, logic needing special consideration, things that don't make sense for Hypervel, etc. Investigate and reach consensus on the findings and recommended action through a second-opinion loop with your peer agent before implementing. Do not dismiss concerns as theoretical without investigation. If no supported, realistic path or meaningful harm is found, do not present it as a defect or add machinery to address it. - **Do not defer work without owner authorization** — do not remove functionality or substitute stubs or TODOs for implementation unless the owner authorizes it. Preserve existing TODO comments unless their underlying work is verified complete or the owner directs otherwise. If blocked, reach consensus on a complete solution through a second-opinion loop with your peer agent. - **Investigate source code bugs** — for bugs found in Hypervel or upstream code being ported, including through PHPStan or tests, reach consensus on the root cause and fix through a second-opinion loop with your peer agent before implementing. Upstream bugs must be fixed, not ported as-is. - **Trace upstream differences before calling them bugs** — a difference from Laravel or an upstream package is not proof that Hypervel is wrong, and matching upstream is not proof that Hypervel is correct. A verified Hypervel defect remains a defect when upstream has the same problem. - **Do not work around incorrect existing code to avoid churn** — if work exposes incorrect types, wrong logic, missing methods/classes, or other real defects in existing Hypervel code, fix the underlying code instead of adding compatibility hacks or local workarounds to sidestep the problem. Prioritize correctness and code quality over keeping the change small. For any non-trivial fix, reach consensus on the root cause and fix through a second-opinion loop with your peer agent before implementing. - **Never weaken or drop tests to work around source issues** — if a test exposes source-side problems (wrong types, broken logic, missing classes/methods, signatures that diverge from Laravel, missing API parity, etc.), reach consensus on the root cause and fix through a second-opinion loop with your peer agent before implementing. Never delete, skip, loosen assertions, or alter tests to make them pass against flawed source code. The test is the spec; the source gets fixed. For type errors specifically, "When tests expose source code type errors" under Writing Tests covers how to identify the correct type. - **Never dismiss issues as "out of scope" or "pre-existing"** — when any issue surfaces (bugs, unexplained divergences or API gaps, incorrect visibility, type inconsistencies, naming mismatches, etc.), investigate and reach consensus on the findings and solution through a second-opinion loop with your peer agent before implementing. Never use phrases like "out of scope", "pre-existing", "not part of this work", "separate concern", or "unrelated" to justify ignoring an issue. Only the owner may authorize leaving a confirmed issue unresolved. Worker-lifetime state has additional second-opinion requirements — static/singleton state changes, unsafe public mutators, static caching opportunities, and per-request state captured by auto-singletons — listed under Container and Coroutine and Worker-Lifetime State. ### Handling failing tests - **Investigate tests broken by your changes before updating them.** When an implementation change causes a previously passing behavioral test to fail, treat the test as evidence of a contract or missed edge case. Trace what it protects through the tested code, callers, upstream source/tests, and relevant history. Before changing the test, reach consensus with your peer agent through a second-opinion loop that the behavior change is correct, including its compatibility and edge-case consequences. - **Easy fixes** (namespace typos, missing return types, a missed namespace update, etc.) — fix and continue. - **Non-trivial failures** (behavioral changes, test logic issues, unclear root causes) — investigate and reach consensus on the root cause and fix through a second-opinion loop with your peer agent before implementing. Investigate all failures thoroughly — don't assume a failure is caused by your change without confirming it. - **Do not skip or remove tests except under the rules below.** Never comment out, skip, or avoid porting a test because the required functionality is missing. If Hypervel should support it, investigate the root cause and reach consensus on the fix through a second-opinion loop with your peer agent. Keep every applicable upstream test, even if redundant or seemingly unnecessary; exclusions require justified Hypervel architectural or API differences. Remove tests for the approved unsupported features listed under Porting Laravel Tests without asking. For any other exclusion or removal, reach consensus through a second-opinion loop on what the test covers and why removal is justified. ## Writing Tests These rules apply to all tests — new tests for framework work and ported tests alike. ### Avoid overengineered tests Test supported public behavior, meaningful branches, verified regressions, and realistic coroutine or worker-lifetime failures. Do not add production APIs, branches, or defensive machinery solely to make speculative states testable. Do not require invariants to survive deliberate framework escape hatches unless the public contract promises that behavior. ### Exceptions in tests In any test, PHPUnit assertion failures, skips, and incomplete markers extend `AssertionFailedError`, which extends `RuntimeException`; a catch must not swallow one and let the test pass. An exception test must fail unless its intended behavior and exception path occurred. Before rewriting an existing or ported exception test, demonstrate a concrete violation; a broad catch alone is not a defect. When writing a new test that controls the exception, prefer prebuilding it and asserting its identity after the catch. ### Directory layout Laravel-ported tests follow upstream placement; the rules below apply to Hypervel-only test files. All tests live in `tests/{PackageName}/` (PascalCase). Tests that require external services go in `tests/Integration/{PackageName}/` — see Integration tests below. When only some integration tests for a package require one service, group them in `tests/Integration/{PackageName}/{ServiceName}/`. When every integration test for the package requires that service, keep them directly in the package directory. Package-specific tests that require one database driver go in `tests/Integration/{PackageName}/Database/{Postgres|MySql|MariaDb|Sqlite}/`. The database workflows discover these directories by convention. ### Base classes **Never extend `PHPUnit\Framework\TestCase` directly.** Always use one of these: | Class | Use When | |-------|----------| | `Hypervel\Tests\TestCase` | Unit tests, mocks only, no container needed. Tests that only need an isolated scratch directory stay here — use `ParallelTesting::tempDir()` (see Temp directories below). | | `Hypervel\Testbench\TestCase` | Integration tests (needs container for facades, config, DB, etc.) **or any test that needs a full app skeleton / writes through `BASE_PATH`** — testbench clones a disposable runtime skeleton per run and exposes its path via `BASE_PATH` (and `TESTBENCH_BASE_PATH` for subprocesses), deleted on shutdown. Committed source is never mutated. | Always call `parent::setUp()` in your setUp method. ### Configuration Put application environment configuration in `defineEnvironment()` or a `#[DefineEnvironment]` method. Do not mutate application config in `setUp()`, because providers or resolved services may already have consumed it. Within test methods, use `config()` directly to read or mutate configuration values. Do not use no-argument `config()` as a dependency source; resolve the `Repository` contract from the container when constructing a test subject manually. ### Test support files All **standalone** test support files — PHP classes, non-class PHP files, and non-PHP files (JSON, SQL, images, templates, etc.) — go in a single **`Fixtures/`** directory (capital F). This matches Laravel's predominant convention. PHP classes in `Fixtures/` are PSR-4 autoloaded like any other test file. Put independently reusable fixture classes and enums in separate PSR-4 files. Keep single-test helpers inline; never share inline helpers between test files. Keep Integration-only fixtures under Integration, and share fixtures across unit and Integration tests only when it makes the tests clearer. Preserve combined files when their structure is itself part of what is being tested. ### Helper class namespacing Tests often define helper classes (models, stubs) with generic names like `User`, `Post`, or `Comment`. When multiple test files use the same namespace and define classes with the same name, PHP throws "Cannot redeclare class" errors. **Use test-specific namespaces only for collision-prone helper classes** (matching Laravel's pattern): ```php // WRONG - shared namespace causes conflicts for generic helper names namespace Hypervel\Tests\Integration\Database; class EloquentDeleteTest extends DatabaseTestCase { ... } class Comment extends Model {} // Conflicts with Comment in other files! // CORRECT - test-specific namespace isolates generic helper names namespace Hypervel\Tests\Integration\Database\EloquentDeleteTest; class EloquentDeleteTest extends DatabaseTestCase { ... } class Comment extends Model {} // No conflict - different namespace ``` Use this when helper classes have generic names likely to appear in other test files. Do not add extra namespaces for helper classes whose names already include the tested feature or package context, such as `FailingHorizonInstallCommand` or `MissingProviderTelescopeInstallCommand`. When a test-specific namespace is needed, the namespace includes the test class name as the final segment. This means: - Each affected test file has its own namespace - Generic helper classes can use simple names (`Comment`, `Post`, `User`) - No `$table` properties needed (Eloquent derives `comments` from `Comment`) - No explicit foreign keys needed (Eloquent derives `user_id` from `User`) PHPUnit loads test files directly (not via autoloading), so the namespace doesn't need to match the directory structure. ### Temp directories for file I/O Tests that write files to disk must never write to the committed `tests/` directory. For tests needing a full app skeleton, `Testbench\TestCase` handles this automatically (see testbench entry in the paths table above). For any test that only needs an isolated scratch directory, use `ParallelTesting::tempDir('TestName')` — store it as a property, delete any leftover copy and create it fresh in `setUp`, then delete it again via `Filesystem::deleteDirectory()` in `tearDown`. Use `sys_get_temp_dir()` directly only when the system temporary path itself is the behavior under test. See `FoundationViteTest` or `OptionTest` for the pattern. The Testbench skeleton clone is shared for the whole worker, so tests that write under `BASE_PATH` must restore or delete the exact files they touch in `tearDown()`. For `.env` files, prefer `useEnvironmentPath()` with an isolated `ParallelTesting::tempDir()` directory. ### Coroutine support All tests run inside coroutines by default. The `RunTestsInCoroutine` trait is on both base test cases (`Hypervel\Tests\TestCase` and `Hypervel\Foundation\Testing\TestCase` / Testbench), so each test method automatically runs in a fresh coroutine. Context is destroyed when the coroutine ends — no manual cleanup needed. **Never add `use RunTestsInCoroutine;` to individual test classes.** It's inherited from the base class. If you encounter a test extending raw `PHPUnit\Framework\TestCase`, change it to extend `Hypervel\Tests\TestCase` instead. **Opting out of coroutines:** Set `protected bool $runTestsInCoroutine = false;` on the test class. This is needed when: - Tests call `run()` directly to create their own coroutines (e.g., pool management tests, parallel HTTP tests) - Tests explicitly verify non-coroutine → coroutine transitions **PHPUnit constraint:** `setUp()` and `tearDown()` run outside the test method's coroutine (PHPUnit 13's `runBare()` is `final`). For DB operations in setUp/tearDown, Foundation TestCase provides `runInCoroutine()` which creates temporary coroutines and bridges transaction state via `preserveTransactionContext()`. **Optional hooks** for code that must run inside the test's coroutine: - `setUpInCoroutine()` — runs inside the coroutine before the test method - `tearDownInCoroutine()` — runs inside the coroutine after the test method These are primarily useful for DB operations or external service setup that needs coroutine context. Most ported Laravel tests won't need them. ### Testing coroutine state isolation To prove state is per-coroutine (not shared on a worker-lifetime singleton), spawn concurrent coroutines via `parallel()` from `Hypervel\Coroutine` and `usleep()` between mutation and read — the sleep forces the runtime to interleave them; without it tasks may complete sequentially and the leak won't reproduce. ```php use function Hypervel\Coroutine\parallel; [$a, $b] = parallel([ function () use ($service) { $service->set('A'); usleep(5000); return $service->get(); }, function () use ($service) { $service->set('B'); usleep(5000); return $service->get(); }, ]); ``` Examples: `tests/Inertia/CoroutineIsolationTest.php`, `tests/Container/CoroutineSafetyTest.php`. Name new tests `CoroutineIsolationTest` / `CoroutineSafetyTest` for discoverability. ### Request context in tests `request()` resolves from `RequestContext` — when no request exists in context (tests that don't make HTTP requests), each `request()` call creates a throwaway fallback instance. This means `request()->merge()` has no effect on subsequent `request()` calls. Replace `request()->merge(['key' => 'value'])` with `RequestContext::set(Request::create('/?key=value'))` to seed a stable request in context. Seed application requests with `RequestContext::set()`; replacing the `'request'` binding with `instance()` bypasses coroutine-local behavior. ### Static state and test cleanup `AfterEachTestSubscriber` handles framework-global cleanup between tests. It calls `flushState()` on framework classes that hold static state, and resets the container itself — `Container::flushState()` + `setInstance(null)` — plus `CoroutineContext::flush()`, with each test in a fresh coroutine. So container singleton/auto-singleton instance state and coroutine context don't leak between tests; only `static`/process-global state and live external resources need package cleanup, not mutable state on a container-cached instance. Do not duplicate framework-static resets in `tearDown()`; `AfterEachTestSubscriber` is their one authoritative registry. Tests still own resources they create, such as child coroutines, subscribers, processes, sockets, and temporary files, and must close or join them through exception-safe cleanup. Tests that create a `Swoole\Table` pass it to `trackSwooleTable()` from `InteractsWithSwooleTables`; otherwise its memory stays allocated until the PHPUnit worker exits. When writing or porting source classes that use static properties for caching (e.g., `$booted`, `$globalScopes`, resolved config values, compiled formats): 1. Add a `public static function flushState(): void` method that resets the static properties to their initial values 2. Check whether the subscriber (`src/testing/src/PHPUnit/AfterEachTestSubscriber.php`) should call it — if the cached state could leak between tests and cause failures, add the call Framework-owned classes go in `AfterEachTestSubscriber`. First-party optional framework packages must stay in grouped optional methods at the bottom of that subscriber and must be invoked through `callIfExists()`. Third-party packages, private packages, and applications should register cleanup for process-local state that survives application teardown through `extra.hypervel.test-state` and a `TestState` registrar instead of hardcoding their classes into the framework subscriber. These callbacks run after the test application is destroyed, so they must not resolve container services; external resources remain owned by their test traits. Do not add `Hypervel\Testing\PHPUnit\AfterEachTestCleanup` itself to `AfterEachTestSubscriber`. Its callbacks are suite-level registrations that must persist for the PHPUnit worker lifetime. Place `flushState()` at the end of the class. The only exception is when the class has trailing magic dispatch/lifecycle methods (`__call`, `__callStatic`, `__get`, `__set`, `__isset`, `__unset`, `__destruct`) at the end; in that case, place `flushState()` immediately before that trailing magic-method block. `__invoke()` is not a placement anchor. Use the standard title docblock for `flushState()` methods: ```php /** * Flush all static state. */ ``` Do not add `Boot-only.`, `Tests only.`, or `Boot or tests only.` warning paragraphs to `flushState()` docblocks. Those warnings belong on public mutators and registrars that userland might call incorrectly, not on this test cleanup hook that is only registered in `AfterEachTestSubscriber`. Keep the docblock to the title only — no extra paragraphs. If the method body has a non-obvious WHY worth explaining (ordering constraints, late-static-binding subtleties, etc.), put it as an inline comment above the relevant line inside the method, not as an extra paragraph under the title. When the property's initial value and `flushState()`'s reset value share a literal (a number, string, class name, etc.), extract it to a `DEFAULT_*` class constant and reference it from both sides — this prevents drift if the default ever changes. Make the constant `public` only if tests or external callers reference it; otherwise `protected`. Nullable lazy caches and callback slots are exempt: `null` there is the structural "not yet computed" sentinel required by the `??=` pattern, not a configurable default — initialize and reset with a literal `null`, no constant. ```php public const DEFAULT_TRUNCATE_AT = 120; public static false|int $truncateAt = self::DEFAULT_TRUNCATE_AT; public static function flushState(): void { static::$truncateAt = self::DEFAULT_TRUNCATE_AT; } ``` ### Per-package base test cases Do **not** create per-package abstract test case classes (e.g., `EngineTestCase`, `CoroutineTestCase`) just for coroutine support — it's already on the base class. A per-package base class is only justified when there is shared setUp logic — e.g., shared container mock setup, shared helpers, or shared test fixtures that multiple test classes in the package need. ### Mockery **Always import as `m`:** Use `use Mockery as m;` and call `m::mock()`, `m::spy()`, etc. Never use the full `Mockery::` prefix. Framework base test cases own Mockery verification in `tearDown()` so unmet expectations are attributed to the test that created them. The global `AfterEachTestSubscriber` remains a fallback for tests using another base case and always resets framework state even when Mockery verification fails. Tests must not add their own `Mockery::close()` calls. ### Stricter typing Hypervel uses stricter types than Laravel. Laravel-trained habits produce incomplete test mocks that loose typing silently accepts — Hypervel's types reject them. This applies to new tests and ported tests alike. **Model properties require type declarations:** ```php // Laravel protected $table = 'users'; protected $fillable = ['name']; public $timestamps = false; // Hypervel protected ?string $table = 'users'; protected array $fillable = ['name']; public bool $timestamps = false; ``` **Mock return types must match:** ```php // Laravel (loose - stdClass works) $connection = m::mock(stdClass::class); // Hypervel (strict - use correct type) $connection = m::mock(PDO::class); $query = m::mock(QueryBuilder::class); ``` **Fluent methods need return values:** ```php // Laravel (null return silently accepted) $builder->shouldReceive('where')->with(...); // Hypervel (must return for chaining) $builder->shouldReceive('where')->with(...)->andReturnSelf(); ``` **Mocking methods with `static` return type:** Methods like `newInstance()` have `static` return type, meaning they must return the same class (or subclass) as the object they're called on. Mockery creates proxy subclasses, so returning the parent class fails: ```php // FAILS - mock is Mockery_1_MyModel, returning MyModel fails static type $this->related = m::mock(MyModel::class); $this->related->shouldReceive('newInstance')->andReturn(new MyModel); // WORKS - use partial mock and andReturnSelf() $this->related = m::mock(MyModel::class)->makePartial(); $this->related->shouldReceive('newInstance')->andReturnSelf(); // Test attributes on the mock itself (partial mock has real Model behavior) $result = $relation->getResults(); $this->assertSame('taylor', $result->username); ``` This is a testing-only issue — the strict types are correct and an improvement. In production code, you never mock Models and call `newInstance()`. **When `andReturnSelf()` isn't enough:** If a test needs to verify distinct instances (e.g., `makeMany()` returns different objects), use a concrete test stub instead of mocks: ```php class EloquentHasManyRelatedStub extends Model { public static bool $saveCalled = false; public function newInstance(mixed $attributes = [], mixed $exists = false): static { $instance = new static; $instance->setRawAttributes((array) $attributes, true); return $instance; } public function save(array $options = []): bool { static::$saveCalled = true; return true; } } // Test verifies real behavior, not mock expectations $this->assertNotSame($instances[0], $instances[1]); $this->assertFalse(EloquentHasManyRelatedStub::$saveCalled); ``` Concrete stubs are the correct approach here — they test actual behavior rather than just verifying mocks were called correctly. ### When tests expose source code type errors If a test fails with a type error, the source code type may be wrong — not the test. Types should be **correct**, not just strict. A narrow type that doesn't cover all valid cases is incorrect. **How to identify:** - Test returns/passes a type that the source code should accept but doesn't - The type is a parent class of what's currently declared (e.g., `Support\Collection` vs `Eloquent\Collection`) **How to fix:** 1. Identify all valid types the method can accept/return 2. Use the common base type that covers all cases without being unnecessarily loose 3. Fix the source code, not the test **Example:** A method returns `Eloquent\Collection` normally, but an `afterQuery` callback can return `Support\Collection`. Since `Eloquent\Collection` extends `Support\Collection`, the correct return type is `Support\Collection` — it covers both cases precisely. **Wrong approach:** Removing types, using `mixed`, or modifying tests to avoid the type check. These hide the real issue. ### Docblocks and types - Add `declare(strict_types=1);` at the top of every file - Add `: void` return types to test methods. This keeps tests consistent with the repo's full-typing rule. - **Use PHPUnit attributes instead of docblock annotations** — prefer `#[DataProvider('...')]`, `#[Depends('...')]`, etc. over their `@dataProvider`/`@depends` docblock equivalents. Do **not** add `@internal`/`@coversNothing` docblocks or `#[CoversNothing]`/`#[CoversClass(…)]` attributes — Hyperf uses both forms but Laravel doesn't, and they serve no purpose outside strict coverage modes ### Integration tests Tests requiring separately configured services or public APIs go in `tests/Integration/{PackageName}/`. Self-contained loopback server fixtures may stay in `tests/{PackageName}/` if they need no external setup and use automatically assigned ports, bounded waits, unconditional cleanup, and parallel isolation. **Optimize integration tests for parallel testing** — ParaTest runs tests concurrently. Use `RefreshDatabase` by default and `DatabaseTruncation` when transaction depth zero is required; reserve `DatabaseMigrations` for migration behavior or a genuinely fresh migrated schema/baseline. Set up only the tables and services each test needs. Keep database cases in one data-provider test when splitting them would make schema setup compete. Load the full schema only when testing migrations or schema parity; do not hide avoidable slowness with longer timeouts. Service workflows enumerate their test directories explicitly. Adding a service-specific directory requires adding it to the matching workflow; using the service trait provides isolation and skip behavior but does not make CI discover the test. #### External service test traits Integration tests that use an external service must use that service's test trait. | Trait | Service | Key Env Vars | |-------|---------|-------------| | `InteractsWithRedis` | Redis / Redis Cluster / Valkey | `REDIS_HOST`, `REDIS_PORT`, `REDIS_CLUSTER_HOSTS_AND_PORTS` | | `InteractsWithMeilisearch` | Meilisearch | `MEILISEARCH_HOST`, `MEILISEARCH_PORT`, `MEILISEARCH_KEY` | | `InteractsWithTypesense` | Typesense | `TYPESENSE_HOST`, `TYPESENSE_PORT`, `TYPESENSE_API_KEY`, `TYPESENSE_PROTOCOL` | | `InteractsWithAlgolia` | Algolia | `ALGOLIA_APP_ID`, `ALGOLIA_SECRET` | | `InteractsWithServer` | Engine test servers (HTTP, TCP, WebSocket, HTTP/2) | `TEST_SERVER_HOST` | This applies whether the test calls the service directly or reaches it through the package code under test. These traits are required for external-service tests to work under ParaTest. Parallel workers share external services unless the trait isolates them. Tests that bypass the trait will leak state across workers and fail depending on timing. The traits handle service-specific setup and cleanup. For example, `InteractsWithRedis` assigns each ParaTest worker its own Redis database and flushes it before and after each test. Redis Cluster only supports database zero, so Cluster suites run serially and flush that database between tests. Both approaches isolate the test keyspace without changing the Redis behavior being tested. If a service is not configured, the trait skips the test before connecting. If the service is configured but unreachable or misconfigured, the test fails. When adding integration tests for a new service type that has no trait yet, create one following this same pattern (per-worker isolation, skip-when-unconfigured, fail-when-unreachable). #### phpunit.xml.dist `tests/Integration/` is **not** excluded from `phpunit.xml.dist`. The skip traits handle graceful skipping when service env vars are not configured. When services are explicitly enabled (CI or local with `.env`), the tests run normally. #### GH workflows Check `.github/workflows/` to determine which workflow runs the affected integration tests. When adding a test directory, update the appropriate workflow so CI actually runs it. When adding integration tests that need a new service, either add them to an existing workflow or create a new one. The workflow must start the service and set the appropriate env vars. #### Environment files Add env vars for new integration tests to **both**: - **`.env.example`** — commented out, as reference for what's available - **`.env`** — with sensible local defaults so developers can uncomment and run locally See the existing entries for database, Redis, Meilisearch, and Typesense as examples. ## Static Analysis The `tests/` directory is excluded from phpstan. Do not run phpstan on tests. Run full PHPStan checks with `composer analyse`. During implementation, use targeted PHPStan only when investigating or validating a specific type issue. `phpstan.types.neon.dist` validates only the committed `types/` fixtures. Never pass source or test paths to it. **When fixing phpstan errors:** 1. **Investigate before coding.** For each error: read the code, check the Laravel equivalent's types (native and docblock), trace through callers and dependents. Report findings with the single, most correct fix. 2. **Don't make the code worse or more convoluted just to satisfy PHPStan.** Fix real issues in the code, but don't add awkward wrappers, fake branches, casts, or wider types just to silence PHPStan. A phpstan fix is a typing change: it must not change runtime behavior, add overhead, or introduce new edge cases. If you cannot satisfy PHPStan within those constraints, reach consensus on a compliant solution through a second-opinion loop with your peer agent. If the code is correct and PHPStan cannot understand it, follow the narrowing / suppression order below. 3. **Native types vs docblocks determine what's dead code.** If a native return type makes a guard unreachable, the guard is dead code — remove it. If only a docblock suggests always-true, the guard is legitimate runtime defense — leave it. 4. **Don't change contract/concrete boundaries to fix phpstan.** Swapping a contract for a concrete (or vice versa) to satisfy a type check diverges from Laravel's API. Only do this when Laravel's typing is genuinely incorrect. 5. **Methods can be added to contracts only if they represent behavior any conforming implementation must provide.** Implementation-specific methods, internal helpers, or driver-specific features don't belong on contracts — find another fix even if adding them would satisfy phpstan. 6. **Wrong docblock types should be fixed**, not suppressed. Check the actual runtime behavior (extension docs, reflection, tests) to determine the correct type. 7. **Type decisions must be evidence-based.** See Development Conventions — check Laravel/Hyperf signatures and docblocks, then trace real control flow. Don't guess. 8. **Narrowing / suppression order.** When the code is correct but PHPStan can't follow it, in order: (1) fix the type signature or docblock; (2) `@var` to narrow to the correct runtime type; (3) a line- or identifier-scoped `@phpstan-ignore` (e.g. magic `__call`/`__get` forwarding). Never use `assert()` to narrow types, and never add a neon-wide rule on your own (see #9). When a container string key is also a PHP class name, keep the canonical service key and use `@var` for its actual runtime type; do not change service resolution solely for PHPStan. 9. **Don't add patterns to `phpstan.neon.dist` on your own.** The neon file's global ignores cover fundamental framework patterns (Eloquent magic, generics, `new static`). Fix new phpstan errors at the source, not by masking them with new neon rules. Global suppression is a rare exception. First, reach consensus through a second-opinion loop with your peer agent that the error cannot be fixed at the source or narrowed locally, and that the pattern is frequent enough to justify global suppression. Then STOP, explain the reason and recommended suppression to the owner, and wait for approval before adding it. ## Porting Packages ### Policy When porting Laravel packages, whether first-party or third-party, keep them as close to 1:1 with upstream as possible so future changes are easy to merge. The exceptions are: - Modernizing PHP types, including native parameter, return, property, and class-constant types, plus other appropriate PHP 8.4+ features, strict types, and strict comparisons - Applying the class-import convention, including its exceptions - Converting mutable Laravel date construction to Hypervel's immutable date conventions, typing configurable factory output as `CarbonInterface`, and capturing date-modifier return values - Converting container array access (`$app['events']`) and dynamic service-property access (`$app->events`) in ported code to named container methods, and applying the Configuration rules to ported config and its consumers - Adding Laravel-style title docblocks to methods (not classes — see Development Conventions) - Formatting JavaScript, TypeScript, Vue and frontend CSS sources with the repository's Vite+ formatter configuration, preserving upstream logic, naming, structure and comments - For ported Laravel packages: making them coroutine-safe, adding Swoole performance enhancements (e.g., static property caching), making them pass PHPStan - Not porting upstream framework-specific integrations that only make sense in the source framework (for example packages, drivers) unless Hypervel intentionally has an equivalent surface - Not porting upstream mechanisms that do not make sense in Hypervel's stateful Swoole architecture (for example Laravel's deferred service provider machinery, where the upstream optimization only matters in a per-request bootstrap model) - Not porting deprecated upstream code or backwards-compatibility shims for versions/features Hypervel does not support — Hypervel is a new framework without Laravel's backwards-compatibility burden, so deprecated APIs and compatibility code that exist only to support older versions should be omitted rather than ported. Before changing or removing a deprecated public Laravel API, reach consensus on the recommended approach through a second-opinion loop with your peer agent. Then STOP, explain the proposed difference and its rationale to the owner, and wait for approval. Here, "upstream" means the framework or package being ported, not one of its dependencies — a Symfony deprecation does not make a Laravel API deprecated while Laravel still retains it. If a deprecated upstream surface still contains behavior that Hypervel actively needs, keep the behavior but move it onto the correct non-deprecated Hypervel-owned surface instead of porting the deprecated alias/wrapper as-is. - Do not retain dead branches or redundant guards for upstream parity. Verify they are unnecessary in supported Hypervel use from native types, real callers and framework guarantees. Check sibling occurrences independently; existing implementations and tests of unsupported states do not justify keeping them. - General performance improvements — reach consensus on the expected benefit, correctness, and complexity tradeoffs through a second-opinion loop with your peer agent before implementing. Hypervel has no obligation to preserve Hypervel-specific behavior from earlier versions, but supported Laravel APIs—including named arguments and protected extension points—must remain compatible unless the user approves a difference. Compatibility means preserving supported behavior, not merely matching names and signatures. Before recommending a departure, investigate the simplest compatible adaptation and reasonable alternatives, and establish the concrete obstacle. Resolve uncertainty through focused tests or prototypes; an assumed architectural conflict or peer agreement is not sufficient evidence. If preserving compatibility would compromise correctness, performance or scalability, or require disproportionate complexity, reach consensus on the cleanest design through a second-opinion loop with your peer agent. Then STOP, explain the proposed difference, supporting evidence and rationale to the owner, and wait for approval before changing it. When Laravel or a tracked upstream introduces an equivalent to a Hypervel-specific enhancement or fix, compare their capabilities, APIs, coroutine safety and performance, then reach consensus on how to reconcile them through a second-opinion loop with your peer agent. Normally prefer upstream when it meets Hypervel’s requirements. Before deprecating or removing the Hypervel-specific API, STOP, explain the recommendation to the owner, and wait for approval. Approved adaptations take precedence over upstream fidelity. Preserve Laravel upstream naming, structure, and style everywhere else. Hyperf is a historical reference rather than an ongoing merge target. For the rare Hyperf port, follow `docs/ai/porting-hyperf.md`. When working on a package, check its README for the upstream reference before making changes. Most Hypervel packages are ports of Laravel first-party or third-party ecosystem packages, such as Spatie packages. Most low-level Swoole infrastructure packages were originally ported from Hyperf, and a few packages are Hypervel-specific. Read `docs/ai/porting-hyperf.md` only when porting a Hyperf package or update. ### Global upstream exclusions Owner-approved exclusions that apply across packages. Package-specific differences belong in the package README. - Laravel Boost and package-provided Boost resources, including skills and guidelines. ### Source workflow #### 1. Package skeleton If the Hypervel version of the package doesn't exist yet, create the skeleton using an existing package as a template: - **Porting a Laravel first-party package:** Use the `cache` package as reference - **Porting a Hyperf package:** Use the `websocket-server` package as reference - **Porting a Laravel-ecosystem third-party package:** Use the `permission` package as a reference Read the reference package's `composer.json`, `LICENSE.md`, and `README.md` and create equivalents for the new package. Every package must be wired in both places: its own `src/{package}/composer.json` for the subtree split, and the root `composer.json` for monorepo development. Update autoloading, `replace`, and Hypervel provider / alias discovery metadata as needed, and add root dependencies with `composer require` — see Providers and Listeners for where providers should be registered. Create the README using the Package READMEs format under Development Conventions. #### 2. Port the files one at a time, alphabetically Check the source package to see what classes exist. Create a comprehensive todo list with a separate entry for each file to port. The porting process is: 1. Copy the file using `cp` — never read the source first and write a new version. Copying first then reading the copy avoids reading the file twice, which wastes context. 2. Read the ENTIRE copied file to understand context. For large files, read them in chunks. 3. Update namespaces and apply the Porting Policy's approved adaptations (modernized types, method docblocks, etc.). For Laravel ports, do not make unrelated naming, structural, or style changes; preserve upstream naming, structure, and style. For Hyperf ports, follow `docs/ai/porting-hyperf.md`. **For very large files where even reading in chunks is impractical** Update the file in chunks from top to bottom — read a chunk, update, read next chunk, update. Do NOT try to search for patterns and update scattered bits. #### 3. Update consumers Search **both `src/` and `tests/`** for any `use` statements or references to the old namespace (e.g., `Illuminate\Database\`) and update them to the new Hypervel namespace. Verify zero remaining references before proceeding. #### 4. Run phpstan After porting is complete, run phpstan on the newly ported package and fix errors. Investigate each error properly — don't reach for ignores without thinking it through. See the Static Analysis section. #### 5. Complete verification Complete the verification workflow under Change Workflow. For failures, follow When to Stop and Report: make straightforward fixes directly; for non-trivial failures, reach consensus on the root cause and fix through a second-opinion loop with your peer agent before implementing. ### Provider and listener reminder When ported code adds a provider or listener, wire providers and aliases in both Composer metadata locations and register listeners in the service provider's `boot()` method through closures that resolve them from the container. Follow the full rules under Providers and Listeners. ### Porting rules - **Preserve source constant/property/method order in Laravel ports** — when porting or merging methods into an existing Hypervel class, insert them in the same relative order as upstream. This keeps diffs meaningful and makes future merges easier. - **Preserve existing comments** — use the following rules for upstream code comments and docblocks: Do not remove or modify upstream code comments unless they are incorrect. Only remove `@param` and `@return` annotations where the description adds nothing beyond what the native type hint and parameter/method name already convey. Examples of removable: `@param string $name The name of the cookie` (just restates `string $name`), `@param int $offset Stream offset` (just restates `int $offset`). Examples to keep: `@param bool $secure Whether the cookie should only be transmitted over a secure HTTPS connection`, `@param int $whence Specifies how the cursor position will be calculated...`, `@return resource|null` (when the native type is `mixed` because `resource` isn't a valid PHP type hint). Keep everything else: behavioral descriptions, `@see` links, `@throws` annotations, warnings, contract explanations, usage notes. Modernize the title line to imperative form ("Returns" → "Return", "Retrieves" → "Retrieve") but do not remove or rewrite the body content beneath it. Translate non-English comments to English and fix grammar errors. - **Record intentionally omitted upstream features where future syncs will look** — When a Laravel feature is intentionally not ported because it does not fit Hypervel's Swoole/coroutine architecture, or because Hypervel has a better native equivalent, record it in three places so a future port cannot miss it: (1) the package README under `Differences From Laravel`, following the Package READMEs rules above and explaining what to use instead; (2) a concise source comment at the natural insertion point where the skipped method/class would otherwise sit; (3) a concise `REMOVED:` comment at the matching upstream test location when tests are skipped. This three-location requirement applies only to intentionally omitted methods or features, not ordinary ported-and-adapted code. Closed decisions only — real gaps still worth doing go in `docs/todo.md`. - **Replace framework names in code** — any occurrence of the word `laravel` or `hyperf` in ported code (string literals, comments, prefixes, identifiers, etc.) must be replaced with `hypervel`, preserving the original casing. For example: `laravel_reserved_` → `hypervel_reserved_`, `LaravelExcelExporter` → `HypervelExcelExporter`, `HYPERF_VERSION` → `HYPERVEL_VERSION`. This does not apply to namespaces (which have their own conversion rules) or to references that describe the upstream source (e.g., docblock `@see` links to Laravel/Hyperf source). - **Don't copy Laravel/Hyperf-specific framework details just to stay 1:1** — preserve public APIs and behavior while translating upstream-specific packages, providers, bootstrap mechanisms, and architecture to Hypervel equivalents. If no equivalent exists, reach consensus on the appropriate adaptation through a second-opinion loop with your peer agent before implementing. ### Test porting workflow Follow the same cp-then-edit process as source files. This workflow applies to both Hyperf and Laravel test porting. Laravel-specific conversions are covered in Porting Laravel Tests below; Hyperf-specific conversions (namespaces, license headers, container and error-handler mocking, NonCoroutine tests) are covered in `docs/ai/porting-hyperf.md`. Test file names and directory structure should mirror the source for both Laravel and Hyperf ports, providing a 1:1 class-to-test mapping. For Laravel ports, this also enables automated porting of upstream PRs. When both Hyperf and Laravel have tests covering the same class, merge them into one file — take the more comprehensive version as the base and add unique tests from the other. Consolidate overlapping Hypervel-specific tests under upstream test names and placement, preserving stronger assertions and distinct coverage. Keep every applicable test from the tracked upstream suite, even if redundant. #### 1. Audit source tests List all test files in the source package's `tests/` directory. For Laravel packages, also check `tests/Integration/{PackageName}/` — that's where Laravel puts its integration tests for each package. Note what each file covers. #### 2. Audit existing Hypervel tests (if any) Read all files in the existing Hypervel test directory for this package. Categorise them: - **Custom tests** (Hypervel-specific, no Hyperf/Laravel equivalent): Keep as-is - **Ported tests** (already ported from source): Keep — new source tests must be merged in #### 3. Create the todo list One entry per test file. Note the strategy: - **Copy and update** — no existing Hypervel test for this - **Merge** — Merge upstream tests into the existing Hypervel test file, preserving distinct Hypervel-specific coverage - **Integration** — needs external service, goes in `tests/Integration/{PackageName}/` - **Investigate** — exposes missing functionality, an unsupported feature, or an architectural difference. Reach consensus through a second-opinion loop with your peer agent on what the test covers, whether Hypervel should support it, and the appropriate fix or justified exclusion. #### 4. Port test files one at a time **For newly copied files (copy and update):** 1. Copy the file using `cp` to the correct location 2. Read the ENTIRE copied file to understand context 3. Update namespaces, base class, imports, types, docblocks, etc. **For merged files:** 1. Read BOTH the source file AND the existing Hypervel file 2. Merge source tests into the Hypervel file, preserving distinct Hypervel-specific coverage 3. Update namespaces, types, docblocks, etc. **For stub/helper files:** Copy `Stub/` directory files the same way. #### 5. Run tests after each file Use this exact cadence for each test class: 1. Port the test class. 2. Run that test class immediately (`./vendor/bin/phpunit --no-progress path/to/TestClass.php`). 3. Fix all straightforward failures. 4. If any failure exposes a source code bug, missing functionality, or unclear behavioral difference, investigate and reach consensus on the root cause and fix through a second-opinion loop with your peer agent before implementing. 5. Once the test class is green, move to the next test class. Work serially on one test class at a time. #### 6. Complete verification After all test files are ported, complete the verification workflow under Change Workflow. ## Porting Laravel Tests Most conversion work is applying general rules from Writing Tests — especially "Stricter typing", "When tests expose source code type errors", and "Helper class namespacing", since Laravel's loose typing and shared test namespaces hide problems that Hypervel exposes. The subsections below cover what is specific to Laravel ports. ### Namespace changes - Change `Illuminate\Tests\{Package}` to `Hypervel\Tests\{Package}` - Change all `Illuminate\` source imports to `Hypervel\` If Laravel's namespace includes the test class name, keep it. Stripping it causes "Cannot redeclare class" errors. ### Missing dependencies Some test files reference classes defined in other test files. Laravel gets away with this due to test suite load order. Make tests self-contained by defining required classes locally. ### Workbench fixtures from upstream packages Laravel packages sometimes ship a `workbench/` directory with controllers, models, middleware, and a `routes/web.php`. Hypervel's testbench workbench is shared across every package's tests, so port these into the package-scoped pattern: - **Controllers, models, middleware** → `tests/{Package}/Fixtures/...`, namespace `Hypervel\Tests\{Package}\Fixtures\...`. - **Routes** → `tests/{Package}/Fixtures/routes.php`. Load only from tests that need them (test setUp, or a small bootstrap script for CLI subprocesses). Never always-load. Update upstream test imports to point at the new Fixtures namespace. ### Approved unsupported features Tests for these features should be **removed** (not commented out) without asking — they will never be supported: - **Built-in database drivers:** SQL Server, MongoDB, DynamoDB are not provided. Hypervel ships MySQL, MariaDB, PostgreSQL, and SQLite drivers. - **Cache drivers:** Memcached, APC / APCu, DynamoDB, MongoDB - **Dynamic connections:** `DB::build()`, `DB::connectUsing()` — incompatible with Swoole connection pooling - **Container access:** ArrayAccess and dynamic service properties ### Laravel porting quick checklist 1. Update namespace to `Hypervel\Tests\{Package}` 2. Add `declare(strict_types=1);` 3. Change `Illuminate\` imports to `Hypervel\` 4. Extend correct base TestCase (`Hypervel\Tests\TestCase` or `Hypervel\Testbench\TestCase`) 5. Ensure `parent::setUp()` is called 6. Add type declarations to model properties 7. Fix mock types (PDO, QueryBuilder, Grammar, etc.) 8. Add `->andReturnSelf()` to chained method mocks 9. Use a test-specific namespace only when helper classes have generic, collision-prone names — already-specific helper names do not need extra namespace ceremony. 10. Remove tests only under the rules in Handling failing tests 11. Run tests and fix any remaining type errors