diff --git a/.env.example b/.env.example index d6a84f9..260f7b5 100644 --- a/.env.example +++ b/.env.example @@ -8,7 +8,6 @@ APP_CONTAINER_COMPILED_ACTIVATION=off ROUTER_MATCHER=fused -AUTH_IDS=random AUTH_STORAGE=memory AUTH_CACHE=array AUTH_PASSWORDS=native @@ -16,4 +15,4 @@ AUTH_TOKENS=simple AUTH_MFA=simple AUTH_NOTIFICATIONS=collect AUTH_PASSKEY=disabled -AUTH_TOKEN_SECRET=foundation-dev-secret +AUTH_TOKEN_SECRET= diff --git a/.gitattributes b/.gitattributes index 8d4d429..c28c300 100644 --- a/.gitattributes +++ b/.gitattributes @@ -33,12 +33,10 @@ /app/Services export-ignore /app/Support export-ignore /bin export-ignore -/bootstrap/cache/config/*.php export-ignore -/bootstrap/cache/console/*.php export-ignore -/bootstrap/cache/container.php export-ignore -/bootstrap/cache/modules.php export-ignore -/bootstrap/cache/optimize.php export-ignore -/bootstrap/cache/routes/*.php export-ignore +/bootstrap/cache/*.php export-ignore +/bootstrap/cache/config export-ignore +/bootstrap/cache/container export-ignore +/bootstrap/cache/routes export-ignore /database export-ignore /resources export-ignore /storage/app export-ignore diff --git a/.github/workflows/security-standards.yml b/.github/workflows/security-standards.yml index f412cfc..f956be5 100644 --- a/.github/workflows/security-standards.yml +++ b/.github/workflows/security-standards.yml @@ -9,37 +9,63 @@ on: branches: [ "main", "master", "develop", "development" ] jobs: - phpforge: - uses: infocyph/phpforge/.github/workflows/security-standards.yml@main - permissions: - security-events: write - actions: read - contents: read - with: - php_versions: '["8.4","8.5"]' - dependency_versions: '["prefer-lowest","prefer-stable"]' - php_extensions: "pcntl" - composer_flags: "" - phpstan_memory_limit: "1G" - psalm_threads: "1" - run_analysis: true - run_svg_report: true - fail_on_skipped_tests: true - run_clean_install: true - benchmark_composer_script: "" - benchmark_result_file: "" - benchmark_baseline_file: "" - benchmark_max_regression_percent: 2 - benchmark_stable_environment: false - enable_redis_service: false - enable_valkey_service: false - enable_memcached_service: false - enable_postgres_service: false - enable_mysql_service: false - enable_scylladb_service: false - enable_elasticsearch_service: false - enable_mongodb_service: false - service_db_name: "infbyte" - service_db_user: "infbyte" - service_db_password: "infbyte" - artifact_retention_days: 61 + integration-qa: + name: "Foundation 2 integration - PHP ${{ matrix.php-version }} - ${{ matrix.dependency-version }}" + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + php-version: ["8.4", "8.5"] + dependency-version: ["prefer-lowest", "prefer-stable"] + env: + XDEBUG_MODE: off + IC_PHPSTAN_MEMORY_LIMIT: "1G" + IC_PEST_FAIL_ON_SKIPPED: "true" + IC_TEST_TASK_TIMEOUT: "300" + steps: + - uses: actions/checkout@v7 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php-version }} + tools: composer:v2 + extensions: pcntl + coverage: none + + - name: Validate and resolve integration dependencies + shell: bash + run: | + composer validate --strict + composer update --no-interaction --prefer-dist --no-progress --${{ matrix.dependency-version }} + + - name: Run PHPForge quality suite + shell: bash + run: | + if [ "${{ matrix.dependency-version }}" = "prefer-lowest" ]; then + composer ic:ci --prefer-lowest + else + composer ic:ci + fi + + clean-install: + name: "Foundation 2 integration - clean install" + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: "8.5" + tools: composer:v2 + extensions: pcntl + coverage: none + + - name: Validate production install + shell: bash + run: | + composer validate --strict + composer update --no-dev --no-interaction --prefer-dist --no-progress --classmap-authoritative + composer check-platform-reqs --no-dev + php -r 'require "vendor/autoload.php";' diff --git a/.gitignore b/.gitignore index af056dd..f9c3dc0 100644 --- a/.gitignore +++ b/.gitignore @@ -9,9 +9,8 @@ /.route-cache-* /.vscode/ /.windsurf/ -/bootstrap/cache/container.php -/bootstrap/cache/modules.php -/bootstrap/cache/optimize.php +/bootstrap/cache/* +!/bootstrap/cache/.gitignore /database/*.sqlite /vendor/ *~ diff --git a/README.md b/README.md index 06d90de..149545c 100644 --- a/README.md +++ b/README.md @@ -2,10 +2,19 @@ Infbyte is the minimal application skeleton for [Infocyph Foundation](https://github.com/infocyph/Foundation). Foundation owns -the reusable framework runtime; Infbyte provides the application bootstrap, -configuration, routes, storage layout, and starter code. +the reusable framework/runtime layer; Infbyte provides opinionated application +bootstrap, starter configuration, routes, writable layout, and application code. -Infbyte 1.x requires PHP 8.4 or newer. +Infbyte requires PHP 8.4+. + +During Foundation 2.0 development this branch targets: + +```json +"infocyph/foundation": "dev-feature/foundation-2.0 as 2.0.x-dev" +``` + +The release constraint will move to the stable Foundation 2.0 line for the final +application release. ## Quick start @@ -15,146 +24,269 @@ cd my-app php infbyte serve ``` -The development server listens on `http://127.0.0.1:8000` by default. Select a -different address or port when needed: +The built-in server defaults to `127.0.0.1:8000` and is for local development +only: ```bash php infbyte serve --host=0.0.0.0 --port=8080 ``` -The built-in server is intended for local development only. Composer invokes -Foundation's `app:install` command to create `.env` from `.env.example` and -generate a unique authentication secret without replacing an existing one. +Composer invokes Foundation's `app:install` post-create command. The application +root executable remains `infbyte`; there is no separate Artisan/Console runtime. + +## Foundation runtime boundary + +Infbyte does not implement a framework layer above Foundation. + +The Web bootstrap is deliberately small: + +```php +use Infocyph\Foundation\Foundation; + +return Foundation::web([ + 'base_path' => dirname(__DIR__), +]); +``` + +Foundation's `CommandDispatcher` selects CLI, Worker, and Scheduler runtimes for +the root `infbyte` executable when commands require them. + +The four Foundation runtimes are: -The starter application exposes: +- Web +- CLI +- Worker +- Scheduler -- `GET /api/health` -- `GET /json` +Optional package providers remain lazy until configuration/code selects their +capability. -## Console +## CLI -Run `php infbyte list` to see every available command. Frequently used commands -include: +List the current command surface with: + +```bash +php infbyte list +``` + +Common families include: | Purpose | Commands | | --- | --- | -| Inspect the application | `about`, `env:show`, `config:show`, `route:list` | +| Inspect | `about`, `env:show`, `config:show`, `config:validate`, `route:list` | | Local development | `serve`, `create:*` | -| Optional capabilities | `module:list`, `module:install`, `module:remove` | -| Database work | `db:show`, `db:table`, `db:seed`, `migrate*` | -| Runtime maintenance | `app:install`, `cache:clear`, `storage:link`, `secret:generate` | -| Background work | `schedule:*`, `worker:*`, `queue:consume` | -| Deployment | `optimize`, `optimize:clear`, `app:ready` | +| Modules | `module:list`, `module:show`, `module:install`, `module:remove`, `module:config:publish`, `module:schema:*` | +| Database | `db:*`, `migrate*` | +| Operations | `execution:*`, `maintenance:*`, `runtime:reload`, `log:tail` | +| Background work | `schedule:*`, `worker:*`, `queue:*`, `messaging:list` | +| Storage/session/auth | `storage:*`, `session:prune`, `auth:prune` | +| Environment protection | `env:encrypt`, `env:decrypt` | +| Deployment | `optimize`, `optimize:clear`, `optimize:report`, `app:ready` | + +Global controls include `--json`, `-q|--quiet`, `--silent`, `-v|-vv|-vvv`, +`--profile`, `--env`, and `-n|--no-interaction`. + +Application commands are registered explicitly in `routes/console.php`, +schedules in `routes/schedule.php`, and non-message maintenance workers in +`routes/workers.php`. -Some commands become usable only after their corresponding optional module is -installed. Missing capabilities report the exact installation command. +## Application generators -Create application artifacts without adding unused runtime services: +Foundation generators create application starting points without silently +editing registration/configuration: ```bash php infbyte create:controller Admin/User php infbyte create:command Reports/Daily +php infbyte create:request StoreUser +php infbyte create:rule ValidVatNumber +php infbyte create:resource User +php infbyte create:job GenerateReport +php infbyte create:handler GenerateReport +php infbyte create:job-middleware AuditJob +php infbyte create:mail Welcome +php infbyte create:notification PasswordChanged +php infbyte create:notification-channel Sms php infbyte create:repository User -php infbyte create:worker Queue -php infbyte create:test Http/UserAccess +php infbyte create:migration CreateUsers +php infbyte create:seeder Production +php infbyte create:worker Metrics ``` -Existing files are preserved unless `--force` is supplied. Register application -commands explicitly in `routes/console.php`, schedules in `routes/schedule.php`, -and supervised workers in `routes/workers.php`. +Existing files are preserved unless `--force` is supplied. -For an intentionally static controller action, use a first-class callable such -as `Route::get('/reports', ReportsController::show(...))`; route caching converts -that safe form to a plain descriptor. Captured closures and instance handlers -remain supported through the general cached-handler path. +## Purpose-first optional modules -## Optional modules - -A new project installs only the core runtime. Add or publish capabilities as the -application needs them: +A new application keeps only the Foundation core runtime. Add capabilities as +they are needed: ```bash php infbyte module:list -php infbyte module:install db +php infbyte module:install database php infbyte module:install cache +php infbyte module:install communication php infbyte module:install messaging -php infbyte module:install session +php infbyte module:config:publish operations ``` -Available modules include database, cache, communication, cryptography, -filesystem, logging, messaging, OTP, passkeys, JSON resources, sessions, and -validation. Installation publishes documented configuration without activating -global middleware, providers, connections, workers, or sessions. Optional -services remain lazy until selected by application code, a route, or a command. +Canonical modules are: + +- `auth` +- `cache` +- `communication` +- `database` +- `filesystem` +- `logging` (built in) +- `messaging` +- `operations` (built in) +- `resources` (built in) +- `security` +- `session` (built in) +- `validation` + +Aliases such as `db`, `crypto`, `otp`, `passkeys`, and `queue` remain accepted, +but the application documentation uses purpose names. OTP and WebAuthn are +implementations inside the `auth` module rather than standalone public modules. + +Installing a module does not add global middleware, open connections, or start +workers. Optional config is published only when requested/needed and remains +outside the lean checked-in skeleton by default. + +## Module schema lifecycle + +Capability-owned schemas use one command family: + +```bash +php infbyte module:schema:status auth +php infbyte module:schema:install auth +php infbyte module:schema:status cache +php infbyte module:schema:install session +php infbyte module:schema:sync +``` + +The `database` module owns DB/migration infrastructure; it does not own arbitrary +application tables. Removing a module never drops schemas/application data. ## Application structure -- `app/` contains application controllers, commands, and generated code. -- `bootstrap/` contains the isolated web and console composition roots. -- `config/` contains application-owned configuration. -- `routes/` contains HTTP, command, schedule, and worker mappings. -- `public/` contains the web entry point. -- `storage/` contains writable runtime data. -- `tests/Feature/` and `tests/Unit/` contain starter Pest examples. +The starter intentionally stays small: + +```text +app/ +bootstrap/ + app.php + providers.php + cache/ +config/ + app.php + auth.php + router.php +public/ +routes/ + api.php + console.php + schedule.php + workers.php +storage/ +tests/ +composer.json +infbyte +``` + +Only application-default config is checked in. Cache/database/filesystem/ +messaging/operations/security/session/validation/communication config belongs +to optional module publication rather than being bulk-copied into every new +project. -Web requests boot only the HTTP path. CLI requests boot only the command -capabilities they require; the ordinary web path does not load console command, -schedule, or worker definitions. +## Database -## Testing +```bash +php infbyte module:install database +php infbyte migrate +php infbyte migrate --pretend +php infbyte migrate:status +php infbyte db:monitor --section=status +``` + +Application migrations are registered explicitly under +`database.migrations.classes`; Foundation does not scan migration directories. -The generated project includes one feature test and one unit test: +## Messaging and workers + +Omnibus-backed messaging is optional: ```bash -composer ic:tests +php infbyte module:install messaging +php infbyte worker:list +php infbyte worker:run reports +php infbyte worker:status reports +php infbyte queue:failed ``` -Foundation provides a native Webrick HTTP test client, as demonstrated by -`tests/Feature/ExampleTest.php`. Infbyte's repository-only release tests are -excluded from Composer-created projects. +Foundation does not expose a parallel messaging manager. Application services +resolve native Omnibus `MessageBus`, `EventDispatcher`, and related APIs through +DI. + +`routes/workers.php` is only for application maintenance workers; queue worker +loops remain Omnibus-owned. + +## Runtime operations + +Useful production/runtime commands include: + +```bash +php infbyte config:validate --production +php infbyte maintenance:status +php infbyte runtime:reload +php infbyte worker:restart +php infbyte schedule:interrupt +php infbyte storage:status +php infbyte log:tail --follow +``` + +Runtime generation commands request graceful shutdown. Foundation does not +replace Supervisor/systemd/Docker/Kubernetes process supervision. ## Production -Compile application metadata before serving production traffic: +Build deployment-owned optimized artifacts before serving production traffic: ```bash php infbyte optimize +php infbyte optimize:report php infbyte app:ready ``` -`optimize` compiles configuration, route, command, schedule, and module metadata -plus the eligible HTTP container graph so requests do less work. It publishes a -final optimize manifest only after the complete set is ready. `optimize:clear` -removes every generated artifact and leaves uncached execution available. +Clear generated artifacts with: -`APP_CONFIG_CACHE_TYPE=single` loads one snapshot; `sharded` loads configuration -namespaces on demand. Neither is universally faster: measure the application's -minimal, authenticated, and database-backed routes before selecting one. -Compiled-container activation remains `off` by default because small requests -may not amortize its boot cost; use `always` only for a measured deployment, -commonly a persistent worker with OPcache. +```bash +php infbyte optimize:clear +``` -Webrick automatically selects the response emitter. A known deployment may set -`WEBRICK_EMITTER` to `fpm`, `frankenphp`, `lsapi`, `unit`, `swoole`, -`roadrunner`, or `workerman`; leave it unset when runtime detection is desired. +Compiled config/route/command/schedule/container artifacts belong to deployment +and are ignored by the skeleton repository. -The included deployment helper validates writable runtime paths and builds the -same caches: +Before deployment: -```bash -./deploy.sh -``` +- set production environment/debug policy; +- configure only the modules the application actually uses; +- run application migrations; +- provision applicable module schemas; +- require `config:validate --production` / `app:ready` to pass; +- use a production web server and external process manager. + +## Testing and release checks -Before deployment, set `APP_ENV=production`, disable debug output, configure -installed modules, apply required migrations, and require `app:ready` to pass. +The current skeleton Composer file does not invent generic test/release script +aliases. Run the tools/scripts actually installed by the application/release +candidate. Foundation's full Composer/PHPForge/static/PHPUnit/integration and +performance matrix is performed in its dedicated release-verification phase. ## Documentation -- [Foundation](https://github.com/infocyph/Foundation/tree/main/docs) -- [Console](https://github.com/infocyph/Console/tree/main/docs) -- [Omnibus](https://github.com/infocyph/Omnibus/tree/main/docs) -- [Webrick](https://docs.infocyph.com/projects/webrick/en/latest/) -- [JsonDispatch](https://docs.infocyph.com/projects/json-dispatch/) +- [Foundation documentation](https://github.com/infocyph/Foundation/tree/feature/foundation-2.0/docs) +- [Omnibus](https://github.com/infocyph/Omnibus) +- [Webrick](https://github.com/infocyph/Webrick) -Detailed framework and package documentation will expand separately; this -README intentionally remains a concise application quick start. +Infbyte keeps framework details in Foundation documentation rather than copying +them into the application skeleton. diff --git a/app/Console/Commands/.gitkeep b/app/Console/Commands/.gitkeep deleted file mode 100644 index 8b13789..0000000 --- a/app/Console/Commands/.gitkeep +++ /dev/null @@ -1 +0,0 @@ - diff --git a/bootstrap/app.php b/bootstrap/app.php index f5c5e5f..049da00 100644 --- a/bootstrap/app.php +++ b/bootstrap/app.php @@ -4,7 +4,6 @@ use Infocyph\Foundation\Foundation; -/** @var array $options */ -$options = require __DIR__ . '/options.php'; - -return Foundation::web($options); +return Foundation::web([ + 'base_path' => dirname(__DIR__), +]); diff --git a/bootstrap/cache/config/.gitignore b/bootstrap/cache/.gitignore similarity index 100% rename from bootstrap/cache/config/.gitignore rename to bootstrap/cache/.gitignore diff --git a/bootstrap/cache/console/.gitignore b/bootstrap/cache/console/.gitignore deleted file mode 100644 index d6b7ef3..0000000 --- a/bootstrap/cache/console/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -* -!.gitignore diff --git a/bootstrap/cache/routes/.gitignore b/bootstrap/cache/routes/.gitignore deleted file mode 100644 index d6b7ef3..0000000 --- a/bootstrap/cache/routes/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -* -!.gitignore diff --git a/bootstrap/console.php b/bootstrap/console.php deleted file mode 100644 index 55734cd..0000000 --- a/bootstrap/console.php +++ /dev/null @@ -1,10 +0,0 @@ - $options */ -$options = require __DIR__ . '/options.php'; - -return Foundation::console($options); diff --git a/bootstrap/options.php b/bootstrap/options.php deleted file mode 100644 index e434e74..0000000 --- a/bootstrap/options.php +++ /dev/null @@ -1,9 +0,0 @@ - $basePath, -]; diff --git a/bootstrap/providers.php b/bootstrap/providers.php index 70ceb85..4b0a029 100644 --- a/bootstrap/providers.php +++ b/bootstrap/providers.php @@ -15,10 +15,22 @@ * \App\Providers\WebServiceProvider::class, */ ], - 'console' => [ + 'cli' => [ /** * Usage: - * \App\Providers\ConsoleServiceProvider::class, + * \App\Providers\CliServiceProvider::class, + */ + ], + 'worker' => [ + /** + * Usage: + * \App\Providers\WorkerServiceProvider::class, + */ + ], + 'scheduler' => [ + /** + * Usage: + * \App\Providers\SchedulerServiceProvider::class, */ ], ]; diff --git a/composer.json b/composer.json index 08f0a6d..514e548 100644 --- a/composer.json +++ b/composer.json @@ -4,8 +4,8 @@ "license": "MIT", "type": "project", "require": { - "php": ">=8.4", - "infocyph/foundation": "^1.3" + "php": "^8.4", + "infocyph/foundation": "dev-feature/foundation-2.0 as 2.0.x-dev" }, "require-dev": { "infocyph/phpforge": "dev-main@dev" diff --git a/config/app.php b/config/app.php index 8c5ecc6..ed119fc 100644 --- a/config/app.php +++ b/config/app.php @@ -8,64 +8,50 @@ | Application Identity |-------------------------------------------------------------------------- | - | "name" labels the application in diagnostics and integrations. "env" - | selects environment-sensitive defaults, "debug" enables development - | diagnostics, and "url" is the canonical base URL used to build links. - | Names and environments are free-form strings, for example `Acme API` and - | `staging`; debug accepts `true|false`; URL example: `https://api.acme.test`. + | Infbyte owns application-facing defaults while Foundation owns the runtime + | implementation. "name" labels diagnostics and integrations, "env" selects + | environment-sensitive policy, "debug" enables development diagnostics, + | and "url" is the application's canonical external base URL. | */ - 'name' => env('APP_NAME', 'Infbyte'), - 'env' => env('APP_ENV', 'local'), - 'debug' => env('APP_DEBUG', true), - 'url' => env('APP_URL', 'http://localhost'), + 'name' => env_string('APP_NAME', 'Infbyte'), + 'env' => env_string('APP_ENV', 'local'), + 'debug' => env_bool('APP_DEBUG', true), + 'url' => env_string('APP_URL', 'http://localhost'), /* |-------------------------------------------------------------------------- | Configuration Cache |-------------------------------------------------------------------------- | - | "type" accepts "sharded" or "single". Sharded caches load namespaces - | on demand; single caches load one compiled snapshot. Build either form - | during deployment with `php infbyte config:cache`. Allowed values: - | `sharded|single`. + | Sharded config remains the normal application default so namespaces stay + | lazy. Build the selected artifact explicitly during deployment. + | Allowed values: sharded|single. | */ 'config_cache' => [ - 'type' => env('APP_CONFIG_CACHE_TYPE', 'sharded'), + 'type' => env_string('APP_CONFIG_CACHE_TYPE', 'sharded'), ], /* |-------------------------------------------------------------------------- - | Dependency Injection Container + | InterMix Container |-------------------------------------------------------------------------- | - | "alias" optionally names a configured container profile. "environment" - | controls environment-aware definitions. "lazy_loading" defers supported - | services, while "request_scope" isolates request-lived entries. - | - | "compiled" selects the application-owned resolver artifact. Activation - | remains off unless a measured deployment explicitly selects "always". - | Debug tracing is disabled by default; "level" selects its detail. - | - | Alias/environment examples: `http` and `production`. Boolean switches use - | `true|false`. The compiled artifact defaults to - | `bootstrap/cache/container.php`; APP_CONTAINER_COMPILED may select another - | application-owned relative or absolute path. - | Compiled activation values: `off|always`. - | Trace levels: `off|node|info|warn|warning|error|verbose`. + | Foundation 2.0 is lazy by default and owns execution scopes directly. + | There is no application request_scope switch. Compiled container activation + | remains opt-in until a deployment deliberately builds and enables it. | */ 'container' => [ 'alias' => env('APP_CONTAINER_ALIAS'), - 'environment' => env('APP_ENV', 'local'), - 'lazy_loading' => env('APP_CONTAINER_LAZY_LOADING', false), - 'request_scope' => env('APP_CONTAINER_REQUEST_SCOPE', true), - 'compiled' => env('APP_CONTAINER_COMPILED', 'bootstrap/cache/container.php'), - 'compiled_activation' => env('APP_CONTAINER_COMPILED_ACTIVATION', 'off'), + 'environment' => env_string('APP_ENV', 'local'), + 'lazy_loading' => env_bool('APP_CONTAINER_LAZY_LOADING', true), + 'compiled' => env_string('APP_CONTAINER_COMPILED', 'bootstrap/cache/container.php'), + 'compiled_activation' => env_string('APP_CONTAINER_COMPILED_ACTIVATION', 'off'), 'debug_tracing' => [ - 'enabled' => env('APP_CONTAINER_DEBUG_TRACING', false), - 'level' => env('APP_CONTAINER_DEBUG_TRACE_LEVEL', 'node'), + 'enabled' => env_bool('APP_CONTAINER_DEBUG_TRACING', false), + 'level' => env_string('APP_CONTAINER_DEBUG_TRACE_LEVEL', 'node'), ], ], ]; diff --git a/config/auth.php b/config/auth.php index d8664ce..3d7e002 100644 --- a/config/auth.php +++ b/config/auth.php @@ -8,25 +8,34 @@ | Authentication Drivers |-------------------------------------------------------------------------- | - | Each key selects the implementation for one authentication capability. - | IDs: `random|uid`. Storage: `memory|database`. Cache: - | `array|cache`. Passwords: `native|security`. Tokens: - | `simple|security`. MFA: `simple|otp`. Notifications: - | `collect|talkingbytes`. Passkeys: `disabled|memory|webauthn`. + | Foundation owns identity generation through UID. These switches select + | only replaceable authentication capabilities. The dependency-light + | defaults work without optional modules. | - | The self-contained defaults keep auth adapters optional. Production auth - | must install and explicitly select the durable modules it requires. + | Optional choices map to purpose-level modules: + | storage=database -> module:install database + | cache=cache -> module:install cache + | passwords/tokens=security -> module:install security + | mfa=otp or passkey=webauthn -> module:install auth + | notifications=talkingbytes -> module:install communication + | + | Module installation also synchronizes database schemas required by the + | active configuration. Database-backed authentication uses the auth schema; + | inspect or provision it explicitly with `module:schema:status auth` and + | `module:schema:install auth` when preparing persistence ahead of activation. + | + | OTP and WebAuthn remain independently selectable even though they share + | the same extended-auth module and installation bundle. | */ 'drivers' => [ - 'ids' => env('AUTH_IDS', 'random'), - 'storage' => env('AUTH_STORAGE', 'memory'), - 'cache' => env('AUTH_CACHE', 'array'), - 'passwords' => env('AUTH_PASSWORDS', 'native'), - 'tokens' => env('AUTH_TOKENS', 'simple'), - 'mfa' => env('AUTH_MFA', 'simple'), - 'notifications' => env('AUTH_NOTIFICATIONS', 'collect'), - 'passkey' => env('AUTH_PASSKEY', 'disabled'), + 'storage' => env_string('AUTH_STORAGE', 'memory'), + 'cache' => env_string('AUTH_CACHE', 'array'), + 'passwords' => env_string('AUTH_PASSWORDS', 'native'), + 'tokens' => env_string('AUTH_TOKENS', 'simple'), + 'mfa' => env_string('AUTH_MFA', 'simple'), + 'notifications' => env_string('AUTH_NOTIFICATIONS', 'collect'), + 'passkey' => env_string('AUTH_PASSKEY', 'disabled'), ], /* @@ -34,11 +43,8 @@ | Token Signing Secret |-------------------------------------------------------------------------- | - | This secret protects authentication tokens. It has no config-file - | default. Local runtime may supply its development-only fallback, while - | production rejects missing or short values. Set AUTH_TOKEN_SECRET to a - | unique high-entropy secret of at least 32 bytes and never commit it. - | Example format: a randomly generated 64-character hexadecimal string. + | app:install generates this value for a new application. Production must + | use unique high-entropy secret material and must never commit it. | */ 'token_secret' => env('AUTH_TOKEN_SECRET'), @@ -48,38 +54,29 @@ | One-Time Passwords |-------------------------------------------------------------------------- | - | "issuer" is shown by authenticator applications. "freshness_window" is - | the number of seconds an MFA result remains recent. TOTP "algorithm", - | `digits`, `period`, and `secret_bytes` define provisioning parameters. - | "window" is the accepted number of adjacent time steps. - | - | Recovery-code "count" and "length" control the generated backup set. - | Replay protection records consumed codes when "enabled" and retains each - | marker for "ttl" seconds. Keep replay protection enabled in production. - | - | Issuer example: `Acme`. Algorithm: `sha1|sha256|sha512`. Digits: `4..10`. - | Period: `1..86400` seconds. Window: `0..100` time steps. Secret bytes, - | recovery count/length, freshness, and replay TTL are positive integers. - | Typical values respectively are 64, 10, 10, 900, and 90. Replay accepts - | `true|false`. + | OTP-backed MFA is provided by the auth module. Enable it with AUTH_MFA=otp + | after installing the module. Replay-safe production use also requires the + | configured shared cache/coordination capability. | */ 'otp' => [ - 'issuer' => env('AUTH_OTP_ISSUER', env('APP_NAME', 'Infbyte')), - 'freshness_window' => env_int('AUTH_OTP_FRESHNESS_WINDOW', 900), + 'issuer' => env_string('AUTH_OTP_ISSUER', env_string('APP_NAME', 'Infbyte')), + 'hotp' => [ + 'look_ahead' => env_int('AUTH_OTP_HOTP_LOOK_AHEAD', 5), + ], 'totp' => [ - 'algorithm' => env('AUTH_OTP_ALGORITHM', 'sha1'), + 'algorithm' => env_string('AUTH_OTP_ALGORITHM', 'sha1'), 'digits' => env_int('AUTH_OTP_DIGITS', 6), 'period' => env_int('AUTH_OTP_PERIOD', 30), - 'secret_bytes' => env_int('AUTH_OTP_SECRET_BYTES', 64), + 'secret_bytes' => env_int('AUTH_OTP_SECRET_BYTES', 20), 'window' => env_int('AUTH_OTP_WINDOW', 1), ], 'recovery_codes' => [ 'count' => env_int('AUTH_OTP_RECOVERY_CODES', 10), - 'length' => env_int('AUTH_OTP_RECOVERY_LENGTH', 10), + 'length' => env_int('AUTH_OTP_RECOVERY_LENGTH', 12), ], 'replay' => [ - 'enabled' => env('AUTH_OTP_REPLAY', true), + 'store' => env('AUTH_OTP_REPLAY_STORE'), 'ttl' => env_int('AUTH_OTP_REPLAY_TTL', 90), ], ], @@ -89,25 +86,17 @@ | WebAuthn / Passkeys |-------------------------------------------------------------------------- | - | "rp_id", "rp_name", and "origin" identify the relying party and must - | match the deployed domain. `attestation` controls authenticator evidence. - | "user_verification" and "resident_key" use WebAuthn preference values. - | "algorithms" lists accepted COSE algorithms and "transports" lists the - | authenticator transports advertised during registration. - | - | RP ID example: `example.com`; RP name example: `Acme`; origin example: - | `https://example.com`. Attestation: `none|direct|indirect|enterprise`. - | User verification and resident key: `required|preferred|discouraged`. - | Algorithms: `ES256|RS256`. Transports: `internal|hybrid|usb|nfc|ble`. + | WebAuthn is provided by the auth module. Enable it with + | AUTH_PASSKEY=webauthn after installing that module. | */ 'webauthn' => [ 'rp_id' => env('WEBAUTHN_RP_ID'), - 'rp_name' => env('WEBAUTHN_RP_NAME', 'Infbyte'), + 'rp_name' => env_string('WEBAUTHN_RP_NAME', env_string('APP_NAME', 'Infbyte')), 'origin' => env('WEBAUTHN_ORIGIN'), - 'attestation' => 'none', - 'user_verification' => env('WEBAUTHN_USER_VERIFICATION', 'preferred'), - 'resident_key' => env('WEBAUTHN_RESIDENT_KEY', 'preferred'), + 'attestation' => env_string('WEBAUTHN_ATTESTATION', 'none'), + 'user_verification' => env_string('WEBAUTHN_USER_VERIFICATION', 'preferred'), + 'resident_key' => env_string('WEBAUTHN_RESIDENT_KEY', 'preferred'), 'algorithms' => ['ES256', 'RS256'], 'transports' => ['internal', 'hybrid', 'usb', 'nfc', 'ble'], ], diff --git a/config/ids.php b/config/ids.php deleted file mode 100644 index cc4e190..0000000 --- a/config/ids.php +++ /dev/null @@ -1,208 +0,0 @@ - env('IDS_DEFAULT', 'uuid7'), - - /* - |-------------------------------------------------------------------------- - | Shared Sequence Coordination - |-------------------------------------------------------------------------- - | - | "driver" selects the sequence backend and "directory" stores filesystem - | sequence state. "wait_time" is the lock retry delay in microseconds and - | "max_attempts" bounds acquisition attempts. Distributed deployments must - | use sequence state that is shared by every participating node. - | Drivers: `file|filesystem|memory`. Directory example: `cache/ids`. - | Wait/max-attempt examples: `1000` microseconds and `1000` attempts. - | - */ - 'sequence' => [ - 'driver' => env('IDS_SEQUENCE_DRIVER', 'filesystem'), - 'directory' => env_string('IDS_SEQUENCE_DIRECTORY', 'cache/ids'), - 'wait_time' => env_int('IDS_SEQUENCE_WAIT_TIME', 1000), - 'max_attempts' => env_int('IDS_SEQUENCE_MAX_ATTEMPTS', 1000), - ], - - /* - |-------------------------------------------------------------------------- - | ULID, NanoID And CUID2 - |-------------------------------------------------------------------------- - | - | ULID "mode" controls random or monotonic ordering. NanoID and CUID2 - | "length" values control their textual output size and collision budget. - | ULID modes: `monotonic|random`. NanoID length: `1..1048576`, example `21`. - | CUID2 length: `2..32`, example `24`. - | - */ - 'ulid' => [ - 'mode' => env('IDS_ULID_MODE', 'monotonic'), - ], - 'nanoid' => [ - 'length' => env_int('IDS_NANOID_LENGTH', 21), - ], - 'cuid2' => [ - 'length' => env_int('IDS_CUID2_LENGTH', 24), - ], - - /* - |-------------------------------------------------------------------------- - | Opaque And Deterministic Identifiers - |-------------------------------------------------------------------------- - | - | Opaque IDs use "length" for output size and "salt" for application-level - | separation. Deterministic IDs use "length" and "namespace" to keep equal - | inputs isolated by purpose. Use high-entropy secrets where the selected - | generator treats a salt as confidential. Opaque length: `1..1024`, example - | `12`; salt example: a random application-specific string. Deterministic - | length: `1..43`, example `24`; namespace example: `invoice`. - | - */ - 'opaque' => [ - 'length' => env_int('IDS_OPAQUE_LENGTH', 12), - 'salt' => env_string('IDS_OPAQUE_SALT', ''), - ], - 'deterministic' => [ - 'length' => env_int('IDS_DETERMINISTIC_LENGTH', 24), - 'namespace' => env_string('IDS_DETERMINISTIC_NAMESPACE', 'default'), - ], - - /* - |-------------------------------------------------------------------------- - | Snowflake Identifiers - |-------------------------------------------------------------------------- - | - | "datacenter_id" and "worker_id" must uniquely identify the generator. - | "custom_epoch" sets the timestamp origin, "clock_backward_policy" controls - | rollback handling, and "output" selects the returned representation. - | Sequence "driver" and "directory" override the shared defaults above. - | Datacenter/worker examples: `3` and `17`; epoch examples: Unix milliseconds - | or `2024-01-01T00:00:00Z`. Clock policy: `wait|throw`. Output: - | `string|int|binary`. Sequence drivers: `file|filesystem|memory`. - | - */ - 'snowflake' => [ - 'datacenter_id' => env_int('IDS_SNOWFLAKE_DATACENTER_ID', 0), - 'worker_id' => env_int('IDS_SNOWFLAKE_WORKER_ID', 0), - 'custom_epoch' => env('IDS_SNOWFLAKE_CUSTOM_EPOCH'), - 'clock_backward_policy' => env('IDS_SNOWFLAKE_CLOCK_BACKWARD_POLICY', 'wait'), - 'output' => env('IDS_SNOWFLAKE_OUTPUT', 'string'), - 'sequence' => [ - 'driver' => env('IDS_SNOWFLAKE_SEQUENCE_DRIVER', env('IDS_SEQUENCE_DRIVER', 'filesystem')), - 'directory' => env_string('IDS_SNOWFLAKE_SEQUENCE_DIRECTORY', env_string('IDS_SEQUENCE_DIRECTORY', 'cache/ids')), - ], - ], - - /* - |-------------------------------------------------------------------------- - | Sonyflake Identifiers - |-------------------------------------------------------------------------- - | - | "machine_id" must be unique among active generators. "custom_epoch", - | "clock_backward_policy", and "output" control time origin, rollback - | handling, and representation. Sequence keys override shared coordination. - | Machine example: `42`; epoch example: `2024-01-01T00:00:00Z`; clock policy: - | `wait|throw`; output: `string|int|binary`; sequence driver: - | `file|filesystem|memory`; directory example: `cache/ids`. - | - */ - 'sonyflake' => [ - 'machine_id' => env_int('IDS_SONYFLAKE_MACHINE_ID', 0), - 'custom_epoch' => env('IDS_SONYFLAKE_CUSTOM_EPOCH'), - 'clock_backward_policy' => env('IDS_SONYFLAKE_CLOCK_BACKWARD_POLICY', 'wait'), - 'output' => env('IDS_SONYFLAKE_OUTPUT', 'string'), - 'sequence' => [ - 'driver' => env('IDS_SONYFLAKE_SEQUENCE_DRIVER', env('IDS_SEQUENCE_DRIVER', 'filesystem')), - 'directory' => env_string('IDS_SONYFLAKE_SEQUENCE_DIRECTORY', env_string('IDS_SEQUENCE_DIRECTORY', 'cache/ids')), - ], - ], - - /* - |-------------------------------------------------------------------------- - | TBSL Identifiers - |-------------------------------------------------------------------------- - | - | "machine_id" identifies the node, "sequenced" enables coordinated values, - | "clock_backward_policy" defines rollback behavior, and "output" selects - | representation. Sequence "driver" and "directory" are profile overrides. - | A machine ID may be 42. Sequenced accepts `true|false`; clock policy accepts - | `wait|throw`; output accepts `string|int|binary`; and sequence driver accepts - | `file|filesystem|memory`. A sequence directory may be `cache/ids`. - | - */ - 'tbsl' => [ - 'machine_id' => env_int('IDS_TBSL_MACHINE_ID', 0), - 'sequenced' => env_bool('IDS_TBSL_SEQUENCED', false), - 'clock_backward_policy' => env('IDS_TBSL_CLOCK_BACKWARD_POLICY', 'wait'), - 'output' => env('IDS_TBSL_OUTPUT', 'string'), - 'sequence' => [ - 'driver' => env('IDS_TBSL_SEQUENCE_DRIVER', env('IDS_SEQUENCE_DRIVER', 'filesystem')), - 'directory' => env_string('IDS_TBSL_SEQUENCE_DIRECTORY', env_string('IDS_SEQUENCE_DIRECTORY', 'cache/ids')), - ], - ], - - /* - |-------------------------------------------------------------------------- - | Randflake Identifiers - |-------------------------------------------------------------------------- - | - | "node_id" identifies the generator. "lease_start" and "lease_end" bound - | its allocated range; "secret" protects generator-specific derivation and - | must be replaced in production. "output" selects representation, while - | sequence "driver" and "directory" override shared coordination settings. - | Node example: `7`; lease example: `100000..199999`; secret example: a - | random 32-byte Base64 string; output: `string|int|binary`; sequence driver: - | `file|filesystem|memory`; directory example: `cache/ids`. - | - */ - 'randflake' => [ - 'node_id' => env_int('IDS_RANDFLAKE_NODE_ID', 0), - 'lease_start' => env_int('IDS_RANDFLAKE_LEASE_START', 0), - 'lease_end' => env_int('IDS_RANDFLAKE_LEASE_END', 0), - 'secret' => env_string('IDS_RANDFLAKE_SECRET', 'change-me'), - 'output' => env('IDS_RANDFLAKE_OUTPUT', 'string'), - 'sequence' => [ - 'driver' => env('IDS_RANDFLAKE_SEQUENCE_DRIVER', env('IDS_SEQUENCE_DRIVER', 'filesystem')), - 'directory' => env_string('IDS_RANDFLAKE_SEQUENCE_DIRECTORY', env_string('IDS_SEQUENCE_DIRECTORY', 'cache/ids')), - ], - ], - - /* - |-------------------------------------------------------------------------- - | Authentication Identifier Purposes - |-------------------------------------------------------------------------- - | - | Each key selects the generator for that auth record type: accounts, audit - | events, challenges, correlations, credentials, devices, grants, - | permissions, roles, and sessions. Keep persisted types stable after data - | exists unless a migration explicitly handles the representation change. - | Every value accepts one of the generator names listed under Default - | Identifier. Example: account `uuid7`, correlation `ulid`, session `uuid7`. - | - */ - 'auth' => [ - 'account' => env('IDS_AUTH_ACCOUNT', 'uuid7'), - 'audit_event' => env('IDS_AUTH_AUDIT_EVENT', 'uuid7'), - 'challenge' => env('IDS_AUTH_CHALLENGE', 'uuid7'), - 'correlation' => env('IDS_AUTH_CORRELATION', 'ulid'), - 'credential' => env('IDS_AUTH_CREDENTIAL', 'uuid7'), - 'device' => env('IDS_AUTH_DEVICE', 'uuid7'), - 'grant' => env('IDS_AUTH_GRANT', 'uuid7'), - 'permission' => env('IDS_AUTH_PERMISSION', 'uuid7'), - 'role' => env('IDS_AUTH_ROLE', 'uuid7'), - 'session' => env('IDS_AUTH_SESSION', 'uuid7'), - ], -]; diff --git a/config/router.php b/config/router.php index f682dd6..879bf1f 100644 --- a/config/router.php +++ b/config/router.php @@ -10,7 +10,7 @@ | | These files are loaded from the routes directory when route metadata has | not been cached. Add a filename for each application-owned route surface. - | Example values: `api.php`, `web.php`, and `admin.php`. + | Example values: api.php, web.php, and admin.php. | */ 'files' => [ @@ -23,9 +23,9 @@ |-------------------------------------------------------------------------- | | The matcher controls how Webrick compiles and dispatches routes. Build the - | selected form during deployment with `php infbyte route:cache`. - | Allowed values: `fused|generated|sharded`. + | selected form during deployment with `php infbyte route:cache` or + | `php infbyte optimize`. Allowed values: fused|generated|sharded. | */ - 'matcher' => env('ROUTER_MATCHER', 'fused'), + 'matcher' => env_string('ROUTER_MATCHER', 'fused'), ]; diff --git a/deploy.sh b/deploy.sh index e06223d..5843cbd 100755 --- a/deploy.sh +++ b/deploy.sh @@ -28,7 +28,6 @@ fi runtime_directories=( "bootstrap/cache" "bootstrap/cache/config" - "bootstrap/cache/console" "bootstrap/cache/routes" "storage" "storage/app" @@ -74,4 +73,4 @@ php_path="$(command -v "$php_bin")" "$php_path" infbyte optimize -printf 'Deployment directories and application caches are ready.\n' +printf 'Deployment directories and Foundation 2.0 runtime artifacts are ready.\n' diff --git a/infbyte b/infbyte index 6327240..694f8de 100755 --- a/infbyte +++ b/infbyte @@ -3,49 +3,17 @@ declare(strict_types=1); -use Composer\InstalledVersions; -use Infocyph\Foundation\Application\Application; -use Infocyph\Foundation\Console\FoundationConsole; -use Infocyph\Foundation\Config\EnvironmentLoader; +use Infocyph\Foundation\Command\CommandDispatcher; -require __DIR__ . '/vendor/autoload.php'; - -$basePath = __DIR__; -$commandManifest = $basePath . '/bootstrap/cache/console/commands.php'; -$commands = []; - -new EnvironmentLoader()->load($basePath); -$applicationName = trim(env_string('APP_NAME', 'infbyte')); -if ($applicationName === '') { - $applicationName = 'infbyte'; +$autoload = __DIR__ . '/vendor/autoload.php'; +if (!is_file($autoload)) { + fwrite(STDERR, "Unable to locate vendor/autoload.php; run composer install first.\n"); + exit(1); } -if (!is_file($commandManifest)) { - $commands = require $basePath . '/routes/console.php'; - - if (!is_array($commands)) { - throw new RuntimeException('Console routes must return a command-to-class map.'); - } -} - -$console = FoundationConsole::create( - applicationFactory: static function (?string $profile) use ($basePath): Application { - $application = require $basePath . '/bootstrap/console.php'; - - if ($profile !== null && $application->environment() !== $profile) { - throw new RuntimeException(sprintf( - 'Console profile "%s" does not match the application environment "%s".', - $profile, - $application->environment() ?? '', - )); - } - - return $application; - }, - name: $applicationName, - version: InstalledVersions::getPrettyVersion('infocyph/foundation') ?? 'dev-main', - commands: $commands, - commandManifest: $commandManifest, -); +require $autoload; -exit($console->run()); +exit(CommandDispatcher::project( + ['base_path' => __DIR__], + displayName: 'Infbyte', +)->run($argv)); diff --git a/infbyte_work_plan.md b/infbyte_work_plan.md new file mode 100644 index 0000000..bf22ee4 --- /dev/null +++ b/infbyte_work_plan.md @@ -0,0 +1,304 @@ +# Infbyte — Foundation 2.0 Live Work Plan + +> This is the evidence-driven source of truth for Infbyte's Foundation 2.0 integration. Foundation owns reusable framework/runtime behavior; Infbyte owns the opinionated application skeleton and host composition. + +## Branches + +- Infbyte: `feature/foundation-2.0` +- Foundation: `feature/foundation-2.0` +- Integration PR: draft PR #5, `feature/foundation-2.0` → `main` + +## Current checkpoint — 2026-08-25 + +- Verified Infbyte source/docs checkpoint: `f2e81b09a843a3d9ea162ed349790107078cf1f8` +- Verified Foundation prerelease checkpoint consumed by the integration line: `f559355b785d4edc56edfde2eadd1aa652c69360` +- Authoritative Infbyte integration Security & Standards run: `32837108534` +- Result: PHP 8.4/8.5 × prefer-lowest/prefer-stable quality suites PASS; clean production install PASS. +- Current phase: **source/dependency/API/docs audit complete; stable Foundation 2.0 publication, constraint cutover, normal release-workflow restoration, and final stable run remain**. + +## Ownership boundary + +Foundation owns reusable framework/runtime behavior: explicit runtimes, DI/provider activation, CLI, scheduler/worker composition, purpose modules, schema orchestration, optimization, operational controls, application contracts, and specialist-package integration. + +Infbyte owns only the opinionated host skeleton: application bootstrap, app-specific defaults, routes/application code, writable layout, deployment conventions, distribution/archive rules, and final developer experience. + +Specialist packages retain their own database/cache/messaging/communication/crypto/validation/filesystem engines. + +## Frozen Infbyte structure + +- root `infbyte` delegates to Foundation `CommandDispatcher`; +- Web bootstrap delegates to `Foundation::web()`; +- Foundation exposes exactly Web, CLI, Worker, Scheduler runtimes; +- provider groups are `common|web|cli|worker|scheduler`; +- checked-in config remains deliberately only `app.php`, `auth.php`, `router.php`; +- checked-in routes are `api.php`, `console.php`, `schedule.php`, `workers.php`; +- optional capability config is module-published on demand; +- `routes/console.php` registers application commands; +- `routes/schedule.php` registers schedule definitions; +- `routes/workers.php` registers non-message maintenance workers; +- generated optimized artifacts are not committed; +- deployment optimization uses `php infbyte optimize`; +- Foundation 1 convenience APIs/runtime hierarchy are not restored. + +## Foundation dependency baseline consumed by Infbyte + +Core Foundation runtime: + +- PHP `^8.4` +- `infocyph/arraykit ^5.1.1` +- `infocyph/intermix ^9.2` +- `infocyph/uid ^5.0` +- `infocyph/webrick ^4.0.2` + +Optional capability packages owned by Foundation modules: + +- `infocyph/cachelayer ^3.2.0` +- `infocyph/dblayer ^5.0` +- `infocyph/epicrypt ^2.1` +- `infocyph/omnibus ^2.5` +- `infocyph/otp ^6.0` +- `infocyph/pathwise ^3.1` +- `infocyph/reqshield ^3.1` +- `infocyph/talkingbytes ^2.0` +- `web-auth/webauthn-lib ^5.3.5` +- `infocyph/phpforge dev-main@dev` + +During branch development Infbyte intentionally requires: + +```json +"infocyph/foundation": "dev-feature/foundation-2.0 as 2.0.x-dev" +``` + +Final release alignment must replace this with the stable Foundation `^2.0` constraint after Foundation 2.0 is published. + +## Frozen purpose-first modules + +Canonical modules: + +- `auth` +- `cache` +- `communication` +- `database` +- `filesystem` +- `logging` +- `messaging` +- `operations` +- `resources` +- `security` +- `session` +- `validation` + +Canonical aliases include: + +- `db|dblayer -> database` +- `crypto|epicrypt -> security` +- `otp|mfa|passkey|passkeys|webauthn -> auth` +- `notifications|talkingbytes -> communication` +- `events|omnibus|queue|queues -> messaging` +- `files|pathwise|storage -> filesystem` +- `reqshield|validator -> validation` +- `ops|runtime -> operations` + +No standalone OTP/passkey public module exists. + +## Config/schema lifecycle inherited from Foundation + +Commands: + +- `module:list` +- `module:show ` +- `module:install ` +- `module:remove ` +- `module:config:publish [--force]` +- `module:schema:status [--connection=...]` +- `module:schema:install [--connection=...]` +- `module:schema:sync [--connection=...]` + +Schema owners: + +- `auth` → Foundation auth schema; +- `cache` → CacheLayer public PDO/SQLite/invalidation schemas; +- `session` → Foundation database-session schema. + +Schema status is read-only. Explicit installation owns mutation. Module removal never drops schema/data. + +Infbyte does not copy optional config into the skeleton merely because Foundation supports the capability. + +## Application API rule + +Foundation `Application` is a narrow runtime/composition object. Infbyte application code resolves concrete services through constructor injection or `Application::make()`. + +Do not rely on or recreate retired convenience facades such as: + +- `$app->auth()` / `$app->authManager()`; +- `$app->session()` / `$app->browserSession()`; +- `$app->router()`; +- `$app->responses()`; +- `$app->testing()`; +- `$app->messaging()`; +- `$app->ids()`; +- generic cache/database/filesystem/security/validation manager methods. + +## Application contracts available to Infbyte apps + +### Validation + +- Foundation `FormRequest` composes Webrick request input with ReqShield; +- custom rules implement ReqShield `Contracts\Rule` directly; +- generators: `create:request`, `create:rule`. + +### Notifications/mail + +Foundation application routing: + +- `Notification` +- `NotificationRecipient` +- `NotificationChannel` +- `NotificationDispatcher` / registry + +TalkingBytes-backed mail: + +- `MailMessage` +- `Mailer` +- `MailNotificationChannel` + +Generators: `create:mail`, `create:notification`, `create:notification-channel`. + +### Messaging/jobs + +- `Job` +- `JobContext` +- `JobMiddleware` +- Omnibus-backed handler pipeline +- generators: `create:job`, `create:handler`, `create:job-middleware`. + +### Resources/testing + +- `JsonResource::resolve(): mixed`; +- `create:resource` targets the current contract; +- resolve `JsonDispatchResponseFactory`, `AuthServices`, and `TestKit` through DI rather than Application shortcuts. + +## Runtime/operations inherited by Infbyte + +### Omnibus 2.5 workers + +- single messaging workers use native Omnibus `WorkerLifecycle` for heartbeat/reload/stop; +- no `pcntl` requirement solely for single-worker generation polling; +- Unix `WorkerPool` remains pcntl/posix based and retains the Foundation watchdog; +- provider-only workers remain messaging-lazy. + +### Scheduler/runtime control + +- overlap/single-server locks refresh during child execution; +- lost lease terminates/fails the child; +- schedule history uses stable identity; +- `schedule:test` reports actual failure status; +- file/cache generation-map mutations are atomic/serialized; +- cache-backed runtime control requires suitable shared visibility and coordination; +- runtime registry visibility is `host|shared`, default `host`; +- process registry is observability metadata, not process-supervision truth. + +### Other operations + +- supervised child commands do not duplicate `--profile` output; +- `log:tail --follow` handles truncation/rotation; +- production OTP validation uses production topology assumptions; +- environment encryption remains Epicrypt-backed with external key material only; +- cache schema status does not create a missing SQLite file. + +## CLI inheritance + +The root executable delegates directly to Foundation, so Infbyte inherits the Foundation command catalog without duplicate command classes. + +Global controls include `--quiet`, `--silent`, `-v|-vv|-vvv`, `--profile`, `--json`, `--env`, `--no-interaction`, help/version/completion. + +## Resolved integration issues — evidence + +The current draft PR #5 head `f2e81b09a843a3d9ea162ed349790107078cf1f8` is validated by run `32837108534`. + +Resolved items: + +- removed stale Foundation 1 test usage such as `Foundation::console()`, `$app->testing()`, broad manager/facade methods and retired schema command names; +- verified the exactly-four-runtime contract and narrow `Application` surface; +- made Web response tests format-independent by decoding JSON; +- aligned route/config/command cache tests to current Foundation 2 CLI contracts and canonical cache paths; +- isolated CLI cache tests in temporary skeletons so runtime artifacts cannot race with distribution tests; +- verified canonical `database` module naming, built-in module status, readiness JSON and `php infbyte module:install database` guidance; +- aligned `.gitattributes` with Foundation 2 cache artifacts (`commands.php`, `schedule.php`, `optimize.php`, `config/`, `container/`, `routes/`); +- aligned create-project archive/deploy/`optimize:clear` tests with current cache-tree ownership; +- removed stale expected `ids.php` base config; checked-in config remains `app.php`, `auth.php`, `router.php`; +- removed obsolete PHPForge reusable-workflow inputs that caused workflow startup failure; +- preserved PHPForge's stable runtime-constraint guard instead of weakening release policy; +- corrected the README skeleton tree so it no longer advertises nonexistent `routes/web.php` or `routes/auth.php` files. + +### CI policy during prerelease integration + +PHPForge's stable-runtime constraint guard correctly rejects the temporary branch alias as a release dependency. Therefore this feature branch currently runs an explicit integration matrix with the same PHPForge quality suite after dependency resolution: + +- PHP 8.4 prefer-lowest: PASS; +- PHP 8.4 prefer-stable: PASS; +- PHP 8.5 prefer-lowest: PASS; +- PHP 8.5 prefer-stable: PASS; +- clean production install on PHP 8.5: PASS. + +This is temporary. Once Foundation 2.0 is published and Infbyte changes to `^2.0`, restore the normal reusable PHPForge Security & Standards workflow so the stable-runtime constraint guard is a release gate again. + +## Final prerelease audit — 2026-08-25 + +Completed against the active feature branch: + +- Composer remains intentionally minimal: PHP, Foundation, and PHPForge only; Infbyte does not duplicate Foundation's specialist dependencies. +- Foundation module/dependency baseline in this plan is current: CacheLayer 3.2.0, DBLayer 5.0, Omnibus 2.5, ReqShield 3.1 and the other frozen specialist versions. +- no stale `4.1`, `2.4`, or `3.0.1` integration-version references remain in indexed Infbyte release content; +- no active `Foundation::console`, old manager classes, `module:install db`, or retired hyphenated module-schema command usage remains; +- README, Composer, workflow, distribution rules and exercised integration tests are aligned with the actual branch tree; +- draft PR #5 remains mergeable and intentionally draft until the stable Foundation dependency exists. + +## Distribution contract now verified + +A create-project archive: + +- retains the application example tests and writable-directory placeholders; +- excludes repository-only verification tests and development metadata; +- contains no generated PHP cache artifacts; +- contains no pre-created `bootstrap/cache/config`, `container`, or `routes` runtime trees; +- provisions `.env` idempotently with secure permissions and generated auth secret; +- `deploy.sh` builds Foundation 2 config/route/command/schedule/optimize/container artifacts; +- `optimize:clear` removes every managed artifact and the complete dedicated config cache tree. + +## Verification status + +Completed with evidence: + +1. [x] Composer dependency resolution on PHP 8.4/8.5 lowest/stable. +2. [x] PHPForge quality suite on PHP 8.4/8.5 lowest/stable (`32837108534`). +3. [x] clean production install (`32837108534`). +4. [x] four-runtime and narrow-Application integration tests. +5. [x] canonical Web handling and route cache consumption. +6. [x] config/route/command cache lifecycle through the Infbyte CLI wrapper. +7. [x] canonical module/readiness/install-guidance contracts. +8. [x] clean create-project/archive/install/deploy/optimize-clear path. +9. [x] Foundation 2 cache/distribution `.gitattributes` alignment. +10. [x] stale Foundation 1 test/API expectations removed from the exercised integration suite. +11. [x] Finish source/docs/dependency stale-version and retired-API audit across the whole Infbyte branch (`f2e81b09a843a3d9ea162ed349790107078cf1f8`, `32837108534`). + +Remaining before stable release: + +12. [ ] Publish Foundation 2.0 and replace `dev-feature/foundation-2.0 as 2.0.x-dev` with stable `^2.0`. +13. [ ] Restore the normal reusable PHPForge release workflow and obtain a full green stable-constraint run. +14. [ ] Record final post-publication Infbyte + Foundation source/CI checkpoints and close all release ambiguities. + +## Do not regress + +- no package-per-module public model; +- no standalone OTP/passkey module; +- no duplicate specialist schema command families; +- no schema/data deletion during module removal; +- no copied specialist engine/SQL/queue/retry/cache/database/communication implementation; +- no broad Application/service facade in Infbyte; +- no retired Console runtime hierarchy; +- no static global application state; +- no optional config copied into the skeleton by default; +- no environment-protection key in `.env`/`.env.example`; +- no generated optimized artifacts committed; +- no weakening of PHPForge stable-release dependency policy to accommodate a prerelease branch alias. diff --git a/routes/api.php b/routes/api.php index 9dedb76..0a73ca2 100644 --- a/routes/api.php +++ b/routes/api.php @@ -3,7 +3,8 @@ declare(strict_types=1); use App\Http\Controllers\SystemController; -use Infocyph\Webrick\Router\Facade\Router as Route; +use Infocyph\Webrick\Router\Definition\Registrar; -Route::get('/api/health', SystemController::health(...)); -Route::get('/json', SystemController::json(...), 'json'); +/** @var Registrar $router */ +$router->get('/api/health', SystemController::health(...)); +$router->get('/json', SystemController::json(...), 'json'); diff --git a/routes/console.php b/routes/console.php index eb030cb..dedf624 100644 --- a/routes/console.php +++ b/routes/console.php @@ -5,6 +5,6 @@ return [ /** * Usage: - * 'reports:daily' => \App\Console\Commands\Reports\DailyCommand::class, + * 'reports:daily' => \App\Command\Reports\DailyCommand::class, */ ]; diff --git a/routes/schedule.php b/routes/schedule.php index d362161..2c064b8 100644 --- a/routes/schedule.php +++ b/routes/schedule.php @@ -2,7 +2,7 @@ declare(strict_types=1); -use Infocyph\Console\Scheduling\Schedule; +use Infocyph\Foundation\Scheduling\Schedule; return static function (Schedule $schedule): void { /** diff --git a/routes/workers.php b/routes/workers.php index 651de55..3a60206 100644 --- a/routes/workers.php +++ b/routes/workers.php @@ -4,7 +4,11 @@ return [ /** + * Application maintenance workers implement + * \Infocyph\Foundation\Worker\WorkerProvider and normally live in + * app/Worker. Queue/message workers are configured through Omnibus. + * * Usage: - * 'queue' => \App\Console\Workers\QueueWorker::class, + * 'maintenance' => \App\Worker\MaintenanceWorker::class, */ ]; diff --git a/tests/Feature/ExampleTest.php b/tests/Feature/ExampleTest.php index 1e8c52b..ac190ff 100644 --- a/tests/Feature/ExampleTest.php +++ b/tests/Feature/ExampleTest.php @@ -3,17 +3,15 @@ declare(strict_types=1); use Infocyph\Foundation\Application\Application; +use Infocyph\Webrick\Request\Request; it('serves the application health endpoint', function (): void { /** @var Application $app */ $app = require dirname(__DIR__, 2) . '/bootstrap/app.php'; - $app->boot(); - $response = $app->testing() - ->http() - ->get('/api/health') - ->assertStatus(200) - ->assertJson(['status' => 'ok']); + $response = $app->handle(Request::fake(method: 'GET', uri: 'http://localhost/api/health')); + $payload = json_decode((string) $response->getBody(), true, flags: JSON_THROW_ON_ERROR); - expect($response->json())->toBe(['status' => 'ok']); + expect($response->getStatusCode())->toBe(200) + ->and($payload)->toBe(['status' => 'ok']); }); diff --git a/tests/Feature/FrameworkRuntimeTest.php b/tests/Feature/FrameworkRuntimeTest.php index b45a8d0..04d2bda 100644 --- a/tests/Feature/FrameworkRuntimeTest.php +++ b/tests/Feature/FrameworkRuntimeTest.php @@ -3,27 +3,15 @@ declare(strict_types=1); use Infocyph\Foundation\Application\Application; -use Infocyph\Foundation\Auth\Contract\Notification\AuthNotifierInterface; -use Infocyph\Foundation\Cache\CacheManager; -use Infocyph\Console\Command\CommandContract; -use Infocyph\Foundation\Console\FoundationConsole; -use Infocyph\Foundation\Database\DatabaseManager; -use Infocyph\Foundation\Filesystem\FilesystemManager; +use Infocyph\Foundation\Application\RuntimeMode; +use Infocyph\Foundation\Config\ConfigRepository; +use Infocyph\Foundation\Filesystem\PathManager; use Infocyph\Foundation\Foundation; use Infocyph\Foundation\Http\HttpKernel; -use Infocyph\Foundation\Notifications\NotificationManager; -use Infocyph\Foundation\Routing\RouteFileLoader; -use Infocyph\Foundation\Routing\RouterManager; -use Infocyph\Foundation\Validation\ValidationManager; +use Infocyph\Foundation\Runtime\ExecutionScope; +use Infocyph\Webrick\Request\Request; -function infbyteApp(): Application -{ - return Foundation::web(infbyteTestOptions()); -} - -/** - * @return array - */ +/** @return array */ function infbyteTestOptions(): array { $root = dirname(__DIR__, 2); @@ -31,7 +19,7 @@ function infbyteTestOptions(): array . '/infbyte-runtime-' . getmypid(); - foreach (['app', 'app/public', 'cache', 'logs', 'sessions', 'uploads'] as $directory) { + foreach (['cache', 'logs', 'sessions', 'uploads'] as $directory) { $path = $runtime . DIRECTORY_SEPARATOR . $directory; if (!is_dir($path) && !mkdir($path, 0775, true) && !is_dir($path)) { @@ -42,7 +30,9 @@ function infbyteTestOptions(): array return [ 'base_path' => $root, '_config_cache' => false, - 'env' => 'testing', + 'app' => [ + 'env' => 'testing', + ], 'paths' => [ 'storage' => $runtime, 'cache' => $runtime . '/cache', @@ -50,124 +40,113 @@ function infbyteTestOptions(): array 'sessions' => $runtime . '/sessions', 'uploads' => $runtime . '/uploads', ], - 'auth' => [ - 'drivers' => [ - 'cache' => 'array', - ], - ], 'router' => [ 'cache' => false, ], ]; } -function expectedNotifierClass(Application $app): string -{ - return $app->config()->get('auth.drivers.notifications') === 'talkingbytes' - ? 'Infocyph\\Foundation\\Auth\\Adapter\\TalkingBytes\\TalkingBytesAuthNotifier' - : 'Infocyph\\Foundation\\Auth\\Support\\CollectingAuthNotifier'; -} - -it('boots with the expected application shape', function (): void { - $app = infbyteApp(); +it('constructs exactly the four explicit Foundation runtimes', function (): void { + $options = infbyteTestOptions(); + $cases = [ + [Foundation::web(...), RuntimeMode::Web, 'runningInWeb'], + [Foundation::cli(...), RuntimeMode::Cli, 'runningInCli'], + [Foundation::worker(...), RuntimeMode::Worker, 'runningInWorker'], + [Foundation::scheduler(...), RuntimeMode::Scheduler, 'runningInScheduler'], + ]; - expect($app->environment())->toBeString()->not->toBeEmpty() - ->and($app->runningInWeb())->toBeTrue(); + foreach ($cases as [$factory, $mode, $predicate]) { + /** @var callable(array):Application $factory */ + $app = $factory($options); - $paths = $app->paths()->all(); + expect($app)->toBeInstanceOf(Application::class) + ->and($app->runtimeMode())->toBe($mode) + ->and($app->{$predicate}())->toBeTrue() + ->and($app->booted())->toBeFalse() + ->and($app->environment())->toBe('testing') + ->and($app->basePath())->toBe(dirname(__DIR__, 2)); + } - expect($paths)->toHaveKey('providers'); - expect(is_file($paths['providers']))->toBeTrue(); - expect(is_dir($paths['storage']))->toBeTrue(); + expect(method_exists(Foundation::class, 'console'))->toBeFalse(); }); -it('keeps the console bootstrap outside the web boot graph', function (): void { - $app = Foundation::console(infbyteTestOptions()); - - expect($app)->toBeInstanceOf(Application::class) - ->and($app->runningInConsole())->toBeTrue() - ->and($app->container()->has(RouteFileLoader::class))->toBeFalse() - ->and($app->container()->has(HttpKernel::class))->toBeFalse(); - - $app->boot(); +it('keeps HTTP unavailable outside the web runtime', function (): void { + $app = Foundation::cli(infbyteTestOptions())->boot(); - expect($app->container()->has(RouteFileLoader::class))->toBeFalse() - ->and($app->container()->has(HttpKernel::class))->toBeFalse() + expect($app->runningInCli())->toBeTrue() + ->and($app->booted())->toBeTrue() ->and(fn() => $app->http()) - ->toThrow(LogicException::class, 'HTTP kernel is unavailable'); + ->toThrow(LogicException::class, 'The HTTP kernel is unavailable in the cli runtime.'); }); -it('defines console commands through an explicit command route map', function (): void { - $commands = require dirname(__DIR__, 2) . '/routes/console.php'; +it('resolves only the narrow Foundation application core directly', function (): void { + $app = Foundation::web(infbyteTestOptions())->boot(); + + expect($app->make(Application::class))->toBe($app) + ->and($app->config())->toBeInstanceOf(ConfigRepository::class) + ->and($app->paths())->toBeInstanceOf(PathManager::class) + ->and($app->execution())->toBeInstanceOf(ExecutionScope::class) + ->and($app->http())->toBeInstanceOf(HttpKernel::class) + ->and($app->booted())->toBeTrue(); + + foreach ([ + 'auth', + 'authManager', + 'cache', + 'database', + 'filesystem', + 'ids', + 'notifications', + 'router', + 'testing', + 'validation', + ] as $retiredConvenienceMethod) { + expect(method_exists($app, $retiredConvenienceMethod))->toBeFalse(); + } +}); - expect($commands)->toBeArray() - ->and($commands)->toBe([]); +it('defines application commands through the explicit command route file', function (): void { + $commands = require dirname(__DIR__, 2) . '/routes/console.php'; - foreach ($commands as $command) { - expect(is_string($command) && is_a($command, CommandContract::class, true))->toBeTrue(); - } + expect($commands)->toBeArray()->toBe([]); }); -it('keeps Foundation system commands out of application Composer scripts', function (): void { +it('keeps Foundation system commands out of application Composer script keys', function (): void { $composer = json_decode( - file_get_contents(dirname(__DIR__, 2) . '/composer.json'), + (string) file_get_contents(dirname(__DIR__, 2) . '/composer.json'), true, flags: JSON_THROW_ON_ERROR, ); $scripts = $composer['scripts'] ?? []; - expect($scripts)->toBeArray() - ->and(array_intersect( - array_keys($scripts), - array_keys(FoundationConsole::commands([])), - ))->toBe([]); -}); - -it('registers the core services', function (): void { - $app = infbyteApp()->boot(); - - expect($app->auth())->toBeObject(); - expect($app->authManager())->toBeObject(); - expect($app->authActions())->toBeObject(); - expect($app->http())->toBeObject(); - expect($app->ids())->toBeObject(); - expect($app->has(CacheManager::class))->toBeFalse(); - expect($app->has(DatabaseManager::class))->toBeFalse(); - expect($app->has(FilesystemManager::class))->toBeFalse(); - expect($app->has(NotificationManager::class))->toBeFalse(); - expect($app->has(ValidationManager::class))->toBeFalse(); -}); - -it('serves the health and JSON routes', function (): void { - $app = infbyteApp()->boot(); - $router = $app->make(RouterManager::class); - $registered = []; - - foreach ($router->routes() as $route) { - $registered[$route->getMethod() . ' ' . $route->getPath()] = $route->getHandler(); + expect($scripts)->toBeArray(); + + foreach ([ + 'app:ready', + 'config:cache', + 'db:monitor', + 'module:list', + 'optimize', + 'route:cache', + 'schedule:run', + 'worker:run', + ] as $systemCommand) { + expect(array_key_exists($systemCommand, $scripts))->toBeFalse(); } - - expect(array_keys($registered))->toBe(['GET /api/health', 'GET /json']); - expect($registered['GET /api/health'])->toBeInstanceOf(Closure::class) - ->and($registered['GET /json'])->toBeInstanceOf(Closure::class); - - $http = $app->testing()->http(); - $health = $http->get('/api/health') - ->assertStatus(200) - ->assertHeader('Content-Type') - ->assertJson(['status' => 'ok']); - $json = $http->get('/json') - ->assertStatus(200) - ->assertHeader('Content-Type') - ->json(); - - expect($health->json())->toBe(['status' => 'ok']) - ->and($json)->toHaveKey('memory') - ->and($json['memory'])->toBeInt(); }); -it('uses the self-contained auth notifier without optional modules', function (): void { - $app = infbyteApp()->boot(); +it('serves the skeleton routes through canonical Foundation web handling', function (): void { + $app = Foundation::web(infbyteTestOptions()); + + $health = $app->handle(Request::fake(method: 'GET', uri: 'http://localhost/api/health')); + $json = $app->handle(Request::fake(method: 'GET', uri: 'http://localhost/json')); + $healthPayload = json_decode((string) $health->getBody(), true, flags: JSON_THROW_ON_ERROR); + $jsonPayload = json_decode((string) $json->getBody(), true, flags: JSON_THROW_ON_ERROR); - expect($app->make(AuthNotifierInterface::class)::class)->toBe(expectedNotifierClass($app)); + expect($app->booted())->toBeTrue() + ->and($health->getStatusCode())->toBe(200) + ->and($healthPayload)->toBe(['status' => 'ok']) + ->and($json->getStatusCode())->toBe(200) + ->and($jsonPayload)->toHaveKey('memory') + ->and($jsonPayload['memory'])->toBeInt(); }); diff --git a/tests/Feature/RouteCacheCliTest.php b/tests/Feature/RouteCacheCliTest.php index 8eed028..58717c7 100644 --- a/tests/Feature/RouteCacheCliTest.php +++ b/tests/Feature/RouteCacheCliTest.php @@ -2,130 +2,87 @@ declare(strict_types=1); -use Composer\InstalledVersions; use App\Http\Controllers\SystemController; -use Infocyph\Foundation\Auth\AuthManager; -use Infocyph\Foundation\Database\DatabaseManager; +use Composer\InstalledVersions; use Infocyph\Foundation\Foundation; -use Infocyph\Foundation\Messaging\MessagingManager; use Infocyph\Foundation\Routing\RouteCachePath; -use Infocyph\Foundation\Session\SessionManager; use Infocyph\Webrick\Request\Request; use Infocyph\Webrick\Router\Matching\FusedMatcher; -it('uses the environment application name and reports the Foundation runtime version', function (): void { - $root = dirname(__DIR__, 2); - [$exitCode, $output] = runInfbyteCommand([ - PHP_BINARY, - $root . '/infbyte', - '--version', - ], ['APP_NAME' => 'Acme Console']); - - expect($exitCode)->toBe(0) - ->and($output)->toBe( - 'Acme Console ' . (InstalledVersions::getPrettyVersion('infocyph/foundation') ?? 'dev-main'), - ); -}); +it('keeps the Infbyte CLI identity independent of the application display name', function (): void { + $fixture = createInfbyteCliFixture(); -it('falls back to infbyte when the environment application name is empty', function (): void { - $root = dirname(__DIR__, 2); - [$exitCode, $output] = runInfbyteCommand([ - PHP_BINARY, - $root . '/infbyte', - '--version', - ], ['APP_NAME' => '']); - - expect($exitCode)->toBe(0) - ->and($output)->toBe( - 'infbyte ' . (InstalledVersions::getPrettyVersion('infocyph/foundation') ?? 'dev-main'), - ); + try { + [$exitCode, $output] = runInfbyteCommand([ + PHP_BINARY, + $fixture . '/infbyte', + '--version', + ], ['APP_NAME' => 'Acme Console']); + + expect($exitCode)->toBe(0) + ->and($output)->toBe( + 'Infbyte ' . (InstalledVersions::getPrettyVersion('infocyph/foundation') ?? 'dev-main'), + ); + } finally { + removeInfbyteTestDirectory($fixture); + } }); -it('builds and clears route cache through the infbyte cli wrapper', function (): void { - $root = dirname(__DIR__, 2); - $cacheFile = rtrim(sys_get_temp_dir(), DIRECTORY_SEPARATOR) - . '/infbyte-route-cache-' . bin2hex(random_bytes(5)) . '.php'; - - [$buildExitCode, $buildOutput] = runInfbyteCommand([ - PHP_BINARY, - $root . '/infbyte', - 'route:cache', - '--matcher=fused', - '--cache=' . $cacheFile, - ]); +it('builds, consumes, and clears route cache through the infbyte cli wrapper', function (): void { + $fixture = createInfbyteCliFixture(); + $cacheFile = $fixture . '/bootstrap/cache/routes/fused.php'; - expect($buildExitCode)->toBe(0); - expect($buildOutput)->toContain('Route cache ready at:'); - expect(is_file($cacheFile))->toBeTrue(); - expect(filesize($cacheFile))->toBeGreaterThan(0); - - $matcher = FusedMatcher::make()->enableCache($cacheFile); - [$cachedRoute] = $matcher->match('GET', 'localhost', '/json'); - $cachedHandler = $cachedRoute->getHandler(); - $webrickVersion = InstalledVersions::getVersion('infocyph/webrick') ?? '0.0.0'; - $usesNativeHandler = version_compare($webrickVersion, '3.3.0', '>='); - if ($usesNativeHandler) { - expect($cachedHandler)->toBe([SystemController::class, 'json']) - ->and(class_exists(\Opis\Closure\Serializer::class, false))->toBeFalse(); - } else { - expect($cachedHandler)->toBeInstanceOf(Closure::class); - } + try { + [$buildExitCode, $buildOutput] = runInfbyteCommand([ + PHP_BINARY, + $fixture . '/infbyte', + 'route:cache', + ]); - $runtime = rtrim(sys_get_temp_dir(), DIRECTORY_SEPARATOR) - . '/infbyte-cached-runtime-' . bin2hex(random_bytes(5)); - $runtimeCache = $runtime . '/bootstrap/cache/routes/fused.php'; - mkdir(dirname($runtimeCache), 0775, true); - mkdir($runtime . '/routes', 0775, true); - copy($cacheFile, $runtimeCache); - file_put_contents( - $runtime . '/routes/missing.php', - "toBe(0) + ->and($buildOutput)->toContain('Routes cached using fused matcher at ') + ->and($cacheFile)->toBeFile() + ->and(filesize($cacheFile))->toBeGreaterThan(0); + + $matcher = FusedMatcher::make()->enableCache($cacheFile); + [$cachedRoute] = $matcher->match('GET', 'localhost', '/json'); + + expect($cachedRoute->getHandler())->toBe([SystemController::class, 'json']); + + file_put_contents( + $fixture . '/routes/api.php', + " $runtime, + 'base_path' => $fixture, '_config_cache' => false, 'router' => [ 'cache' => true, - 'files' => ['missing.php'], + 'files' => ['api.php'], 'matcher' => 'fused', ], ]); - $repository = $app->container()->getRepository(); $response = $app->handle(Request::fake(method: 'GET', uri: 'http://localhost/json')); + $payload = json_decode((string) $response->getBody(), true, flags: JSON_THROW_ON_ERROR); expect($response->getStatusCode())->toBe(200) - ->and((string) $response->getBody())->toContain('memory') - ->and($repository->hasResolvedSingleton(AuthManager::class))->toBeFalse() - ->and($repository->hasResolvedSingleton(SessionManager::class))->toBeFalse() - ->and($repository->hasResolvedSingleton(DatabaseManager::class))->toBeFalse() - ->and($repository->hasResolvedSingleton(MessagingManager::class))->toBeFalse() - ->and(get_included_files())->not->toContain($runtime . '/routes/missing.php'); + ->and($payload)->toHaveKey('memory') + ->and($payload['memory'])->toBeInt() + ->and(get_included_files())->not->toContain($fixture . '/routes/api.php'); + + [$clearExitCode, $clearOutput] = runInfbyteCommand([ + PHP_BINARY, + $fixture . '/infbyte', + 'route:clear', + ]); + + expect($clearExitCode)->toBe(0) + ->and($clearOutput)->toContain('Route cache cleared.') + ->and($cacheFile)->not->toBeFile(); } finally { - if (isset($app)) { - $app->container()->unset(); - } - unlink($runtimeCache); - rmdir(dirname($runtimeCache)); - rmdir(dirname(dirname($runtimeCache))); - rmdir(dirname(dirname(dirname($runtimeCache)))); - unlink($runtime . '/routes/missing.php'); - rmdir($runtime . '/routes'); - rmdir($runtime); + removeInfbyteTestDirectory($fixture); } - - [$clearExitCode, $clearOutput] = runInfbyteCommand([ - PHP_BINARY, - $root . '/infbyte', - 'route:clear', - '--matcher=fused', - '--cache=' . $cacheFile, - ]); - - expect($clearExitCode)->toBe(0); - expect($clearOutput)->toContain('Route cache cleared:'); - expect(file_exists($cacheFile))->toBeFalse(); }); it('derives the dedicated routes cache path by default', function (): void { @@ -138,154 +95,173 @@ expect(RouteCachePath::for($app->config()))->toBe($root . '/bootstrap/cache/routes/fused.php'); }); -it('builds and clears the default sharded config cache through the infbyte cli wrapper', function (): void { - $root = dirname(__DIR__, 2); - $cacheDirectory = rtrim(sys_get_temp_dir(), DIRECTORY_SEPARATOR) - . '/infbyte-config-cache-' . bin2hex(random_bytes(5)); - - [$buildExitCode, $buildOutput] = runInfbyteCommand([ - PHP_BINARY, - $root . '/infbyte', - 'config:cache', - '--path=' . $cacheDirectory, - ]); +it('builds and fully clears the default sharded config cache through the infbyte cli wrapper', function (): void { + $fixture = createInfbyteCliFixture(); + $cacheDirectory = $fixture . '/bootstrap/cache/config'; - expect($buildExitCode)->toBe(0); - expect($buildOutput)->toContain('Configuration cached (sharded):'); - expect($cacheDirectory . '/__manifest.php')->toBeFile() - ->and($cacheDirectory . '/app.php')->toBeFile() - ->and($cacheDirectory . '/__flat.php')->not->toBeFile() - ->and($cacheDirectory . '/__compiled.php')->not->toBeFile(); - - [$clearExitCode, $clearOutput] = runInfbyteCommand([ - PHP_BINARY, - $root . '/infbyte', - 'config:clear', - '--path=' . $cacheDirectory, - ]); + try { + [$buildExitCode, $buildOutput] = runInfbyteCommand([ + PHP_BINARY, + $fixture . '/infbyte', + 'config:cache', + ]); - expect($clearExitCode)->toBe(0); - expect($clearOutput)->toContain('Configuration cache cleared:'); - expect($cacheDirectory . '/__manifest.php')->not->toBeFile() - ->and($cacheDirectory . '/app.php')->not->toBeFile() - ->and($cacheDirectory . '/__flat.php')->not->toBeFile(); + expect($buildExitCode)->toBe(0) + ->and($buildOutput)->toContain('Configuration cached using sharded.') + ->and($cacheDirectory . '/__manifest.php')->toBeFile() + ->and($cacheDirectory . '/app.php')->toBeFile() + ->and($cacheDirectory . '/__flat.php')->toBeFile(); - rmdir($cacheDirectory); + [$clearExitCode, $clearOutput] = runInfbyteCommand([ + PHP_BINARY, + $fixture . '/infbyte', + 'config:clear', + ]); + + expect($clearExitCode)->toBe(0) + ->and($clearOutput)->toContain('Configuration cache cleared.') + ->and($cacheDirectory)->not->toBeDirectory(); + } finally { + removeInfbyteTestDirectory($fixture); + } }); -it('can explicitly build a single config cache through the infbyte cli wrapper', function (): void { - $root = dirname(__DIR__, 2); - $cacheDirectory = rtrim(sys_get_temp_dir(), DIRECTORY_SEPARATOR) - . '/infbyte-single-config-cache-' . bin2hex(random_bytes(5)); - - [$exitCode, $output] = runInfbyteCommand([ - PHP_BINARY, - $root . '/infbyte', - 'config:cache', - '--type=single', - '--path=' . $cacheDirectory, - ]); +it('can select and fully clear the single config cache through application configuration', function (): void { + $fixture = createInfbyteCliFixture(); + $cacheDirectory = $fixture . '/bootstrap/cache/config'; - expect($exitCode)->toBe(0); - expect($output)->toContain('Configuration cached (single):'); - expect($cacheDirectory . '/__manifest.php')->toBeFile() - ->and($cacheDirectory . '/app.php')->not->toBeFile() - ->and($cacheDirectory . '/__flat.php')->not->toBeFile(); - - [$clearExitCode] = runInfbyteCommand([ - PHP_BINARY, - $root . '/infbyte', - 'config:clear', - '--path=' . $cacheDirectory, - ]); + try { + [$exitCode, $output] = runInfbyteCommand([ + PHP_BINARY, + $fixture . '/infbyte', + 'config:cache', + ], ['APP_CONFIG_CACHE_TYPE' => 'single']); + + expect($exitCode)->toBe(0) + ->and($output)->toContain('Configuration cached using single.') + ->and($cacheDirectory . '/__manifest.php')->toBeFile() + ->and($cacheDirectory . '/app.php')->not->toBeFile() + ->and($cacheDirectory . '/__flat.php')->not->toBeFile(); + + [$clearExitCode] = runInfbyteCommand([ + PHP_BINARY, + $fixture . '/infbyte', + 'config:clear', + ]); - expect($clearExitCode)->toBe(0); - rmdir($cacheDirectory); + expect($clearExitCode)->toBe(0) + ->and($cacheDirectory)->not->toBeDirectory(); + } finally { + removeInfbyteTestDirectory($fixture); + } }); it('builds and clears compiled command metadata through the infbyte cli wrapper', function (): void { - $root = dirname(__DIR__, 2); - $cacheDirectory = rtrim(sys_get_temp_dir(), DIRECTORY_SEPARATOR) - . '/infbyte-command-cache-' . bin2hex(random_bytes(5)); - $manifest = $cacheDirectory . '/commands.php'; - - [$buildExitCode, $buildOutput] = runInfbyteCommand([ - PHP_BINARY, - $root . '/infbyte', - 'command:cache', - '--path=' . $manifest, - ]); + $fixture = createInfbyteCliFixture(); + $manifest = $fixture . '/bootstrap/cache/commands.php'; - expect($buildExitCode)->toBe(0); - expect($buildOutput)->toContain('Command manifest ready at:'); - expect($manifest)->toBeFile(); - expect(glob($cacheDirectory . '/commands-*.php') ?: [])->not->toBeEmpty(); - expect($manifest . '.d')->not->toBeDirectory(); - - [$clearExitCode, $clearOutput] = runInfbyteCommand([ - PHP_BINARY, - $root . '/infbyte', - 'command:clear', - '--path=' . $manifest, - ]); - - expect($clearExitCode)->toBe(0); - expect($clearOutput)->toContain('Command manifest cleared:'); - expect($manifest)->not->toBeFile(); - expect(glob($cacheDirectory . '/commands-*.php') ?: [])->toBeEmpty(); - expect($manifest . '.d')->not->toBeDirectory(); + try { + [$buildExitCode, $buildOutput] = runInfbyteCommand([ + PHP_BINARY, + $fixture . '/infbyte', + 'command:cache', + ]); - rmdir($cacheDirectory); -}); + expect($buildExitCode)->toBe(0) + ->and($buildOutput)->toContain('Command manifest cached: ') + ->and($manifest)->toBeFile() + ->and(glob($fixture . '/bootstrap/cache/.commands-*') ?: [])->toBeEmpty(); -it('reports readiness and optional module state through the infbyte cli', function (): void { - $root = dirname(__DIR__, 2); + [$clearExitCode, $clearOutput] = runInfbyteCommand([ + PHP_BINARY, + $fixture . '/infbyte', + 'command:clear', + ]); - [$readinessExitCode, $readinessOutput] = runInfbyteCommand([ - PHP_BINARY, - $root . '/infbyte', - 'app:ready', - '--json=1', - ]); - [$modulesExitCode, $modulesOutput] = runInfbyteCommand([ - PHP_BINARY, - $root . '/infbyte', - 'module:list', - '--json=true', - ]); + expect($clearExitCode)->toBe(0) + ->and($clearOutput)->toContain('Command manifest cleared.') + ->and($manifest)->not->toBeFile(); + } finally { + removeInfbyteTestDirectory($fixture); + } +}); - expect($readinessExitCode)->toBe(2); - expect(json_decode($readinessOutput, true, flags: JSON_THROW_ON_ERROR)['production_ready'])->toBeFalse(); - expect($modulesExitCode)->toBe(0); +it('reports readiness and canonical module state through the infbyte cli', function (): void { + $fixture = createInfbyteCliFixture(); - $modules = array_column( - json_decode($modulesOutput, true, flags: JSON_THROW_ON_ERROR)['modules'], - null, - 'name', - ); + try { + [$readinessExitCode, $readinessOutput] = runInfbyteCommand([ + PHP_BINARY, + $fixture . '/infbyte', + 'app:ready', + '--json=1', + ]); + [$modulesExitCode, $modulesOutput] = runInfbyteCommand([ + PHP_BINARY, + $fixture . '/infbyte', + 'module:list', + '--json=true', + ]); - expect($modules['db']['installed'])->toBeFalse() - ->and($modules['cache']['installed'])->toBeFalse() - ->and($modules['filesystem']['installed'])->toBeFalse() - ->and($modules['logging']['installed'])->toBeTrue() - ->and($modules['messaging']['installed'])->toBeTrue() - ->and($modules['resources']['installed'])->toBeTrue() - ->and($modules['session']['installed'])->toBeTrue(); + $readiness = json_decode($readinessOutput, true, flags: JSON_THROW_ON_ERROR); + $moduleRows = json_decode($modulesOutput, true, flags: JSON_THROW_ON_ERROR); + + expect($readinessExitCode)->toBe(1) + ->and($readiness['ready'])->toBeFalse() + ->and($readiness)->toHaveKey('checks') + ->and($modulesExitCode)->toBe(0) + ->and($moduleRows)->toBeArray(); + + $modules = array_column($moduleRows, null, 'name'); + + expect($modules)->toHaveKeys([ + 'auth', + 'cache', + 'communication', + 'database', + 'filesystem', + 'logging', + 'messaging', + 'operations', + 'resources', + 'security', + 'session', + 'validation', + ])->not->toHaveKey('db') + ->and($modules['database']['packages']['infocyph/dblayer']['installed'])->toBeFalse() + ->and($modules['database']['packages']['infocyph/dblayer']['constraint'])->toBe('^5.0'); + + foreach (['logging', 'operations', 'resources', 'session'] as $builtIn) { + expect($modules[$builtIn]['installed'])->toBeTrue(); + } + } finally { + removeInfbyteTestDirectory($fixture); + } }); -it('explains how to install a service owned by an absent optional module', function (): void { - $root = dirname(__DIR__, 2); +it('reports canonical database installation guidance through module schema metadata', function (): void { + $fixture = createInfbyteCliFixture(); - [$exitCode, $output] = runInfbyteCommand([ - PHP_BINARY, - $root . '/infbyte', - 'auth:schema:status', - '--json=1', - ]); + try { + [$exitCode, $output] = runInfbyteCommand([ + PHP_BINARY, + $fixture . '/infbyte', + 'module:show', + 'auth', + '--json=1', + ]); - expect($exitCode)->toBe(2) - ->and($output)->toContain('requires infocyph/dblayer') - ->and($output)->toContain('php infbyte module:install db'); + $module = json_decode($output, true, flags: JSON_THROW_ON_ERROR); + $authSchema = $module['schema_status'][0] ?? null; + + expect($exitCode)->toBe(0) + ->and($module['name'])->toBe('auth') + ->and($authSchema)->toBeArray() + ->and($authSchema['state'])->toBe('unavailable') + ->and($authSchema['detail'])->toContain('php infbyte module:install database'); + } finally { + removeInfbyteTestDirectory($fixture); + } }); /** @@ -316,3 +292,82 @@ function runInfbyteCommand(array $arguments, array $environment = []): array return [$exitCode, implode("\n", $output)]; } + +function createInfbyteCliFixture(): string +{ + $root = dirname(__DIR__, 2); + $fixture = rtrim(sys_get_temp_dir(), DIRECTORY_SEPARATOR) + . '/infbyte-cli-' . bin2hex(random_bytes(6)); + + foreach ([ + $fixture, + $fixture . '/bootstrap/cache', + $fixture . '/storage/cache', + $fixture . '/storage/logs', + $fixture . '/storage/sessions', + $fixture . '/storage/uploads', + $fixture . '/vendor', + ] as $directory) { + if (!mkdir($directory, 0775, true) && !is_dir($directory)) { + throw new RuntimeException(sprintf('Unable to create CLI fixture directory "%s".', $directory)); + } + } + + copy($root . '/infbyte', $fixture . '/infbyte'); + copy($root . '/composer.json', $fixture . '/composer.json'); + copyInfbyteTestDirectory($root . '/config', $fixture . '/config'); + copyInfbyteTestDirectory($root . '/routes', $fixture . '/routes'); + copy($root . '/bootstrap/providers.php', $fixture . '/bootstrap/providers.php'); + file_put_contents( + $fixture . '/vendor/autoload.php', + 'isDot()) { + continue; + } + + $target = $destination . DIRECTORY_SEPARATOR . $entry->getFilename(); + if ($entry->isDir()) { + copyInfbyteTestDirectory($entry->getPathname(), $target); + continue; + } + + copy($entry->getPathname(), $target); + } +} + +function removeInfbyteTestDirectory(string $directory): void +{ + if (!is_dir($directory)) { + return; + } + + $entries = new RecursiveIteratorIterator( + new RecursiveDirectoryIterator($directory, FilesystemIterator::SKIP_DOTS), + RecursiveIteratorIterator::CHILD_FIRST, + ); + + foreach ($entries as $entry) { + $path = $entry->getPathname(); + + if ($entry->isLink() || $entry->isFile()) { + unlink($path); + continue; + } + + rmdir($path); + } + + rmdir($directory); +} diff --git a/tests/Feature/SkeletonDistributionTest.php b/tests/Feature/SkeletonDistributionTest.php index 5da994b..ec78ab0 100644 --- a/tests/Feature/SkeletonDistributionTest.php +++ b/tests/Feature/SkeletonDistributionTest.php @@ -13,7 +13,6 @@ expect($config)->toBe([ 'app.php', 'auth.php', - 'ids.php', 'router.php', ]); }); @@ -26,7 +25,11 @@ ->and($attributes)->not->toContain("/tests export-ignore\n") ->and($attributes)->toContain('/tests/Feature/FrameworkRuntimeTest.php export-ignore') ->and($attributes)->toContain('/tests/Feature/RouteCacheCliTest.php export-ignore') - ->and($attributes)->toContain('/tests/Feature/SkeletonDistributionTest.php export-ignore'); + ->and($attributes)->toContain('/tests/Feature/SkeletonDistributionTest.php export-ignore') + ->and($attributes)->toContain('/bootstrap/cache/*.php export-ignore') + ->and($attributes)->toContain('/bootstrap/cache/config export-ignore') + ->and($attributes)->toContain('/bootstrap/cache/container export-ignore') + ->and($attributes)->toContain('/bootstrap/cache/routes export-ignore'); }); it('builds a clean create-project archive and provisions its environment', function (): void { @@ -79,9 +82,10 @@ expect($project . '/composer.json')->toBeFile() ->and($project . '/bootstrap/install.php')->not->toBeFile() - ->and($project . '/bootstrap/cache/config/.gitignore')->toBeFile() - ->and($project . '/bootstrap/cache/console/.gitignore')->toBeFile() - ->and($project . '/bootstrap/cache/routes/.gitignore')->toBeFile() + ->and($project . '/bootstrap/cache/.gitignore')->toBeFile() + ->and($project . '/bootstrap/cache/config')->not->toBeDirectory() + ->and($project . '/bootstrap/cache/container')->not->toBeDirectory() + ->and($project . '/bootstrap/cache/routes')->not->toBeDirectory() ->and($project . '/storage/cache/.gitignore')->toBeFile() ->and($project . '/storage/logs/.gitignore')->toBeFile() ->and($project . '/storage/sessions/.gitignore')->toBeFile() @@ -102,7 +106,6 @@ ->and($project . '/tests/Feature/RouteCacheCliTest.php')->not->toBeFile() ->and($project . '/tests/Feature/SkeletonDistributionTest.php')->not->toBeFile() ->and($project . '/vendor')->not->toBeDirectory() - ->and($project . '/bootstrap/cache/config/__manifest.php')->not->toBeFile() ->and($project . '/database')->not->toBeDirectory() ->and($cachedPhpFiles)->toBe([]) ->and($exportedComposer['require']['infocyph/foundation'] ?? null) @@ -151,8 +154,13 @@ expect($deployExitCode)->toBe(0, implode("\n", $deployOutput)) ->and($project . '/bootstrap/cache/config/__manifest.php')->toBeFile() ->and($project . '/bootstrap/cache/routes/fused.php')->toBeFile() - ->and($project . '/bootstrap/cache/console/commands.php')->toBeFile() - ->and($project . '/bootstrap/cache/modules.php')->toBeFile(); + ->and($project . '/bootstrap/cache/commands.php')->toBeFile() + ->and($project . '/bootstrap/cache/schedule.php')->toBeFile() + ->and($project . '/bootstrap/cache/optimize.php')->toBeFile() + ->and($project . '/bootstrap/cache/container/web.php')->toBeFile() + ->and($project . '/bootstrap/cache/container/cli.php')->toBeFile() + ->and($project . '/bootstrap/cache/container/worker.php')->toBeFile() + ->and($project . '/bootstrap/cache/container/scheduler.php')->toBeFile(); $clearOutput = []; $clearExitCode = 0; @@ -163,10 +171,15 @@ ), $clearOutput, $clearExitCode); expect($clearExitCode)->toBe(0, implode("\n", $clearOutput)) - ->and($project . '/bootstrap/cache/config/__manifest.php')->not->toBeFile() + ->and($project . '/bootstrap/cache/config')->not->toBeDirectory() ->and($project . '/bootstrap/cache/routes/fused.php')->not->toBeFile() - ->and($project . '/bootstrap/cache/console/commands.php')->not->toBeFile() - ->and($project . '/bootstrap/cache/modules.php')->not->toBeFile(); + ->and($project . '/bootstrap/cache/commands.php')->not->toBeFile() + ->and($project . '/bootstrap/cache/schedule.php')->not->toBeFile() + ->and($project . '/bootstrap/cache/optimize.php')->not->toBeFile() + ->and($project . '/bootstrap/cache/container/web.php')->not->toBeFile() + ->and($project . '/bootstrap/cache/container/cli.php')->not->toBeFile() + ->and($project . '/bootstrap/cache/container/worker.php')->not->toBeFile() + ->and($project . '/bootstrap/cache/container/scheduler.php')->not->toBeFile(); } finally { removeInfbyteDistributionFixture($fixture); } @@ -184,7 +197,7 @@ function removeInfbyteDistributionFixture(string $directory): void ); foreach ($files as $file) { - if ($file->isDir()) { + if ($file->isDir() && !$file->isLink()) { rmdir($file->getPathname()); } else { unlink($file->getPathname());