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..6dd55c4 100644 --- a/.gitignore +++ b/.gitignore @@ -7,4 +7,7 @@ composer.lock .vscode/ *.swp *.swo -.DS_Store \ No newline at end of file +.DS_Store +# Pollora harness reports +/.harness/ +patches.lock.json 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/.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 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');