diff --git a/.browserslistrc b/.browserslistrc new file mode 100644 index 0000000..0152f61 --- /dev/null +++ b/.browserslistrc @@ -0,0 +1 @@ +extends @wordpress/browserslist-config diff --git a/.claude/skills/README.md b/.claude/skills/README.md new file mode 100644 index 0000000..935bcd7 --- /dev/null +++ b/.claude/skills/README.md @@ -0,0 +1,21 @@ +# AI skills + +Skills for AI assistants (Claude Code, Cursor, and any tool that reads the Claude Code skill convention) that drive this plugin's setup and the `@rtcamp/wp-tooling` scaffold engine. Each skill is a directory with a `SKILL.md` (frontmatter `name:` + `description:`, portable Markdown body). + +| Skill | Invoke | What it does | +|---|---|---| +| [`init/`](init/SKILL.md) | `/init` | Turn this cloned skeleton into a named project and manage it afterwards: rename the starter tokens, keep/remove the example sets, toggle optional features (Tailwind, HMR, Dev Tools). Drives `npm run init`. | +| [`scaffold/`](scaffold/SKILL.md) | `/scaffold` | Add one feature (CPT, taxonomy, block, REST controller, shortcode, cron, CLI, settings page, user role, service, CI workflow) via `npx wp-tooling add`, TDD-first. | +| [`setup/`](setup/SKILL.md) | `/setup` | Generic tooling bootstrap for plugins and themes, shipped with `@rtcamp/wp-tooling`. Not a personalization path for this skeleton; use `/init`. | + +`init` is skeleton-specific. `scaffold` tracks `@rtcamp/wp-tooling` and introspects the project, so it stays correct as the layout evolves. `setup` has no Copilot counterpart. + +Developer-facing descriptions of both routes are in [docs/initialization.md](../../docs/initialization.md) and [docs/scaffolding.md](../../docs/scaffolding.md). + +**Copilot parity:** `init` and `scaffold` also exist for GitHub Copilot as prompt files in [`.github/prompts/`](../../.github/prompts/) (`/init`, `/scaffold`), kept consistent with these skills. Shared conventions and the knowledge-graph (graphify) policy live in [`AGENTS.md`](../../AGENTS.md). + +## Safety + +These skills are opinionated about safety and never: run a package manager or `npm run build` without consent; read, log, or transmit secret values; apply cross-file wiring without showing the diff and getting consent; or commit, push, or open PRs without approval. `init` additionally never runs a destructive setup without confirming the resolved values against a clean working tree. + +Consented exceptions to the package-manager rule: `npm run init` (the project's own setup script), and the `npm install`/`composer install` `init` runs at the start of setup. After any code change, the skills refresh the LOCAL graph with `graphify update .`; they never commit it and never run the cross-repo `/graphify . --update` or `merge-graphs` (see [AGENTS.md](../../AGENTS.md)). diff --git a/.claude/skills/init/SKILL.md b/.claude/skills/init/SKILL.md new file mode 100644 index 0000000..c937c5b --- /dev/null +++ b/.claude/skills/init/SKILL.md @@ -0,0 +1,118 @@ +--- +name: init +description: Set up this cloned plugin-elementary into a named project, or manage its identity and capabilities later. Installs dependencies (`npm install`, `composer install`), then drives `npm run init`. Always confirms before destructive or install steps; expects a clean working tree. +--- + +# init + +Two jobs: **bootstrap + setup** a fresh clone into a real plugin, or **manage** an already-set-up project. Always interactive: gather inputs first, confirm the resolved plan, then act. This skill is a thin front end for `npm run init` - it does not reimplement what the engine already asks for. + +## Use for +- First run: install dependencies, rename starter tokens, pick capabilities to keep. +- First run WITH a feature brief: after rename + capabilities, implement the described features by chaining to the **scaffold** skill (TDD), replacing the matching `Example*` placeholders (step 5). +- Later: rename / re-prefix, toggle features (Tailwind, HMR, Dev Tools). + +## Do not use for +- Adding a feature class → the **scaffold** skill (`npx wp-tooling add`). +- Bug fixes, refactors, hand-written features. +- Re-running init to change capabilities after first setup. Removal is one-shot (markers are consumed); a second run leaves dangling `Main::CLASSES` refs. Set the set ONCE; change it later via scaffold. + +## Be interactive (required) +Ask the developer for every input the chosen path needs, in ONE batched message, before acting. Never assume a project name. Show derived values back in that same exchange. Wait for explicit consent before any install, file rewrite, or destructive step. If an answer is missing, ask; do not guess. + +## Plan and announce (required) - developer experience first +1. **State the plan up front** in one short message: the mode (setup / manage) and the ordered steps for THIS request - detect mode -> install deps -> ask + run init -> (setup with a brief) hand each feature to the scaffold skill. +2. **Maintain a live TODO list** (`TodoWrite` in Claude Code): one entry per planned step, exactly one `in_progress` at a time. +3. **Announce each step with a short title** and report its outcome in one line. Never go silent during a long step. + +## Steps + +### 1. Detect mode +```bash +test -f .wp-scaffold.json && echo manage || echo setup +``` +No `.wp-scaffold.json` → **setup**. Present → **manage** (read it for current identity; `npm run init -- --list` shows feature status). + +### 2. Install dependencies +With consent, run: +```bash +npm install +composer install +``` +Skip either that's already installed and current. If consent is declined, surface both as developer actions and stop - init cannot run without them. + +Then verify a clean working tree (`git status`) - init rewrites files irreversibly, and a clean tree is the only undo. If dirty, ask to commit/stash. + +### 3. Ask, confirm, run - one step +`npm run init` is a single command; don't turn gathering its inputs into a multi-round-trip wizard. + +**Setup:** in ONE message ask for project name (required, e.g. `Acme Blog` → namespace `Acme_Blog\Features`, package `rtcamp/acme-blog-features`, text domain, prefixes, main file - show these back), version (default `1.0.0`), which capability sets to remove, and which features to enable (defaults: keep all sets, `hmr` on, `tailwind` and `dev-tools` off - see Capability model). The engine derives the tokens itself; do not read the engine source (`identity.js` etc.) - the mapping above is the contract, and the graph answers any deeper question (see the graphify policy in `AGENTS.md`). + +**Manage:** ask which of identity / features to change. It's a single flag on an existing project - no wizard needed. + +Get explicit consent on the resolved values (init is destructive), then run: +```bash +# Setup: +npm run init -- --name="Acme Blog" --version=1.0.0 --yes \ + --remove-examples=cron,rest --features=hmr,tailwind,dev-tools +# Manage: +npm run init -- --list +npm run init -- --enable=dev-tools --yes +npm run init -- --features=hmr --yes # exact enabled set (empty = none) +``` +- `--keep-examples` keeps all; `--remove-examples` (no value) removes all; `--remove-examples=a,b` removes listed keys. +- `--features=a,b` sets the exact enabled set; `--enable`/`--disable` are deltas. +- `--yes` requires `--name`. For the guided wizard, run bare `npm run init` (space toggles, enter confirms) - the wizard already asks everything above, so prefer it over hand-building flags when the developer has no strong opinion on the exact command. +- `npm run init` also runs `npm run sync-ai`; that is expected. + +**Setup WITH a feature brief - clear the briefed examples in THIS step (not a separate round-trip):** keep the capabilities the brief needs (do NOT list them in `--remove-examples`); remove every other capability. Then, batched with the init run, strip the kept-briefed capabilities of their demo code so scaffold can fill them: for each, delete the `Example*` class file(s) under `inc/Modules//`, delete `tests/php/Test.php`, and set that module's `get_classes()` to `return [];` (drop the example `use` imports). The module stays wired in `Main::CLASSES` - only its example contents go, leaving an empty, already-wired module ready for scaffold. Example: brief = Testimonial CPT + Industry taxonomy + draft REST -> keep `post-types`/`taxonomies`/`rest`, `--remove-examples=blocks,shortcodes,cli,cron,settings,roles,cache,transients,admin`, then empty the PostTypes/Taxonomies/REST modules. + +### 4. After init +- Tailwind enabled → developer runs `npm install` (it added `src/css/tailwind.css`, `postcss.config.js`, and pinned `@rtcamp/tailwind-config`). +- Dev Tools enabled → it's still a private package (`rtcamp/wp-dev-tools`, VCS-sourced): developer runs `composer update rtcamp/wp-dev-tools`, which may need GitHub auth, then `npm run dev:connect`. +- `composer dump-autoload` (engine runs it when `composer.json` is present). +- Trust the engine's own output to verify (it reports the removed sets, drops their `Main::CLASSES` lines and tests, and regenerates the autoloader). To confirm a symbol or reference, run a single `graphify query`/`affected` against the graph - never grep `Main.php`/`inc/` to check removal. +- If you hand-edited PHP and are NOT handing off to scaffold (e.g. a manage-mode dangling `Main::CLASSES` cleanup), run `composer lint` on the change and fix it. When a brief follows (step 5), leave the lint/PHPStan/test gates to the scaffold skill - init does not run them. +- **Refresh the LOCAL graph** with `graphify update .` (plugin slice, tree-sitter, no API, seconds). **Never commit or push `graphify-out/graph.json`** - the committed file is a maintainer-maintained cross-repo baseline (rebuilt and pushed when this or a shared repo changes); your local refresh stays local. Do NOT run the cross-repo `/graphify . --update` subagent or `merge-graphs` (expensive; clobbers the baseline). If graphify is not installed, say so in one line; do not block. +- Report: new identity, capabilities removed/kept, features toggled, outstanding developer actions. + +### 5. Implement the brief by handing off to scaffold (setup only, when features were described) +By this point step 3 has already emptied the briefed capabilities (their `Example*` classes/tests are deleted and the modules are empty but still wired in `Main::CLASSES`). init's remaining job is purely to **hand each described feature to the scaffold skill** - it does NOT build the class, write or run tests, set up the test env (`npm run wp-env start`), or run the lint/PHPStan gates. The **scaffold skill takes over** and owns all of that. + +For each described feature, **invoke the scaffold skill**, passing the brief (e.g. a `testimonial` CPT; an `industry` taxonomy attached to `testimonial`; a REST controller that creates testimonials in `draft`). The scaffold skill owns conventions, the `wp-tooling add` call, wiring into the already-present module, the TDD loop, test execution, and the gates - report its result; do not duplicate that work here. + +**Pass your findings to scaffold (speeds it up).** init has already resolved the project identity while renaming, so hand those values to the scaffold skill in the same message instead of making it re-derive them. State explicitly: the resolved **PHP namespace root** (e.g. `Acme\Testimonials`), **base_path** (`inc`), **tests_namespace** (e.g. `Acme\Testimonials\Tests`), **tests_path** (`tests/php`), **text_domain** and **slug** (e.g. `testimonial-features`), the **plugin display name**, and **which capabilities step 3 kept-and-emptied** (their `inc/Modules//` folders already exist and are wired in `Main::CLASSES`, so scaffold wires into them rather than creating a module). With these provided, the scaffold skill skips its own `composer.json` / plugin-header / `Main.php` discovery reads. It still reads source files when it needs exact code (the graph and a handoff are for orientation, not a substitute for reading). + +For a kept capability the brief did NOT mention, leave its `Example*` but tell the developer it is demo-only, not for deployment, and offer to scaffold a real class or drop the capability. + +End state: init has renamed, trimmed capabilities, and emptied the briefed examples (step 3); the scaffold skill has implemented the real features. + +## Capability model +ONE "Select the capabilities to include" prompt. Each is keep-or-remove; removing deletes it entirely (concrete classes, `inc/Modules/.php`, its `Main::CLASSES` line, coupled Core regions, feature deps). + +| Category | Keys | +|---|---| +| Content | `post-types`, `taxonomies` | +| Editor & Front-end | `blocks`, `shortcodes`, `tailwind` (feature) | +| APIs & Automation | `rest`, `cli`, `cron` | +| Admin | `settings`, `admin`, `roles` | +| Utilities | `cache`, `transients` | +| Developer Tooling | `test-measure` (CI workflow), `hmr` (feature), `dev-tools` (feature) | + +Sets are kept by default; pass keys to `--remove-examples` to drop. Features: `hmr` on, `tailwind` and `dev-tools` off; toggle via `--features`/`--enable`/`--disable`. `dev-tools` (`rtcamp/wp-dev-tools`, runtime telemetry over MCP - Query Monitor + the MCP Adapter) is the one feature still sourced from a private VCS repo; enabling it needs GitHub auth on `composer update` (see step 4). Everything else here installs from public registries. + +### Changing capabilities after setup +Add later → scaffold skill / `npx wp-tooling add /` (writes the class, wires the module). Remove later → delete its `inc/Modules//` classes + `Main::CLASSES` line by hand. Toggle a feature any time via `npm run init -- --enable=`/`--disable=`. Do not re-run init to change the capability set. + +## Hard rules +- **BASE (see AGENTS.md guardrails):** never run history/remote `git`/`gh` (commit, push, `branch -D`, `reset --hard`, PR, issue comment, `gh secret set`) - print them as developer actions. Never do a destructive operation outside this plugin directory. +- Never commit or push `graphify-out/graph.json`; local graph refreshes stay local. +- Package managers only with consent: `npm install`, `composer install` (step 2), and `npm run init`. Outside those, surface install commands as developer actions. +- Never run a destructive init without confirming resolved values, on a clean tree. +- Never commit, push, open PRs, or edit `.wp-scaffold.json` by hand. +- Never invent flags. Supported: `--name`, `--version`, `--yes`, `--keep-examples`, `--remove-examples[=...]`, `--features`, `--enable`, `--disable`, `--reinit`, `--list`, `--clean`, `--help`. Run `npm run init -- --help` if unsure. + +## Reference +- Engine: `@rtcamp/wp-tooling/init` (via `bin/init.js`). Capability map: `bin/scaffold.config.js`. Dev Tools feature: `bin/features/dev-tools.js`. +- Conventions renamed into: `AGENTS.md`, `.github/instructions/structure.instructions.md`. +- Graphify policy: `AGENTS.md`. diff --git a/.claude/skills/scaffold/SKILL.md b/.claude/skills/scaffold/SKILL.md new file mode 100644 index 0000000..4ad936c --- /dev/null +++ b/.claude/skills/scaffold/SKILL.md @@ -0,0 +1,266 @@ +--- +name: scaffold +description: Add a scaffold (PHP class, block, CI workflow) to the current project using @rtcamp/wp-tooling. TDD-first - derive test cases from the developer brief, scaffold via the engine, write tests, then implement to green under a red-green-refactor loop. Never runs package-manager commands or writes secret values; surfaces them as developer actions. +--- + +# scaffold + +Drive `@rtcamp/wp-tooling`. Map the developer's request to a scaffold, derive test cases first, invoke the engine, expand tests, implement to green, report. + +This skill is the canonical wp-tooling scaffold skill, tailored to this skeleton's structure (`inc/Modules/`, namespace `\Modules\`, tests in `tests/php/`) and the private-package pilot. + +## Use for + +CPT, taxonomy, REST controller, dynamic block, shortcode, cron, CLI command, admin/settings page, user role, plain Registrable service, framework module, CI/CD workflow. + +## Do not use for + +Refactors, bug fixes, hand-written features, anything no scaffold covers. + +## Workflow + +### 0. Plan and announce - before any other work + +Before discovery, scaffolding, or coding, write a TODO list covering every step you intend to run for this task and show it to the developer. Use the host's task-tracking surface (`TodoWrite` in Claude Code; the host equivalent elsewhere). + +Minimum entries: + +1. Introspect project (§2). +2. Derive test-case checklist (§4) and confirm with developer. +3. Scaffold call(s) - one entry per kind for multi-kind features. +4. Apply wiring (§6a) - one entry per `ai.wiring` snippet, with consent. +5. Expand tests, run red (§7 A-B). +6. Implement to green (§7 C-E), one test at a time. +7. Refactor + final gates (§7 F-G). +8. Report (§9). + +Update the list in real time. Mark each entry `in_progress` before starting it and `completed` the moment it is done. Exactly one entry `in_progress` at any time. Add new entries when the work reveals them (e.g. a missing kind-module, a stuck-loop escalation). + +This makes progress legible to the developer and gives them a stable surface to interrupt or re-prioritise. + +### 1. Discover + +```bash +npx wp-tooling list --json +``` + +Result: `{ scaffolds: [{ id, slug, category, kind, origin, counts }, ...] }`. Pick one `category/slug`. If ambiguous, ask the developer with candidates. Never guess. Entries with `origin: "remote"` live in another repo: their manifest is fetched on the first `add`, so `counts` is `null` here and the kind is always `template`. That first `add` does network I/O and can fail with `EFETCHFAIL` (see Engine errors) - treat a remote scaffold exactly like a local one once it resolves; the only difference is the fetch. + +### 2. Introspect once per session (cache result) + +**Reuse `/init`'s findings if it handed off to you in this session.** When `/init` chained here, you already established the resolved identity and conventions while running init - root namespace, base path (`inc/`), tests namespace + path (`\Tests` -> `tests/php/`), text domain, and constant prefix. They are facts you set, not guesses: reuse them and SKIP the `composer.json` / plugin-header / `Main.php` reads below that only recover them. You still read a sample implementation for the registration pattern (next paragraph). Because init deleted the briefed `Example*`, sample the pattern from the framework abstract for the kind (`vendor/rtcamp/wp-primitives/inc/Contracts/Abstracts/Abstract.php`) or any remaining example; do not guess. + +**The graph saves tokens for orientation only; read the file for any data you copy.** This repo commits a queryable graph (`graphify-out/graph.json`). Use it to LOCATE things cheaply so you open fewer files: which classes implement a kind, what references a symbol, the path between two classes - `graphify query "..."`, `graphify explain ""`, `graphify affected ""`, `graphify path "A" "B"`. The graph is structural (not the full source), so it is NOT a source of truth for the patterns you act on. For registration shape, exact namespace, how a module's `get_classes()` lists classes, and wiring location, **read the actual file** - 100% accuracy beats a few saved tokens. (If you or init changed code this session, refresh the local slice first with `graphify update .`, seconds, **never push `graphify-out/graph.json`** - AGENTS.md graphify policy.) + +Read, in order (use the graph to find them fast; read the files for their contents): + +- `composer.json` -> `autoload.psr-4` (root namespace + base path; here `\` -> `inc/`, e.g. `RT_Testimonial\Features\` -> `inc/`). `autoload-dev.psr-4` for the tests namespace (`\Tests\` -> `tests/php/`). +- `package.json` -> scripts. For block scaffolds, parse `build:blocks` for `--output-path=` and pass as `--build_dir`. Default `assets/build/blocks`. +- Main plugin file -> bootstrap class (`Main`) + `Main::CLASSES`. +- 2-3 existing implementations of the same kind under `inc/Modules//` (e.g. `Example*`): registration pattern, class-name suffix, sub-namespace, and how the module's `get_classes()` lists them. **Read these files** - they are the source of truth for the patterns you copy; use `graphify` only to find them. +- Block scaffolds: sample one `src/blocks/*/block.json` for vendor prefix and source dir. +- CI scaffolds: sample one `.github/workflows/*.yml` for filename and trigger style. + +Anchors (`// scaffold::classes`) are hints, not ground truth. Sampled patterns win. + +Confirm findings with the developer in one short message. Proceed on confirmation. + +### 3. Canonical layout + +Files group by **kind**, never by feature. `` = this skeleton's `composer.json` `autoload.psr-4` root (e.g. `RT_Testimonial\Features` -> `inc/`); tests autoload via `\Tests` -> `tests/php/`. **The engine's own defaults target a different layout (`includes/...`, `Inc\...`, `tests/...`), so you MUST pass this skeleton's conventions on every `add` (§5).** + +| Scaffold | Source dir | Source ns | Test dir | Test ns | Module file (wire here) | +|---|---|---|---|---|---| +| `wp/cpt` | `inc/Modules/PostTypes/` | `\Modules\PostTypes` | `tests/php/` | `\Tests` | `inc/Modules/PostTypes.php` | +| `wp/taxonomy` | `inc/Modules/Taxonomies/` | `\Modules\Taxonomies` | `tests/php/` | `\Tests` | `inc/Modules/Taxonomies.php` | +| `wp/block-dynamic` | `inc/Modules/Blocks/` + `src/blocks//` + `assets/build/blocks//` | `\Modules\Blocks` | `tests/php/` | `\Tests` | `inc/Modules/Blocks.php` | +| `wp/rest` | `inc/Modules/REST/` | `\Modules\REST` | `tests/php/` | `\Tests` | `inc/Modules/REST.php` | +| `wp/shortcode` | `inc/Modules/Shortcodes/` | `\Modules\Shortcodes` | `tests/php/` | `\Tests` | `inc/Modules/Shortcodes.php` | +| `wp/admin-page` | `inc/Modules/Admin/` | `\Modules\Admin` | `tests/php/` | `\Tests` | `inc/Modules/Admin.php` | +| `wp/settings-page` | `inc/Modules/Settings/` | `\Modules\Settings` | `tests/php/` | `\Tests` | `inc/Modules/Settings.php` | +| `wp/user-role` | `inc/Modules/Roles/` | `\Modules\Roles` | `tests/php/` | `\Tests` | `inc/Modules/Roles.php` | +| `wp/cli` | `inc/Modules/CLI/` | `\Modules\CLI` | `tests/php/` | `\Tests` | `inc/Modules/CLI.php` | +| `wp/cron` | `inc/Modules/Cron/` | `\Modules\Cron` | `tests/php/` | `\Tests` | `inc/Modules/Cron.php` | +| `wp/registrable` | `inc/Modules//` | `\Modules\` | `tests/php/` | `\Tests` | `inc/Modules/.php` | + +Match the existing directory case exactly (`REST`, `CLI`, not `Rest`/`Cli`). + +**Modules host one kind each. No `Modules//...`.** A multi-kind feature (e.g. Testimonials = CPT + taxonomy + REST) spans the per-kind directories and wires into each kind's module. + +If the project already has a per-feature module folder, flag as anti-pattern. Offer migration before adding new artifacts. Do not scaffold into it. + +### 4. Derive test cases from the developer brief - BEFORE any scaffold call + +Write a test-case checklist covering: + +- **Happy path** - central behaviour the developer stated. +- **Edge cases** - empty/missing/boundary inputs, large input. +- **Error paths** - invalid input, missing auth, wrong capability. +- **Integration** - kind-specific: + - `wp/cpt`: `post_type_exists()`, supports, REST exposure, attached taxonomies. + - `wp/taxonomy`: `taxonomy_exists()`, attached object types, term assignment. + - `wp/rest`: route appears in `rest_get_server()->get_routes()`, permission check, request/response schema, dedupe behaviour. + - `wp/block-dynamic`: block name, `register_hooks` action, `render()` markup with `WP_Query` fixture, empty state, count cap, attribute filters. + - `wp/cron`: `wp_next_scheduled()`, callback fires, unschedule works. + - `wp/cli`: `WP_CLI::add_command` registered, `__invoke` behaviour, dry-run flag. + +Show the checklist to the developer. Ask: confirm, add, remove? Resolve before scaffolding. This is the cheapest place to catch a misread requirement. + +### 5. Apply conventions, invoke the engine + +Always invoke the engine for any kind it covers. Hand-writing is not a substitute. + +**Always pass this skeleton's conventions (§3)** - the engine defaults target a different layout. With `` = the autoload root and `` = the module dir: + +```bash +npx wp-tooling add wp/ --non-interactive --json \ + --namespace='\Modules\' --base_path=inc/Modules/ \ + --tests_namespace='\Tests' --tests_path=tests/php --text_domain= \ + --slug= --class= --= ... +``` + +Per-scaffold inputs are not listed by `--help`; run once with `--dry-run` to discover required inputs (a CPT needs `slug`/`singular`/`plural`; a taxonomy also needs `--object_type=` to attach it to a CPT; a REST controller needs `--name` + `--rest_namespace`). Dry-run preview: append `--dry-run`. Never use the interactive wizard. + +**Multi-kind feature run order:** + +1. `wp/module` for any missing kind-module (e.g. `--name=PostTypes --kind=cpt`). Most kept capabilities already ship a module (`inc/Modules/.php`). +2. Artifacts in dependency order: CPT before any taxonomy attaching to it; CPT before any block/REST controller querying it. +3. Re-read `ai.wiring` after each call. + +Result shape: `{ scaffold, engine, developer, ai, warnings }`. + +### 6. Process the result + +| Block | Action | +|---|---| +| `engine.wrote` / `engine.skipped` | Already on disk. Report. | +| `developer.install.composer` / `developer.install.npm` | Print as copy-paste command. **Never run `composer require` / `npm install`.** Pilot notes: npm installs need `npm install --install-links`; the framework (`rtcamp/wp-primitives`) already ships, so ignore a `composer require rtcamp/wp-primitives` suggestion. | +| `developer.secrets` | Print as `gh secret set` checklist. **Never read/write/log/transmit values.** | +| `ai.wiring` | Adaptive wiring with consent (see 6a). | +| `ai.tests` | Mandatory expansion under TDD loop (see 7). | +| `warnings` | Print to developer. | + +#### 6a. Adaptive wiring + +For each `{ targetFile, anchor, snippet, description }`: + +0. **Target file** - the engine miscomputes `targetFile` for this layout (it emits `inc/Modules/Modules/.php`, doubled). The real module file is `inc/Modules/.php` (§3). +1. **Snippet** - use canonical if it matches the sampled project pattern; else translate using the sampled shape with the new class substituted. Show both. If patterns conflict or no samples exist, ask. +2. **Location** - the skeleton modules carry no `// scaffold::classes` anchor, so insert the `::class` after the last existing entry in the module's `get_classes()` array (and add the matching `use` import). Anchor present -> after it; else after the last `::class` in `get_classes()`; else skip and print as a manual instruction. +3. **Consent** - show targetFile + line range + description + rendered snippet. Ask `[apply / different location / edit snippet / skip]`. Never apply without consent. +4. **Idempotent** - search first, do not re-insert. + +### 7. TDD loop (mandatory for every scaffolded artifact) + +Tests come before implementation. No exceptions. If the developer says "skip tests", explain the policy and decline. + +For block scaffolds, surface a developer action before testing: "run `npm run build` so the editor can read the compiled block." Do not run it yourself. + +| Step | Action | +|---|---| +| A | Expand the engine's stub into the full suite from §4's checklist. Strip every `markTestIncomplete`. | +| B | Run: `npm run test:php` (PHP, under wp-env) / `npm run test:js` (JS). Expect red. PHP tests need the WP test env: if the runner cannot connect, surface `npm run wp-env start` as a developer action and retry. | +| C | Implement just enough production code to flip **one** failing test green. | +| D | Re-run. Confirm that one test passes. | +| E | Loop B-D one test at a time. | +| F | Once green, refactor; re-run. | +| G | Final gates - all must pass: full PHPUnit suite (`npm run test:php`), full Jest suite if JS touched (`npm run test:js`), `composer format` (phpcbf), `composer lint` (phpcs), `composer phpstan` (PHPStan level 5), `npm run lint:js`. Never silence a real phpcs/phpstan finding with a blanket ignore; fix it, or escalate (§8). | + +Frameworks per kind: + +| Kind | Framework | +|---|---| +| `wp/cpt`, `wp/taxonomy`, `wp/cron`, `wp/cli`, `wp/rest`, `wp/shortcode`, `wp/admin-page`, `wp/settings-page`, `wp/user-role`, `wp/registrable` | PHPUnit | +| `wp/block-dynamic` | Jest (edit.js) + PHPUnit (render method) | +| `block/interactive` | Jest + Playwright | +| `ci/*` | actionlint + yaml-parse | + +### 8. Escalate when stuck - do not guess + +Stop and report findings to the developer when any of the following holds. Wait for response before continuing. + +- 3 consecutive iterations of step C-D on the same test without progress. +- A test result contradicts your model of the code (likely hallucination - re-read the file on disk before guessing again). +- Sampled project patterns conflict and you cannot resolve which to follow. +- A `ai.wiring` snippet's anchor and project pattern both differ from canonical. +- Developer requirements remain ambiguous after one round of clarification. + +Escalation report format: **what you tried, what you observed, what's blocking, 1-3 specific resolution options.** Do not keep iterating in silence. + +### 9. Refresh the graph, then report + +Once green, refresh the LOCAL graph so it reflects the new artifacts (and any later query stays accurate): `graphify update .` (tree-sitter, seconds). **Never commit or push `graphify-out/graph.json`** - it is a maintainer-maintained cross-repo baseline; your refresh stays local (AGENTS.md graphify policy). Do not run the cross-repo `/graphify . --update` subagent or `merge-graphs`. + +Then report: + +- Files written, grouped by top-level directory. +- Wiring applied: file, line, pattern used. +- Tests authored and pass count per file. +- Lint + PHPStan result. +- Outstanding developer actions: composer / npm installs (pilot: `--install-links`), `npm run build` (blocks), secrets to set, branch-protection note (CI). + +## Pilot environment + engine quirks (this skeleton) + +**Test env (`wp-env`):** +- Always use `npx wp-env` (or `node_modules/.bin/wp-env`), never bare `wp-env`. +- If `wp-env start` reports a port already allocated, start on free alternates: `WP_ENV_PORT=8890 WP_ENV_TESTS_PORT=8891 npm run wp-env start` (find a free pair with `lsof -nP -iTCP: -sTCP:LISTEN`). +- `wp-env start` can flake on a transient image pull (TLS timeout); one retry is allowed, and exit 0 does not mean "up" - confirm `WordPress test site started` appears in the output. +- `pretest:php` skips `composer install` when `vendor/` is already mounted; if `npm run test:php` still fails on it, run PHPUnit directly: `npx wp-env run tests-cli --env-cwd=wp-content/plugins/$(basename "$PWD") -- vendor/bin/phpunit -c phpunit.xml.dist`. + +**Generated-code quirks (write the code right up front; these survive `composer format`):** +- **Fully-qualify WP global classes** (`\WP_Error`, `\WP_REST_Request`, `\WP_REST_Response`) everywhere they appear - in code AND docblocks - with NO `use` statement for them. Reason: `composer format` force-qualifies `WP_Error` (Slevomat `FullyQualifiedExceptions` treats `*Error` as an exception) and then strips the now-unused imports, including docblock-only ones like `WP_REST_Request` (`UnusedUses` runs without `searchAnnotations`); PHPStan (scanning `inc/`) then reports `class.notFound` / `return.type` for `\WP_Error`. Writing them fully-qualified avoids the format -> phpstan round-trip. +- **`wp/rest` overridden controller methods must match WP core's untyped signatures.** The template emits typed overrides like `get_items_permissions_check( WP_REST_Request $request ): bool`; adding a parameter type to a `WP_REST_Controller` override is a fatal LSP/contravariance violation that crashes `wp-env start` during provisioning. Rewrite every overridden method (`get_item`/`create_item`/`update_item`/`delete_item` + their `*_permissions_check`) with **untyped** params/return and express the types in PHPDoc only. +- **`wp/rest` route is double-versioned.** The generated `$namespace` already includes the version (e.g. `testimonials/v1`) yet `register_routes()` appends `/v{version}` again -> `v1/v1`. Pick one source: set `$namespace` to the base only and keep `/v{version}`, or register with `$this->namespace` directly. Fix the generated test's asserted route too. +- The engine emits compact `declare(strict_types=1);` and test files with no `@package` tag or per-method doc comments. Run `composer format` (phpcbf) for the spacing, then hand-add the `@package` file tag + a one-line doc comment on each `test_*` method (PHPCS requires them; phpcbf does not add them). +- **File-header order:** the file docblock must immediately follow ` `TestimonialFeedController`); `--slug` and `--text_domain` are not `wp/rest` inputs (warned + ignored). The generated controller is GET-only - replace it with the create/POST flow the brief needs. +- **REST tests:** the generated test calls `register_routes()` directly, which trips WP's `_doing_it_wrong` under `WP_UnitTestCase`. Register via the action instead: in `set_up()`, `global $wp_rest_server; $wp_rest_server = new WP_REST_Server(); do_action( 'rest_api_init' );`, and reset it to `null` in `tear_down()`. + +**Judge your work scoped, not repo-wide.** Run the gates against the files you authored (`vendor/bin/phpcs `, `vendor/bin/phpstan analyse `) and scope `composer format` to them; a repo-wide non-zero exit is not necessarily your code. The skeleton ships gate-clean, so any finding outside your files is a regression to report, not ambient noise. + +## Hard rules - never violate + +- **BASE (see AGENTS.md guardrails):** never run history/remote `git`/`gh` (commit, push, `branch -D`, `reset --hard`, PR, issue comment, `gh secret set`, `gh repo edit`); print them as developer actions. `git clone`/`checkout` are fine. Never do a destructive operation outside this plugin directory. Never commit or push `graphify-out/graph.json`. +- Never write production code before its test exists on disk. +- Never hand-write an artifact the engine can scaffold. +- Never group multiple kinds under a per-feature folder (`Modules//...`). +- Never declare an artifact done without its test file passing. +- Never declare PHP done without the §7 G gates (`composer format` -> `composer lint` -> `composer phpstan`) clean on the generated code. +- Never lower assertion strength to make a test pass (`assertTrue(true)`, widened types). +- Never delete or skip a test the developer would expect to pass. +- Never leave `markTestIncomplete`, `markTestSkipped`, or `@todo` in committed state. +- Never run `composer require`, `npm install`, `npm run build`, or any write-side CLI without explicit consent. +- Never read, write, log, or transmit secret values. +- Never edit branch protection, repo settings, webhooks, or any GitHub admin surface. +- Never apply wiring without showing the diff and getting consent. +- Never invent a third registration pattern when canonical and sampled disagree - ask. +- Never restore scaffold anchor comments without explicit consent. +- Never modify `composer.json`, `package.json`, or any lockfile beyond what the engine wrote. + +**Detect-and-correct:** if you notice an earlier artifact in the wrong directory (e.g. `includes/...` from unpassed conventions) or missing its test file, stop new work, migrate to the canonical layout (§3), add the missing tests, then resume. + +## Engine errors + +| Code | Response | +|---|---| +| `ENOSCAFFOLD` | Surface `available` list, suggest closest, ask. | +| `EMISSINGINPUT` | Read `missingDetails`, run §2 discovery, retry with resolved values. | +| `EBADSCAFFOLD` | Invalid manifest. Surface verbatim, do not retry. For an `origin: "remote"` scaffold this means the fetched manifest at its pinned ref is broken - surface it; do not try to repair another repo's scaffold. | +| `EWRITEFAIL` | Surface path + errno. Do not retry. | +| `ERENDERFAIL` | Scaffold author bug. Surface. | +| `EFETCHFAIL` | Network/HTTP failure fetching an `origin: "remote"` scaffold's manifest or a template. Surface `url` + `statusCode`. If the payload sets `rateLimited`, tell the developer to set `WP_TOOLING_GITHUB_TOKEN` and stop. A timeout is transient - one retry is reasonable; a 404 means the source pin (`sources.json` repo/ref/path) or the owning repo's index is wrong - surface, do not retry. Never hand-write the artifact to work around a failed fetch (the engine owns it). | +| Unknown | Surface, exit non-zero, do not crash. | + +## CI/CD variant + +- `ai.wiring` usually empty. +- `developer.secrets` usually populated. For multi-workflow setups, emit one consolidated `gh secret set` checklist at the end (dedupe). +- `ai.tests` framework is `actionlint` or `yaml-parse`. Validate; do not fill the YAML. + +## Reference + +- Engine contract: `node_modules/@rtcamp/wp-tooling/docs/ai-orchestration.md` +- Examples: `node_modules/@rtcamp/wp-tooling/docs/examples.md` +- Engine source: `node_modules/@rtcamp/wp-tooling/src/scaffolds/` +- Test templates: `node_modules/@rtcamp/wp-tooling/scaffolds/wp//templates/test.php.mustache` diff --git a/.claude/skills/scaffold/evals/evals.json b/.claude/skills/scaffold/evals/evals.json new file mode 100644 index 0000000..613da9d --- /dev/null +++ b/.claude/skills/scaffold/evals/evals.json @@ -0,0 +1,49 @@ +{ + "skill_name": "scaffold", + "evals": [ + { + "id": 1, + "prompt": "I need a WP-CLI command in my plugin that exports Query Monitor data as JSON. The plugin uses Acme\\Blog\\ -> src/ PSR-4 autoload. src/Plugin/Main.php has a register() method wiring commands via $this->commandRegistry->add(new \\Acme\\Blog\\CLI\\X()). Existing commands in src/CLI/ end with Command suffix (ImportCommand, PurgeCommand). Scaffold the qm-export command following these conventions.", + "expected_output": "A new PHP class under src/CLI/QmExportCommand.php with the project's Command suffix and Acme\\Blog\\CLI namespace, plus a PHPUnit test stub. Wiring inserted into src/Plugin/Main.php using the project's translated commandRegistry->add() pattern (not the canonical WP_CLI::add_command). No composer require executed.", + "files": [], + "expectations": [ + "The skill picks the wp/cli scaffold (not a guess across multiple candidates).", + "The skill introspects the project before invoking the engine and reports the four facts: namespace Acme\\Blog, base path src, Command suffix, commandRegistry->add() registration pattern.", + "The engine invocation passes --class=QmExportCommand (project's Command suffix), not the canonical bare QmExport.", + "The engine invocation passes --namespace=Acme\\Blog\\CLI (the sub-namespace sampled from the project).", + "The engine writes the PHP class file and a PHPUnit test stub.", + "The wiring inserted into src/Plugin/Main.php uses the project's commandRegistry->add() pattern, NOT the canonical WP_CLI::add_command() snippet.", + "Wiring is shown with an explicit consent prompt before applying.", + "The skill does NOT run composer require, composer install, or npm install on the user's behalf." + ] + }, + { + "id": 2, + "prompt": "Set up the GitHub Actions workflow that auto-deploys this WordPress plugin to wordpress.org SVN on each release. The plugin slug is acme-blog. The repo has package.json and composer.json but no .github/workflows directory yet.", + "expected_output": ".github/workflows/cd-wporg.yml written by the engine. The skill surfaces WPORG_USERNAME and WPORG_PASSWORD as a `gh secret set` checklist for the user. No secret values are invented or echoed. No `gh secret set` is executed automatically. actionlint (or yaml-parse) validation result reported.", + "files": [], + "expectations": [ + "The skill picks the ci/cd-wporg scaffold.", + "The engine invocation passes --plugin-slug=acme-blog.", + "The engine writes .github/workflows/cd-wporg.yml (and the actionlint test entry references the same file).", + "Both WPORG_USERNAME and WPORG_PASSWORD are surfaced as a `gh secret set` checklist for the user to run themselves.", + "The skill does NOT invent secret VALUES (no fake passwords, no 'changeme' placeholders presented as if real).", + "The skill does NOT execute `gh secret set` itself.", + "The skill reports the actionlint (or yaml-parse fallback) validation result for the generated YAML." + ] + }, + { + "id": 3, + "prompt": "Add a dynamic Gutenberg block called pricing-table to my project. Blocks live at src/blocks/ and we use a vendor prefix of 'acme' for block.json names (existing blocks look like acme/hero, acme/cta).", + "expected_output": "src/blocks/pricing-table/ created with block.json (name: acme/pricing-table), edit.js, index.js, render.php. The vendor prefix 'acme' is detected from an existing block.json and applied. No npm install or wp-scripts build executed automatically.", + "files": [], + "expectations": [ + "The skill picks the wp/block-dynamic scaffold.", + "The skill introspects an existing src/blocks/*/block.json to confirm the 'acme' vendor prefix and the src/blocks/ source directory.", + "The engine invocation passes a vendor/namespace input that produces 'acme/pricing-table' in the generated block.json (not 'wp-tooling/pricing-table' or unprefixed 'pricing-table').", + "The engine writes block.json, edit.js, index.js, and render.php under src/blocks/pricing-table/.", + "The skill does NOT run npm install or wp-scripts build." + ] + } + ] +} diff --git a/.claude/skills/setup/SKILL.md b/.claude/skills/setup/SKILL.md new file mode 100644 index 0000000..1b886e2 --- /dev/null +++ b/.claude/skills/setup/SKILL.md @@ -0,0 +1,388 @@ +--- +name: setup +description: Bootstrap a WordPress plugin or theme from a natural-language description. Detects project type, applies tooling (EditorConfig, PSR-4, PHPCS, PHPStan, ESLint, Stylelint, PHPUnit, Jest, pa11y), and chains feature scaffolds (CLI commands, blocks, REST, etc.) in one session. Asks for clarification whenever intent is ambiguous - never assumes. +--- + +# setup + +You are configuring a WordPress plugin or theme from a natural-language request. Your job is to understand exactly what the developer wants, turn that into a sequenced plan of scaffold invocations, confirm the plan, and execute it. You never assume anything you cannot verify by reading the project directory. + +This skill is meant to be **copied verbatim** into the user's repo at `.claude/commands/setup.md` or `.claude/skills/setup.md`. It assumes `@rtcamp/wp-tooling` is on the `PATH` (via `npx`) or installed as a project dev dependency. + +## When to use this skill + +Use when the developer asks to: + +- "Set up my plugin/theme" or "scaffold this as a [description]." +- Bootstrap a new empty project directory. +- Add rtCamp standard tooling to an existing project. +- Create a feature scaffold (CLI command, block, REST controller, etc.) as part of a setup session. + +Do not use this skill for: isolated bug fixes, one-off file edits, or anything outside the `@rtcamp/wp-tooling` scaffold catalogue. + +## Workflow + +### 0. Parse the request + +Read the developer's message carefully. Extract: + +- **Project type**: plugin vs theme. +- **Standards**: VIP vs non-VIP vs WordPress.org plugin vs plain WordPress. +- **Languages needed**: PHP, JS (blocks, scripts), CSS/SCSS. +- **Features wanted**: CLI command, REST endpoint, cron job, block, post type, integrations, etc. +- **Tests wanted**: PHPUnit (PHP unit/integration), Jest (JS), pa11y (a11y), or none. +- **Any specific names, namespaces, or requirements** mentioned by the developer. + +If the request is vague or any of the above is unclear, **stop and ask before reading the project directory**. Do not try to infer project type from the directory if the developer's message already says it. Do not proceed with gaps. + +Example clarifying questions (ask all at once, never drip): + +``` +Before I start, I need a few details: + +1. Is this a WordPress plugin or a theme? +2. Will this be deployed on WordPress VIP? (Determines the PHPCS standard.) +3. What is the PHP root namespace you want to use? (e.g. Acme\\ImageOptimizer) +4. What directory holds the PHP source? (e.g. includes/ or src/) +5. Do you want tests set up? If so: PHPUnit for PHP, Jest for JS, pa11y for a11y - any or all? +6. You mentioned a CLI command - what should it do? (I need a command name or slug.) +``` + +### 1. Detect what already exists + +After the request is clear, read the project directory to avoid duplicating work. + +**Project type (verify against developer's description):** + +```bash +grep -rl "Plugin Name:\|Theme Name:" . --include="*.php" --exclude-dir=vendor --exclude-dir=node_modules -l | head -1 +``` + +**VIP indicators:** + +```bash +grep -rl "WordPress-VIP-Minimum\|automattic/vip-coding-standards\|VIP_GO_ENV" . \ + --include="*.{php,xml,json,yml}" --exclude-dir=vendor --exclude-dir=node_modules | head -3 +``` + +**Languages present:** + +```bash +find . -name "*.php" -not -path "*/vendor/*" | head -1 +find . \( -name "*.js" -o -name "*.jsx" \) -not -path "*/node_modules/*" -not -path "*/build/*" | head -1 +find . \( -name "*.scss" -o -name "*.css" \) -not -path "*/node_modules/*" | head -1 +``` + +**PSR-4 autoload:** + +```bash +grep -A 8 '"autoload"' composer.json 2>/dev/null +``` + +**Existing tooling configs (skip if present):** + +```bash +ls .editorconfig phpcs.xml.dist phpstan.neon.dist eslint.config.js .stylelintrc.js \ + phpunit.xml.dist jest.config.js .pa11yci.json 2>/dev/null +``` + +**Existing namespace (if PSR-4 missing):** + +```bash +grep -r "^namespace " . --include="*.php" --exclude-dir=vendor | head -3 +``` + +If detection contradicts what the developer said, flag it and ask. Never silently override the developer's stated intent with what you find on disk. + +### 2. Build the scaffold plan + +Construct the plan in two phases: **project setup** and **feature scaffolds**. + +#### Phase A: Project setup + +| Condition | Scaffold | Skip if | +|---|---|---| +| Always | `setup/editorconfig` | `.editorconfig` exists | +| PHP present, no PSR-4 in `composer.json` | `setup/psr4` | `autoload.psr-4` already set | +| VIP project | `lint/phpcs/vip` | `phpcs.xml.dist` exists | +| Non-VIP project | `lint/phpcs/full` | `phpcs.xml.dist` exists | +| Developer explicitly chose core-only PHPCS | `lint/phpcs/core` | `phpcs.xml.dist` exists | +| PHP present | `lint/phpstan` | `phpstan.neon.dist` exists | +| JS present | `lint/eslint` | `eslint.config.js` exists | +| CSS or SCSS present | `lint/stylelint` | `.stylelintrc.js` exists | +| Developer wants PHP tests | `setup/phpunit` | `phpunit.xml.dist` exists | +| Developer wants JS tests | `setup/jest` | `jest.config.js` exists | +| Developer wants a11y tests | `setup/pa11y` | `.pa11yci.json` exists | + +#### Phase B: Feature scaffolds + +Map each feature the developer mentioned to one or more scaffold IDs from the catalogue: + +| Developer said | Scaffold ID | +|---|---| +| CLI command | `wp/cli` | +| REST endpoint / API | `wp/rest` (if available) | +| Cron job / background job | `wp/cron` (if available) | +| Gutenberg block | `wp/block-dynamic` | +| Cache / transients | `utility/cache` | +| CI pipeline | `ci/cd-wporg` (or other CI scaffold) | + +Run `npx wp-tooling list --json` to see exactly what is available. If a feature the developer wants has no matching scaffold, note it explicitly as a manual task in the final report. + +For each feature scaffold, you need the same project-convention information as the `scaffold` skill requires (namespace, base path, class suffix, registration pattern). Collect this once from the project and cache it. + +### 3. Confirm the full plan before doing anything + +Show the complete two-phase plan. Be specific: include the scaffold ID, what file(s) it writes, and any inputs it will use. + +``` +Here is what I will do. Please confirm or adjust before I start. + +Phase A - Project setup: + 1. setup/editorconfig → .editorconfig + 2. setup/psr4 → wiring in composer.json (namespace: Acme\ImageOptimizer, path: includes/) + 3. lint/phpcs/vip → phpcs.xml.dist (WordPress-VIP-Minimum + WordPress-Docs) + 4. lint/phpstan → phpstan.neon.dist (extends rtCamp wp-phpstan baseline) + 5. lint/eslint → eslint.config.js + 6. setup/phpunit → phpunit.xml.dist, tests/bootstrap.php + +Phase B - Feature scaffolds: + 7. wp/cli → includes/Cli/OptimizeImagesCommand.php + (namespace: Acme\ImageOptimizer\Cli, class suffix: Command, + registers via $this->boot(...) in includes/Plugin.php) + +Skipped (already present): none. + +Not in catalogue (manual tasks): none. + +Confirm? Or adjust (e.g. "remove phpunit", "use full phpcs instead of vip", "add jest")? +``` + +Do not start running scaffolds until the developer confirms. If they adjust, update the plan and confirm once more before starting. + +### 4. Execute Phase A + +Run each setup scaffold in order. Use `--non-interactive --json --cwd .`. + +For `setup/editorconfig`: + +```bash +npx wp-tooling add setup/editorconfig --non-interactive --json --cwd . +``` + +For `setup/psr4` (use detected or developer-supplied namespace and base path): + +```bash +npx wp-tooling add setup/psr4 \ + --non-interactive --json --cwd . \ + --namespace='Acme\ImageOptimizer' --base-path='includes' +``` + +For lint and test setup scaffolds: + +```bash +npx wp-tooling add lint/phpcs/vip --non-interactive --json --cwd . +npx wp-tooling add lint/phpstan --non-interactive --json --cwd . +npx wp-tooling add lint/eslint --non-interactive --json --cwd . +npx wp-tooling add setup/phpunit --non-interactive --json --cwd . --source-dir=includes +npx wp-tooling add setup/pa11y --non-interactive --json --cwd . --base-url=http://localhost:8888 +``` + +Process each result before running the next: + +- Report files written and skipped. +- Apply `setup/psr4` wiring to `composer.json` with explicit consent (see §Wiring below). +- Accumulate `developer.install.*` and `developer.scripts.*` across all scaffolds. + +### 5. Execute Phase B + +For each feature scaffold, follow the full workflow from `skills/scaffold.md` - introspect conventions, apply naming, invoke the engine, adaptive wiring, **expand test stubs into a real suite, then drive implementation from those tests** (red → green → refactor). + +Do not batch feature scaffolds. Run one at a time, apply its wiring, complete the TDD loop (see `skills/scaffold.md` §5b), then move to the next. + +For `wp/cli`, pass the same project conventions detected in Stage 1 (namespace, base path, class suffix) - defaults assume the rtCamp skeleton (`Inc\Cli`, `includes/Cli`) and will be wrong for any other project: + +```bash +npx wp-tooling add wp/cli \ + --non-interactive --json --cwd . \ + --namespace='Acme\ImageOptimizer\Cli' --base-path='includes/Cli' \ + --name=optimize-images --class=OptimizeImagesCommand +``` + +The engine emits `ai.wiring` (where to register the command) and a thin test stub at `tests/Cli/OptimizeImagesCommandTest.php`. Show the wiring snippet, get consent, apply. Then turn the stub into a real test suite (happy path, dry-run flag, edge cases, error handling) and implement `__invoke()` test-by-test until the suite is green. Never leave `markTestIncomplete` in the final state. + +**Code quality (required).** For any PHP a feature scaffold writes or changes under `inc/` or `tests/`, run the compliance pipeline once the implementation is green, exactly as in the `scaffold` skill (`skills/scaffold.md` §7a): `composer format` (phpcbf) -> `composer lint` (phpcs, `rtCampWP` ruleset via `phpcs.xml.dist`) -> `composer phpstan` (PHPStan level 5 via `phpstan.neon.dist`), resolving every finding in the generated code. Write it compliant in the first place (`declare( strict_types = 1 );`, short arrays, typed signatures, prefixed globals, documented). If a fix is unclear or would change behaviour, public API, or intent, STOP and ask - never silence a real issue with a blanket `phpcs:ignore` / `@phpstan-ignore`. + +**Phase B feature scaffolds require the matching test framework from Phase A.** If Phase A skipped `setup/phpunit` because the developer did not ask for tests, surface this before running Phase B feature scaffolds: + +``` +You asked for a CLI command, but Phase A skipped setup/phpunit. +The wp/cli scaffold ships a PHPUnit stub I cannot run without it. + +Options: + 1. Add setup/phpunit now (recommended - I drive feature development from tests). + 2. Proceed without tests (stub will be written but not executed; I will note this as a manual follow-up). + +Which? +``` + +Default to (1). Only proceed without tests if the developer explicitly chooses (2). + +### Wiring: composer.json PSR-4 + +When `setup/psr4` wiring is received: + +1. Show the current `"autoload"` block in `composer.json` (or note it is absent). +2. Show the intended entry: namespace `Acme\ImageOptimizer` maps to `includes/`. +3. Ask: `Apply PSR-4 autoload to composer.json? [apply / skip]` +4. If apply: paste the engine's `ai.wiring[0].snippet` verbatim. The engine already emits the PSR-4 key JSON-encoded with its trailing backslash (e.g. `"Acme\\ImageOptimizer\\"`) - **do not escape it again**, or you will double the backslashes and break autoload. +5. Remind: run `composer dump-autoload --optimize` after applying. + +If `composer.json` does not exist, offer to create a minimal one: + +``` +composer.json not found. I can create a minimal one: + + { + "name": "vendor/image-optimizer", + "type": "wordpress-plugin", + "require": { "php": ">=8.3" }, + "autoload": { + "psr-4": { "Acme\\ImageOptimizer\\": "includes/" } + } + } + +Create it? [yes / skip PSR-4 / give me the values to use] +``` + +### Wiring: feature scaffolds + +Same as `skills/scaffold.md` §5 - `ai.wiring`. Show diff, get consent, apply. + +### 6. Consolidated final report + +``` +Setup complete. + +Files written (Phase A): + .editorconfig + phpcs.xml.dist (WordPress VIP Minimum + Docs) + phpstan.neon.dist (extends rtCamp wp-phpstan baseline) + eslint.config.js + phpunit.xml.dist + tests/bootstrap.php + composer.json (PSR-4 autoload added, Acme\ImageOptimizer → includes/) + +Files written (Phase B): + includes/Cli/OptimizeImagesCommand.php + tests/Cli/OptimizeImagesCommandTest.php + +Wiring applied: + includes/Plugin.php:43 - $this->boot('optimize-images', \Acme\ImageOptimizer\Cli\OptimizeImagesCommand::class); + +Tests: + tests/Cli/OptimizeImagesCommandTest.php - stub passing. + +Developer actions (run these yourself): + + composer dump-autoload --optimize + + composer require --dev \ + automattic/vip-coding-standards:^3.0 \ + wp-coding-standards/wpcs:^3.0 \ + squizlabs/php_codesniffer:^3.7 \ + phpunit/phpunit:^12.0 \ + yoast/phpunit-polyfills:^4.0 \ + brain/monkey:^2.6 + + npm install --save-dev \ + eslint@^10.0.0 \ + @wordpress/eslint-plugin@^25.1.0 \ + @rtcamp/eslint-config@^0.1.0 + +Scripts to add to composer.json: + + "scripts": { + "lint:php": "phpcs --standard=phpcs.xml.dist", + "lint:php:fix": "phpcbf --standard=phpcs.xml.dist", + "phpstan": "phpstan analyse", + "test": "phpunit", + "test:unit": "phpunit --testsuite Unit", + "test:integration": "phpunit --testsuite Integration" + } + +Scripts to add to package.json: + + "scripts": { + "lint:js": "eslint 'src/**/*.js' 'assets/**/*.js'", + "lint:js:fix": "eslint 'src/**/*.js' 'assets/**/*.js' --fix" + } + +Skipped: none. +Outstanding manual tasks: none. +``` + +Deduplicate packages. Sort alphabetically within each block. Pinned packages use exact versions; everything else uses range specifiers. + +## PHPCS standard reference + +| Scaffold ID | When to use | Ruleset | +|---|---|---| +| `lint/phpcs/full` | Most rtCamp projects (recommended default) | `vendor/rtcamp/wp-primitives/phpcs.xml.dist`, WordPress-Core + Extra + Docs + VIP-Go | +| `lint/phpcs/vip` | WordPress VIP platform projects | `WordPress-VIP-Minimum` + `WordPress-Docs` | +| `lint/phpcs/core` | Projects explicitly opting out of VIP-Go rules | `WordPress` - Core + Extra + Docs only | + +Developers can add `` entries to `phpcs.xml.dist` to override or extend the selected standard. + +## Test scaffolds reference + +| Scaffold ID | What it installs | When to apply | +|---|---|---| +| `setup/phpunit` | `phpunit.xml.dist`, `tests/bootstrap.php`, PHPUnit + polyfills + Brain Monkey | PHP plugin or theme with tests | +| `setup/jest` | `jest.config.js`, `@wordpress/jest-preset-default` | JS blocks or scripts with unit tests | +| `setup/pa11y` | `.pa11yci.json`, `pa11y-ci` | Any project needing WCAG2AA accessibility coverage | + +## Rules: never assume, always ask + +Before running any scaffold, every required input must be confirmed by the developer or verified from the project files. The following facts require explicit confirmation or verification - never infer them silently: + +- Plugin vs theme. +- VIP vs non-VIP. +- PHP root namespace and base directory. +- CLI command slug and class name. +- Block slug and vendor prefix. +- REST route and controller name. +- pa11y base URL. + +If the developer's request is clear enough that a fact can be read unambiguously from the project (e.g. namespace from `composer.json` autoload, VIP from existing `phpcs.xml.dist`), no need to ask - cite the source in the confirmation plan instead. + +## Hard prohibitions + +You **must never** (BASE, see AGENTS.md guardrails): + +- Run history/remote `git`/`gh` at all (commit, push, `branch -D`, `reset --hard`, PR, issue comment, `gh secret set`); print them as developer actions. `git clone`/`checkout` for setup are fine. +- Do a destructive operation outside this plugin directory (cloning a NEW sibling is additive and OK; deleting/overwriting existing out-of-repo files is not). +- Commit or push `graphify-out/graph.json`; local graph refreshes stay local. +- Run `composer require`, `npm install`, `composer dump-autoload`, or any package manager command without explicit user approval. +- Edit `composer.json` scripts or `package.json` scripts - show them, let the developer apply. +- Apply wiring to any file without showing the diff and receiving consent. +- Apply more scaffolds than the confirmed plan. +- Silently skip a scaffold; always report skips with a reason. +- Set up CI/CD - that requires a separate `scaffold` skill invocation targeting `ci/` scaffolds. +- Declare generated PHP done without running the §5 code-quality pipeline (`composer format` -> `composer lint` -> `composer phpstan`) clean, or silence a real finding with a blanket `phpcs:ignore` / `@phpstan-ignore` to pass it. +- Commit, push, or open PRs without explicit approval. + +## Error handling + +For `ENOSCAFFOLD`: The requested scaffold id does not exist. Surface the `available` list, show the closest match, and ask the developer what to do. + +For `EMISSINGINPUT`: Read `missingDetails`, collect the values from the project or ask the developer, and retry with the resolved values. + +For `EWRITEFAIL`: Surface the path and OS error. Ask whether to retry (e.g. after the developer fixes permissions) or skip the file. + +For `EBADSCAFFOLD`: Scaffold author bug. Surface verbatim. Do not retry. + +## Reference + +- Worked conversation examples: `node_modules/@rtcamp/wp-tooling/docs/examples.md` (greenfield plugin, VIP image-optimizer setup, all phases in one prompt). +- One-off scaffold additions are handled by the companion `scaffold` skill. diff --git a/.claude/skills/setup/evals/evals.json b/.claude/skills/setup/evals/evals.json new file mode 100644 index 0000000..729012a --- /dev/null +++ b/.claude/skills/setup/evals/evals.json @@ -0,0 +1,49 @@ +{ + "skill_name": "setup", + "evals": [ + { + "id": 1, + "prompt": "Set up a new WordPress VIP plugin called My Plugin in the Acme\\ImageOptimizer namespace under includes/. I want PHPCS, PHPStan, and PHPUnit. Then add a WP-CLI command called optimize-images that I'll fill in later.", + "expected_output": "Two-phase plan executed across the greenfield project: Phase A installs editorconfig + psr4 + phpcs/vip + phpstan + phpunit (skipping eslint/stylelint/jest/pa11y with reasons), Phase B scaffolds the wp/cli command. A minimal composer.json is offered when the file doesn't exist. One consolidated final report covers files written, dev-deps to install, and outstanding manual tasks. No composer require / composer dump-autoload / npm install / gh secret set executed.", + "files": [], + "expectations": [ + "Stage 0: The skill parses the request and identifies all stated facts (plugin, VIP=yes, namespace, base path, features, tests). Does NOT ask clarifying questions for facts already stated.", + "Stage 1: Detects the project is greenfield (no tooling configs, no composer.json autoload).", + "Stage 2: Plan includes setup/editorconfig + setup/psr4 + lint/phpcs/vip + lint/phpstan + setup/phpunit for Phase A, and wp/cli for Phase B.", + "Stage 2: Skips lint/eslint and lint/stylelint with reasons (no JS or CSS detected). Skips setup/jest and setup/pa11y with reasons (not requested).", + "Stage 3: Shows the full plan with scaffold ids + files each will write + inputs + skipped-with-reasons, AND waits for confirmation before executing.", + "Stage 4: Executes scaffolds one at a time. For setup/psr4 when no composer.json exists, offers the minimal-composer.json template with the JSON-escaped namespace key 'Acme\\\\ImageOptimizer\\\\'.", + "Stage 5: Defaults to adding setup/phpunit before running wp/cli if it wasn't already in Phase A (Phase B test-driven scaffolds need their framework).", + "Stage 6: Emits one consolidated final report — files written across both phases, dedup'd `composer require --dev` lines (using composerDev field), scripts to add to manifests, skipped-with-reasons, outstanding manual tasks.", + "The skill does NOT run composer require, composer dump-autoload, npm install, or gh secret set on the user's behalf." + ] + }, + { + "id": 2, + "prompt": "Bootstrap this thing for me", + "expected_output": "Recognises the request is ambiguous and stops to ask clarifying questions before reading the project directory. Questions are batched (not drip-fed) and cover at minimum: plugin vs theme, VIP vs non-VIP, namespace + base path, which tests are wanted. No scaffolds executed before the developer provides clarification.", + "files": [], + "expectations": [ + "The skill recognises the request is ambiguous and STOPS before reading the project directory.", + "The skill asks clarifying questions in ONE batch (not drip-fed across multiple turns), covering at minimum: plugin vs theme, VIP vs non-VIP, namespace + base path, which tests are wanted.", + "The skill does NOT attempt to infer project type from disk before asking — the developer's message is silent on the basics, and 'ask first' is the rule.", + "The skill does NOT execute any scaffolds before the developer provides clarification." + ] + }, + { + "id": 3, + "prompt": "Add rtCamp tooling to my existing plugin. It's at the repo root. composer.json already declares Acme\\Blog\\ -> src/ PSR-4 autoload and there's already a phpcs.xml.dist with WordPress-VIP-Minimum. Add PHPStan and PHPUnit; I don't need JS lint or a11y.", + "expected_output": "Detection cites the existing composer.json autoload and phpcs.xml.dist. setup/psr4 and lint/phpcs/vip skipped with reasons. Plan includes setup/editorconfig (if missing), lint/phpstan, setup/phpunit. lint/eslint, lint/stylelint, setup/jest, setup/pa11y NOT included because the developer ruled them out. Plan confirmed with developer before executing. Final report lists skipped scaffolds with the reason for each.", + "files": [], + "expectations": [ + "The skill detects the existing composer.json autoload (does not ask for namespace) and the existing phpcs.xml.dist (VIP confirmed without asking).", + "Both findings are cited in the confirmation plan with their source (composer.json, phpcs.xml.dist), not silently used.", + "The plan skips setup/psr4 (already configured) and lint/phpcs/vip (config exists) — both reported as skipped with reasons.", + "The plan includes only setup/editorconfig (if missing), lint/phpstan, setup/phpunit.", + "The plan does NOT include lint/eslint, lint/stylelint, setup/jest, or setup/pa11y — developer explicitly ruled them out.", + "The skill confirms the plan with the developer before executing.", + "The final report explicitly lists skipped scaffolds with the reason for each." + ] + } + ] +} diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..f72a2e3 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,18 @@ +# WordPress Coding Standards +# https://make.wordpress.org/core/handbook/coding-standards/ + +root = true + +[*] +charset = utf-8 +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true +indent_style = tab + +[{.babelrc,.eslintrc,.rtlcssrc,*.json,*.yml}] +indent_style = space +indent_size = 2 + +[*.md] +trim_trailing_whitespace = false \ No newline at end of file diff --git a/.env.local.example b/.env.local.example new file mode 100644 index 0000000..1f6592c --- /dev/null +++ b/.env.local.example @@ -0,0 +1,29 @@ +# Local site hostname (without protocol or trailing slash). +# LocalWP: yoursite.local, Lando: yoursite.lndo.site, wp-env: localhost, WP Studio: localhost +WP_HOST=yoursite.local + +# HMR / BrowserSync live reload master switch (default: on). Honoured by both +# the build (BrowserSync server) and PHP (client enqueue). Set to false to fully +# disable live reload. Off values: 0, false, no, off. Toggle from `npm run init`. +ENABLE_HMR=true + +# BrowserSync port. Default: 3003. Change if port 3003 is already in use. +# Both the build and PHP read this value directly, so changing it here is +# enough. Define PROJECT_NAME_FEATURES_BROWSER_SYNC_URL only to override the +# full client URL (e.g. remote/proxy setups), not just the port. +BS_PORT=3003 + +# Block HMR dev-server port (used by `npm run start:blocks`). +# Default: 8886. 8887 is the starter theme's dev server and 8888/8889 are wp-env, +# so the plugin and theme can hot-reload side by side. Change it if 8886 is in use. +BLOCKS_DEV_SERVER_PORT=8886 + +# Disable BrowserSync client enqueue. Truthy: 1, true, yes, on (default off). +# The BrowserSync server still runs; only the frontend client is skipped. +# DISABLE_BS=true + +# SSL cert paths — only needed when your local site runs on HTTPS. +# LocalWP stores certs at: +# macOS: ~/Library/Application Support/Local/run/router/nginx/certs/.key / .crt +WP_SSL_KEY=/path/to/yoursite.local.key +WP_SSL_CERT=/path/to/yoursite.local.crt diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 243ce7b..402c0bd 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1 +1 @@ -* @Adi-ty \ No newline at end of file +* @Adi-ty @aryanjasala @AnuragVasanwala diff --git a/.github/ISSUE_TEMPLATE/bug.md b/.github/ISSUE_TEMPLATE/bug.md new file mode 100644 index 0000000..4bc8afb --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug.md @@ -0,0 +1,24 @@ +--- +name: Bug Report +about: There is a bug in functionality that needs to be fixed. +title: '' +labels: '' +assignees: '' + +--- + +## Description + + + +## Steps to Reproduce + + + +## Screenshots + + + +## Additional Information + + diff --git a/.github/ISSUE_TEMPLATE/bug.yml b/.github/ISSUE_TEMPLATE/bug.yml new file mode 100644 index 0000000..c55b6e9 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug.yml @@ -0,0 +1,50 @@ +name: Bug Report Form +description: If something isn't working as expected +labels: + - bug +body: + - type: textarea + attributes: + label: Current Behaviour + description: A clear and concise description of the buggy behavior. + validations: + required: true + - type: textarea + attributes: + label: Expected Behaviour + description: >- + A clear and concise description of what you expected to happen (or + code). + validations: + required: true + - type: textarea + attributes: + label: Steps to reproduce the bug + description: Steps to reproduce if any. + placeholder: Steps to reproduce bug. + value: |- + 1. Go to '...' + 2. Click on '....' + 3. Scroll down to '....' + 4. See error + validations: + required: true + - type: textarea + attributes: + label: Testing Environment (if applicable) + description: Steps to reproduce if any. + validations: + required: true + - type: textarea + attributes: + label: Additional context/Screenshots + description: >- + Add any other context about the problem here. If applicable, add + screenshots to help explain. + validations: + required: true + - type: input + attributes: + label: Estimated time in hours + description: Enter the estimated time required to finish this task. + placeholder: 40 Hours diff --git a/.github/ISSUE_TEMPLATE/enhancement.yml b/.github/ISSUE_TEMPLATE/enhancement.yml new file mode 100644 index 0000000..9ea3baa --- /dev/null +++ b/.github/ISSUE_TEMPLATE/enhancement.yml @@ -0,0 +1,25 @@ +name: Enhancement +description: template for missing feature +title: '[Enhancement]: ' +labels: + - enhancement +body: + - type: textarea + attributes: + label: Description + description: A clear and concise description of the problem or missing capability + validations: + required: true + - type: textarea + attributes: + label: Describe the solution you'd like + description: If you have a solution in mind, please describe it. + - type: textarea + attributes: + label: Describe alternatives you've considered + description: Have you considered any alternative solutions or workarounds? + - type: input + attributes: + label: Estimated time in hours + description: Enter the estimated time required to finish this task. + placeholder: 40 Hours diff --git a/.github/ISSUE_TEMPLATE/epic.md b/.github/ISSUE_TEMPLATE/epic.md new file mode 100644 index 0000000..a1fdc67 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/epic.md @@ -0,0 +1,24 @@ +--- +name: Epic +about: An Epic for tracking multiple tasks related to a theme of work. +title: '' +labels: '' +assignees: '' +--- + +## Summary + + + +## References + + + +## Tasks + + diff --git a/.github/ISSUE_TEMPLATE/feature.yml b/.github/ISSUE_TEMPLATE/feature.yml new file mode 100644 index 0000000..789e1df --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature.yml @@ -0,0 +1,25 @@ +name: Feature Request +description: template for missing feature +title: '[Feature Request]: ' +labels: + - feature +body: + - type: textarea + attributes: + label: Description + description: A clear and concise description of the problem or missing capability + validations: + required: true + - type: textarea + attributes: + label: Describe the solution you'd like + description: If you have a solution in mind, please describe it. + - type: textarea + attributes: + label: Describe alternatives you've considered + description: Have you considered any alternative solutions or workarounds? + - type: input + attributes: + label: Estimated time in hours + description: Enter the estimated time required to finish this task. + placeholder: 40 Hours diff --git a/.github/ISSUE_TEMPLATE/task.md b/.github/ISSUE_TEMPLATE/task.md new file mode 100644 index 0000000..7956bff --- /dev/null +++ b/.github/ISSUE_TEMPLATE/task.md @@ -0,0 +1,24 @@ +--- +name: Task +about: A descriptive task to be done. +title: '' +labels: '' +assignees: '' +--- + +## Summary + + + +## References + + + +## Acceptance Criteria + + diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..1a3d51b --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,32 @@ +## Description + + + +## Technical Details + + + +## Checklist + + + +## Screenshots + + + +## To-do + + + +## Fixes/Covers issue + + +Fixes # + + diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 0000000..6f21757 --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,27 @@ +# Copilot instructions: Plugin Elementary + +Read **[AGENTS.md](../AGENTS.md)** first: it is the source of truth for this project's conventions (stack, structure, TDD, framework patterns, security, guardrails), shared across all AI tools. This file adds only the Copilot-specific review conduct. + +Detailed, path-scoped rules live in [`.github/instructions/`](instructions/): +- `primitives-php.instructions.md` (generated by `npm run sync-ai` from the framework; absent until then): `**/*.php` framework architecture, security, testing, review flags. +- `structure.instructions.md`: plugin layout and wiring. + +This is the skeleton/template: names are placeholders (`Project_Name` / `project-name`) filled in per project. The framework (`rtCamp\WPPrimitives`) lives in the gitignored `vendor/` and is not visible at review. + +## Task prompts + +Two reusable prompts in [`prompts/`](prompts/), invoked in Copilot Chat. They are the Copilot peers of the Claude skills, kept consistent with them: +- `/init` ([init.prompt.md](prompts/init.prompt.md)): set up / manage the project; installs dependencies (`npm install`, `composer install`), then drives `npm run init`. +- `/scaffold` ([scaffold.prompt.md](prompts/scaffold.prompt.md)): add one feature, TDD-first, via `@rtcamp/wp-tooling`. + +Both are agentic: gather inputs and confirm before acting, and after any code change refresh the local knowledge graph (`graphify update .`, never committed) per the graphify policy in [AGENTS.md](../AGENTS.md). The review conduct below applies to PR review, not to running these prompts. + +## Review conduct + +These tune *how* you comment; the *what* lives in AGENTS.md and the path-scoped files. Balance matters: catch every real issue, but don't drown it in noise. + +- **One comment per distinct issue, and never more than one per comment.** Every distinct problem gets its OWN separate comment at its own line, even when several issues sit on the same or adjacent lines (e.g. an unauthenticated `permission_callback`, raw `$_POST` use, and SQL injection in one method are THREE comments, not one). Do not merge unrelated findings. Conversely, when a *single* root cause spans several lines (e.g. the `Singleton` trait plus its `get_instance()` bootstrap), post it once at the clearest line and name the related lines; don't repeat the same finding. +- **Always post the missing-tests (TDD) finding** when a new/changed feature or class ships without a matching test in `tests/php/`. This is mandatory and exempt from dedupe, trimming, and any comment-budget; never drop it in favour of other findings. +- **Order the rest by impact:** security → architecture/contract (`Singleton`/`Shareable`/`Loader`/PSR-4/abstracts) → WordPress correctness → style. Lead with what breaks things. +- **Don't spend comments on what PHPCS/PHPStan already enforce.** A bare "add a `void` return type" or "add visibility" is noise; the linters catch it. Only raise a typing issue when it changes behavior or hides a real bug. If a non-linter-covered style note is worth raising, give it its own comment too. +- **On a correct, deliberate implementation, don't manufacture findings.** An idiomatic, defensible choice is not a bug. Prefer **zero comments** over low-value ones. A clean PR comes back clean. (A genuine correctness/security observation on otherwise-good code is still welcome; a stylistic preference is not.) diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..6a6bb14 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,27 @@ +version: 2 +updates: + - package-ecosystem: github-actions + directory: '/' + schedule: + interval: weekly + open-pull-requests-limit: 10 + labels: + - Dependencies + + - package-ecosystem: npm + directory: '/' + schedule: + interval: weekly + open-pull-requests-limit: 10 + versioning-strategy: increase + labels: + - Dependencies + + - package-ecosystem: composer + directory: '/' + schedule: + interval: weekly + open-pull-requests-limit: 10 + versioning-strategy: increase + labels: + - Dependencies diff --git a/.github/instructions/primitives-php.instructions.md b/.github/instructions/primitives-php.instructions.md new file mode 100644 index 0000000..fbd1357 --- /dev/null +++ b/.github/instructions/primitives-php.instructions.md @@ -0,0 +1,56 @@ +--- +applyTo: "**/*.php" +description: "Framework + WordPress rules for PHP. Canonical source, shipped by rtcamp/wp-primitives." +--- + +# PHP rules: framework, WordPress, security, tests + +`rtcamp/wp-primitives` (`rtCamp\WPPrimitives`) is in gitignored `vendor/` (invisible at review). These rules ARE its contract. + +## Architecture + +`Main` (`use Singleton; use Loader;`) lists classes in `const CLASSES`. The `Loader` instantiates each → if `Registrable`, calls `register_hooks()` (skipped when `ConditionallyRegistrable::can_register()` is false) → if `Shareable`, caches it in a `Container` (fetch via `get_shared()`). Otherwise a fresh, non-shared instance. + +Decision order for a new class, **do NOT default to Singleton**: +1. **plain `Registrable`**: default; loaded by the `Loader`, never retrieved elsewhere. +2. **`Registrable` + `Shareable`**: only if another class must retrieve it via `get_shared()`. +3. **`Singleton`**: only the `Main` bootstrap. + +Extend the framework abstracts; never hand-roll their job: `AbstractModule`, `AbstractFeature`, and `Abstract{PostType,Taxonomy,Block,Shortcode,RESTController,SettingsPage,AdminPage,UserRole,Ability,AbilityRegistrar}`. + +Flag genuine contract/security violations, not style. Allow any correct implementation. + +## Mandatory + +- **TDD**: failing PHPUnit test first (`tests/php/` mirrors `inc/`, extend the package `TestCase`), then code. +- `declare( strict_types = 1 );` at the top of every file. Full param + return types. `@package` + `@since` on every class/trait docblock. +- `snake_case` methods, `PascalCase` classes, filename === class, namespace === directory (PSR-4). +- `static::`, never `self::`, for late static binding. + +## WordPress security (always) + +- Escape on output (`esc_html`/`esc_attr`/`esc_url`/`wp_kses_post`); sanitize on input (`sanitize_text_field`/`absint`/`sanitize_key`). +- Verify a nonce **and** `current_user_can()` before any mutation (form/AJAX/REST). +- DB: `$wpdb->prepare()` with placeholders; never concatenate input. +- REST routes: always set a real `permission_callback` (never `__return_true` for writes); validate via the `args` schema. +- Use `===`/`!==`. + +## WordPress conventions + +- i18n: wrap user strings with the package text domain (`__()`, `esc_html__()`). +- Assets: `wp_enqueue_script/style` on the enqueue hooks, conditional per screen; never inline `