From a94f99d2cc792d7f07b0a6268c8bc04181e805be Mon Sep 17 00:00:00 2001 From: Olivier Gorzalka Date: Wed, 28 Oct 2026 09:09:58 +0100 Subject: [PATCH 1/3] chore: the Pollora harness checks the package, in enforce mode Declares laravel/mcp and pollora/discovery, which the code uses; loads the test stubs from Pest only (PHPStan loaded them with WordPress's stubs and crashed on the redeclared functions); requires strict types in pint.json; records the public surface. --- .claude/settings.json | 27 +++++++++++++ .github/workflows/harness.yml | 19 +++++++++ .gitignore | 4 +- .harness.json | 8 ++++ AGENTS.md | 56 ++++++++++++++++++++++++++ CHANGELOG.md | 1 + api-surface.json | 15 +++++++ composer-dependency-analyser.php | 14 +++++++ composer.json | 23 +++++++---- config/nectar.php | 2 + patches.lock.json | 29 +++++++++++++ phpstan.neon | 3 ++ pint.json | 7 +++- src/Mcp/Tools/ActiveThemeInfo.php | 2 +- src/Mcp/Tools/DiscoveredComponents.php | 2 +- src/Mcp/Tools/WordPressInfo.php | 2 +- tests/Arch.php | 15 +++++++ tests/Pest.php | 3 ++ 18 files changed, 218 insertions(+), 14 deletions(-) create mode 100644 .claude/settings.json create mode 100644 .github/workflows/harness.yml create mode 100644 .harness.json create mode 100644 AGENTS.md create mode 100644 api-surface.json create mode 100644 composer-dependency-analyser.php create mode 100644 patches.lock.json create mode 100644 tests/Arch.php 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..2c6db3d --- /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.11 + permissions: + contents: read diff --git a/.gitignore b/.gitignore index 112570e..0bb9f15 100644 --- a/.gitignore +++ b/.gitignore @@ -7,4 +7,6 @@ composer.lock .vscode/ *.swp *.swo -.DS_Store \ No newline at end of file +.DS_Store +# Pollora harness reports +/.harness/ diff --git a/.harness.json b/.harness.json new file mode 100644 index 0000000..540e8f2 --- /dev/null +++ b/.harness.json @@ -0,0 +1,8 @@ +{ + "profile": "integration", + "mode": "enforce", + "ratchet": { + "phpstan_level": 5, + "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/CHANGELOG.md b/CHANGELOG.md index d6f2583..3d2bfb3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed +- `laravel/mcp` (`^1.0`) and `pollora/discovery` (`^0.1 || ^1.0`) are declared: the package uses their classes and relied on them being installed by `laravel/boost` and `pollora/framework`. `laravel/mcp` 0.x, which Boost also accepts, is no longer accepted - The REST examples of the `pollora-rest-api` skill and the core guidelines declare a permission on every route (`Everyone`, `IsLoggedIn`, `IsAdmin`): `pollora/wp-rest` 0.2 deprecates a route without one ## [1.7.0] - 2026-10-14 diff --git a/api-surface.json b/api-surface.json new file mode 100644 index 0000000..196d956 --- /dev/null +++ b/api-surface.json @@ -0,0 +1,15 @@ +{ + "version": 1, + "surface": { + "command:nectar:mcp": "command", + "config:nectar.enabled": "key", + "config:nectar.mcp": "key", + "config:nectar.mcp.tools": "key", + "config:nectar.mcp.tools.exclude": "key", + "config:nectar.mcp.tools.include": "key", + "config:nectar.wp_cli": "key", + "config:nectar.wp_cli.allowed_commands": "key", + "hook:pollora/async/recover": "hook", + "hook:pollora/async/run": "hook" + } +} diff --git a/composer-dependency-analyser.php b/composer-dependency-analyser.php new file mode 100644 index 0000000..a325852 --- /dev/null +++ b/composer-dependency-analyser.php @@ -0,0 +1,14 @@ +ignoreErrorsOnPackage('laravel/framework', [ErrorType::SHADOW_DEPENDENCY]); diff --git a/composer.json b/composer.json index 14dbc37..99202ed 100644 --- a/composer.json +++ b/composer.json @@ -11,8 +11,10 @@ ], "type": "library", "require": { - "php": "^8.3", "laravel/boost": "^2.0", + "laravel/mcp": "^1.0", + "php": "^8.3", + "pollora/discovery": "^0.1 || ^1.0", "pollora/framework": "^12.0 || ^13.0" }, "require-dev": { @@ -20,7 +22,8 @@ "laravel/pint": "^1.0", "orchestra/testbench": "^9.0|^10.0|^11.0", "pestphp/pest": "^3.0", - "phpstan/phpstan": "^2.0" + "phpstan/phpstan": "^2.0", + "pollora/harness": "^0.1.11" }, "autoload": { "psr-4": { @@ -30,10 +33,7 @@ "autoload-dev": { "psr-4": { "Pollora\\Nectar\\Tests\\": "tests/" - }, - "files": [ - "tests/Helpers/wordpress-stubs.php" - ] + } }, "extra": { "laravel": { @@ -48,7 +48,12 @@ "wikimedia/composer-merge-plugin": true, "cweagans/composer-patches": true }, - "sort-packages": true + "sort-packages": true, + "audit": { + "ignore-abandoned": [ + "symplify/rule-doc-generator-contracts" + ] + } }, "scripts": { "test": [ @@ -60,7 +65,9 @@ "test:types": "phpstan analyse", "test:unit": "pest", "lint": "pint", - "refacto": "rector" + "refacto": "rector", + "qa": "@php vendor/bin/pollora-qa check", + "qa:fast": "@php vendor/bin/pollora-qa check --fast" }, "minimum-stability": "dev", "prefer-stable": true diff --git a/config/nectar.php b/config/nectar.php index 7ac1be9..3b4f966 100644 --- a/config/nectar.php +++ b/config/nectar.php @@ -1,5 +1,7 @@ filter(fn ($file) => $file->getExtension() === 'php') + ->filter(fn ($file): bool => $file->getExtension() === 'php') ->map(fn ($file) => $file->getFilenameWithoutExtension()) ->values() ->all(); diff --git a/src/Mcp/Tools/DiscoveredComponents.php b/src/Mcp/Tools/DiscoveredComponents.php index ea7c5d9..802f868 100644 --- a/src/Mcp/Tools/DiscoveredComponents.php +++ b/src/Mcp/Tools/DiscoveredComponents.php @@ -24,7 +24,7 @@ class DiscoveredComponents extends Tool * * @var array */ - private const TYPE_MAP = [ + private const array TYPE_MAP = [ 'post_type' => 'post_types', 'taxonomy' => 'taxonomies', 'hook' => 'hooks', diff --git a/src/Mcp/Tools/WordPressInfo.php b/src/Mcp/Tools/WordPressInfo.php index 16fc5aa..53a6d4b 100644 --- a/src/Mcp/Tools/WordPressInfo.php +++ b/src/Mcp/Tools/WordPressInfo.php @@ -26,7 +26,7 @@ public function handle(Request $request): Response if (! function_exists('get_plugins') && defined('ABSPATH')) { $pluginFile = ABSPATH.'wp-admin/includes/plugin.php'; if (file_exists($pluginFile)) { - require_once $pluginFile; // @phpstan-ignore requireOnce.fileNotFound + require_once $pluginFile; } } 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(); diff --git a/tests/Pest.php b/tests/Pest.php index 2e65600..f29b165 100644 --- a/tests/Pest.php +++ b/tests/Pest.php @@ -4,4 +4,7 @@ use Pollora\Nectar\Tests\TestCase; +// Loaded here, not by Composer's autoload-dev: PHPStan loads that autoload, then WordPress's stubs, and both declare these functions. +require_once __DIR__.'/Helpers/wordpress-stubs.php'; + uses(TestCase::class)->in('Feature'); From 2e251dfd3f582cc483f383cadc9f7ee13a55f45f Mon Sep 17 00:00:00 2001 From: Olivier Gorzalka Date: Wed, 28 Oct 2026 09:10:12 +0100 Subject: [PATCH 2/3] chore: do not track patches.lock.json, composer-patches generates it --- .gitignore | 1 + patches.lock.json | 29 ----------------------------- 2 files changed, 1 insertion(+), 29 deletions(-) delete mode 100644 patches.lock.json diff --git a/.gitignore b/.gitignore index 0bb9f15..6dd55c4 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,4 @@ composer.lock .DS_Store # Pollora harness reports /.harness/ +patches.lock.json diff --git a/patches.lock.json b/patches.lock.json deleted file mode 100644 index a0197fc..0000000 --- a/patches.lock.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - "_hash": "b1670ee28b3d45a162495e27466ecfa3f672e61993e7ca3c41f50d944962e83c", - "patches": { - "johnpbloch/wordpress-core": [ - { - "package": "johnpbloch/wordpress-core", - "description": "Patch __ method in l10n to stop conflicting with Laravel", - "url": "https://raw.githubusercontent.com/Pollora/framework/5681565cb42923cdec3b1c0e855a2721b8bc3724/patches/wordpress-core.patch", - "sha256": "e54909905a89c376f68b0dc4988d7e48bea9929491675bab232eab068c929b33", - "depth": 1, - "extra": { - "provenance": "dependency:johnpbloch/wordpress-core" - } - } - ], - "php-stubs/wordpress-stubs": [ - { - "package": "php-stubs/wordpress-stubs", - "description": "Rename __ to __wp so Laravel can declare __()", - "url": "https://raw.githubusercontent.com/Pollora/framework/b26034b3306c96098f756c0ca80631a596db25c9/patches/wordpress-stubs.patch", - "sha256": "5374bec5ea271947b027cc9801b32f06fab21d637b586c852ad5ee730e4e534f", - "depth": 1, - "extra": { - "provenance": "dependency:php-stubs/wordpress-stubs" - } - } - ] - } -} From b1acae2ea5dbaeb4a459a52f12076b1ffc54840c Mon Sep 17 00:00:00 2001 From: Olivier Gorzalka Date: Wed, 28 Oct 2026 09:12:43 +0100 Subject: [PATCH 3/3] chore: semgrep skips the migration prompt, whose snippet shows removed code --- .semgrepignore | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 .semgrepignore diff --git a/.semgrepignore b/.semgrepignore new file mode 100644 index 0000000..cf8bd59 --- /dev/null +++ b/.semgrepignore @@ -0,0 +1,3 @@ +# Documentation, not a template a site renders: its Boost snippet shows the Loop facade calls +# that Pollora 13 removed, `{!! Loop::title() !!}` included. +src/Mcp/Prompts/UpgradePolloraV13/upgrade-pollora-v13.blade.php