diff --git a/.claude/settings.json b/.claude/settings.json new file mode 100644 index 0000000..c8079f4 --- /dev/null +++ b/.claude/settings.json @@ -0,0 +1,27 @@ +{ + "hooks": { + "PreToolUse": [ + { + "matcher": "Edit|Write", + "hooks": [ + { "type": "command", "command": "\"$CLAUDE_PROJECT_DIR\"/vendor/bin/pollora-qa hook pre-edit" } + ] + } + ], + "PostToolUse": [ + { + "matcher": "Edit|Write", + "hooks": [ + { "type": "command", "command": "\"$CLAUDE_PROJECT_DIR\"/vendor/bin/pollora-qa hook post-edit" } + ] + } + ], + "Stop": [ + { + "hooks": [ + { "type": "command", "command": "\"$CLAUDE_PROJECT_DIR\"/vendor/bin/pollora-qa hook stop" } + ] + } + ] + } +} diff --git a/.github/workflows/harness.yml b/.github/workflows/harness.yml new file mode 100644 index 0000000..167170e --- /dev/null +++ b/.github/workflows/harness.yml @@ -0,0 +1,19 @@ +name: Harness + +on: + push: + branches: [main] + pull_request: + # A label (api-change, breaking) changes what the public surface gate allows. + types: [opened, synchronize, reopened, labeled, unlabeled] + schedule: + - cron: '17 3 * * *' + workflow_dispatch: + +permissions: {} + +jobs: + harness: + uses: Pollora/harness/.github/workflows/php-package.yml@v0.1.1 + permissions: + contents: read diff --git a/.gitignore b/.gitignore index 00eced4..729fb3d 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,5 @@ patches.lock.json tests/e2e/node_modules/ tests/e2e/playwright-report/ tests/e2e/test-results/ +# Pollora harness reports +/.harness/ diff --git a/.harness.json b/.harness.json new file mode 100644 index 0000000..7b94e33 --- /dev/null +++ b/.harness.json @@ -0,0 +1,8 @@ +{ + "profile": "integration", + "mode": "observe", + "ratchet": { + "phpstan_level": 6, + "phpstan_baseline": 0 + } +} diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..29d1a6a --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,56 @@ +# Agent instructions + + +## Pollora harness + +This repository follows the Pollora harness (profile `integration`, observe mode). The same gates run on your machine, in agent hooks and in CI; a pull request merges only when they pass. + +### Before you hand back + +- `composer qa:fast` runs the fast gates on what you changed (style, static analysis, architecture, dependencies). It must pass. +- `composer qa` runs every gate, as CI does. +- `vendor/bin/pollora-qa rules ` explains a rule quoted in a failure. + +### Never + +- Never edit `.harness.json`, `phpstan-baseline.neon`, `api-surface.json`: they hold the thresholds. If one is wrong, stop and explain why to the user. +- Never add `@phpstan-ignore`, a baseline entry or a `nosemgrep` comment without a written reason on the same line. +- Never weaken a test to make it pass, and never mark a test skipped to get a green run. +- Never break the public API (attributes, hooks fired, configuration keys, commands) in a minor version: deprecate first. + +### Always + +- A bug fix comes with a test that fails without the fix. +- Domain code depends on ports (interfaces), never on WordPress functions, Laravel or the Infrastructure layer. +- The common case of a public API needs no configuration; options are chained methods or named arguments. + +### Rules checked + +- **ARCH-01** Domain code imports neither Illuminate nor the Infrastructure or UI layers _(Pest arch, G1)_ +- **ARCH-02** Domain code calls no global WordPress function _(PHPStan rule pollora.noWordPressCallInDomain, G1)_ +- **ARCH-03** Application code depends on the Domain layer only _(Pest arch, G1)_ +- **ARCH-04** A core package requires neither illuminate/* nor laravel/*, unless listed in .harness.json _(pollora-qa deps, G1)_ +- **ARCH-05** The framework core references no third-party plugin (WooCommerce, ACF, Meta Box, WP Rocket) _(Semgrep, G1)_ +- **ARCH-06** Only integration packages, templates and projects depend on pollora/framework _(pollora-qa deps, G1)_ +- **ARCH-07** Every dependency used is declared, none declared is unused _(composer-dependency-analyser, G1)_ +- **ARCH-08** Attribute classes are final, readonly and declare their targets _(Pest arch, G1)_ +- **ARCH-09** declare(strict_types=1) in every file _(Pest arch + Pint, G1)_ +- **CONV-03** Laravel naming: ServiceProvider, Command, Exception, Repository, Factory suffixes _(Pest arch, G1)_ +- **CONV-04** Hooks fired by Pollora are named pollora// _(pollora-qa surface, G2)_ +- **CONV-05** Configuration lives under pollora.; env() is only called in config/ _(Pest arch, G1)_ +- **CONV-06** Commands are named pollora: or pollora:make- _(pollora-qa surface, G2)_ +- **CONV-07** Domain and Application code never calls app() or resolve(); it receives contracts _(Pest arch, G1)_ +- **CONV-10** One named exception per business case; never throw a bare \Exception _(Semgrep, G1)_ +- **CONV-11** Native types everywhere; PHPDoc only for generics, array shapes, @internal, @deprecated _(PHPStan + Pest type coverage, G1)_ +- **SEC-01** A REST route declared by attribute states its permission; a public route says so explicitly _(Semgrep, G1)_ +- **SEC-03** No $wpdb query built by interpolation without prepare() _(Semgrep, G1)_ +- **SEC-04** No unescaped Blade output {!! !!} outside a commented allowlist _(Semgrep, G1)_ +- **SEC-05** No unserialize, eval or extract _(Semgrep, G1)_ +- **SEC-08** No secret in the repository _(gitleaks, G1)_ +- **SEC-09** No dependency with a known vulnerability _(composer audit, G1)_ +- **QA-01** Ratchet: no metric gets worse (PHPStan level and baseline, coverage, mutation score) _(pollora-qa ratchet, G2)_ +- **QA-02** The harness files are protected: baseline, .harness.json and api-surface.json change only through a human _(Agent hooks + CODEOWNERS, G0)_ +- **QA-04** The public surface (attributes, hooks fired, configuration keys, commands) changes only with a label: api-change to add, breaking to remove or change _(pollora-qa surface, G2)_ +- **QA-05** No PHP signature breaks backward compatibility outside a major version _(roave/backward-compatibility-check, G2)_ +- **QA-03** A change to a core package keeps the framework green _(Downstream job, G2)_ + diff --git a/api-surface.json b/api-surface.json new file mode 100644 index 0000000..1505043 --- /dev/null +++ b/api-surface.json @@ -0,0 +1,43 @@ +{ + "version": 1, + "surface": { + "config:debugbar-pollora.admin": "key", + "config:debugbar-pollora.admin.enabled": "key", + "config:debugbar-pollora.admin.hidden_collectors": "key", + "config:debugbar-pollora.collectors": "key", + "config:debugbar-pollora.collectors.bridges": "key", + "config:debugbar-pollora.collectors.doctor": "key", + "config:debugbar-pollora.collectors.pollora": "key", + "config:debugbar-pollora.collectors.wp_assets": "key", + "config:debugbar-pollora.collectors.wp_blocks": "key", + "config:debugbar-pollora.collectors.wp_cache": "key", + "config:debugbar-pollora.collectors.wp_capabilities": "key", + "config:debugbar-pollora.collectors.wp_hooks": "key", + "config:debugbar-pollora.collectors.wp_http": "key", + "config:debugbar-pollora.collectors.wp_languages": "key", + "config:debugbar-pollora.collectors.wp_queries": "key", + "config:debugbar-pollora.collectors.wp_request": "key", + "config:debugbar-pollora.collectors.wp_timeline": "key", + "config:debugbar-pollora.enabled": "key", + "config:debugbar-pollora.iframes": "key", + "config:debugbar-pollora.options": "key", + "config:debugbar-pollora.options.bridges": "key", + "config:debugbar-pollora.options.bridges.query_monitor": "key", + "config:debugbar-pollora.options.wp_capabilities": "key", + "config:debugbar-pollora.options.wp_capabilities.backtrace": "key", + "config:debugbar-pollora.options.wp_hooks": "key", + "config:debugbar-pollora.options.wp_hooks.count_filters": "key", + "config:debugbar-pollora.options.wp_hooks.timings": "key", + "config:debugbar-pollora.options.wp_hooks.timings_limit": "key", + "config:debugbar-pollora.options.wp_queries": "key", + "config:debugbar-pollora.options.wp_queries.hard_limit": "key", + "config:debugbar-pollora.options.wp_queries.slow_threshold": "key", + "config:debugbar-pollora.options.wp_queries.soft_limit": "key", + "config:debugbar-pollora.options.wp_queries.trace": "key", + "hook:pollora/async/dispatched": "hook", + "hook:pollora/debugbar/message": "hook", + "hook:pollora/debugbar/register": "action", + "hook:pollora/debugbar/start": "hook", + "hook:pollora/debugbar/stop": "hook" + } +} diff --git a/composer.json b/composer.json index 2c7d322..9a8f81f 100644 --- a/composer.json +++ b/composer.json @@ -19,9 +19,10 @@ "require": { "php": "^8.3", "fruitcake/laravel-debugbar": "^4.4", - "pollora/framework": "^13.35.3" + "pollora/framework": "^13.36.0" }, "require-dev": { + "antecedent/patchwork": "^2.2", "brain/monkey": "^2.7", "laravel/pint": "^1.22", "mockery/mockery": "^1.6", @@ -29,7 +30,9 @@ "pestphp/pest": "^3.8", "php-stubs/wordpress-stubs": "^7.1", "phpstan/phpstan": "^2.1", - "szepeviktor/phpstan-wordpress": "^2.0" + "pollora/harness": "^0.1.1", + "szepeviktor/phpstan-wordpress": "^2.0", + "johnbillion/extended-cpts": "^5.0.3" }, "autoload": { "psr-4": { @@ -68,7 +71,9 @@ "phpstan": "phpstan analyse --memory-limit=1G", "phpstan:ci": "phpstan analyse --memory-limit=1G --no-progress", "lint": "pint", - "lint:check": "pint --test" + "lint:check": "pint --test", + "qa": "@php vendor/bin/pollora-qa check", + "qa:fast": "@php vendor/bin/pollora-qa check --fast" }, "scripts-descriptions": { "test": "Run the whole quality gate as CI does", diff --git a/phpstan.neon b/phpstan.neon index 65bc89d..fa91882 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -1,5 +1,6 @@ includes: - - vendor/szepeviktor/phpstan-wordpress/extension.neon + - vendor/pollora/harness/presets/phpstan/base.neon + # Larastan is not included yet: it boots a Laravel application and this package's test stubs cannot be loaded with it parameters: level: 6 diff --git a/rector.php b/rector.php new file mode 100644 index 0000000..696f504 --- /dev/null +++ b/rector.php @@ -0,0 +1,11 @@ +withSkip([ + // Each skip says why, like the framework's rector.php. + ]); diff --git a/tests/Arch.php b/tests/Arch.php new file mode 100644 index 0000000..3aa969e --- /dev/null +++ b/tests/Arch.php @@ -0,0 +1,15 @@ +register();