diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 00000000..953e6919 --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,35 @@ +# Copilot instructions — Theme Elementary + +A custom WordPress **block theme** (PHP 8.2+) built on `rtcamp/wp-framework` (`rtCamp\WPFramework`, installed in gitignored `vendor/`: not visible at review). + +Detailed, path-scoped rules live in `.github/instructions/`: +- `framework-php.instructions.md` (generated by `npm run sync-ai` from the framework; absent until then): all `**/*.php` framework architecture, security, testing, review flags. +- `structure.instructions.md`: theme layout and wiring. + +## Stack + +- PHP 8.2+, Composer, PSR-4 autoload (namespace === directory, filename === class). +- PHPUnit + `wp-phpunit` in `tests/php/` (mirrors `inc/`). +- PHPCS (WordPress-Core/Extra/Docs + VIPCS) and PHPStan: zero errors before merge. +- Block theme: `theme.json`, `templates/`, `parts/`, `patterns/`, `styles/`. + +## Universal rules + +- **TDD**: write the failing test first, then the implementation. Never ship code without a test. +- **Do not default to Singleton.** Hook WordPress through the framework `Loader` + `Registrable`; use `Shareable` only when an instance must be retrieved later. Full decision tree in `framework-php.instructions.md`. +- Prefer official WordPress / `@wordpress/*` APIs and block-theme mechanisms (`theme.json`, patterns, template parts) over custom PHP. +- Never modify WordPress core or anything under `vendor/`; extend via actions and filters. + +## Commands + +- `vendor/bin/phpunit -c phpunit.xml.dist` · `composer phpcs` · `composer phpcs:fix` · `composer phpstan`. + +## Review conduct + +These tune *how* you comment; the *what* lives in 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/instructions/structure.instructions.md b/.github/instructions/structure.instructions.md new file mode 100644 index 00000000..7dce62f2 --- /dev/null +++ b/.github/instructions/structure.instructions.md @@ -0,0 +1,20 @@ +--- +applyTo: "inc/**" +description: "Theme structure. Merges with framework-php.instructions.md and copilot-instructions.md." +--- + +# Theme structure + +Block theme. Namespace `rtCamp\Theme\Elementary\` → `inc/`; tests `rtCamp\Theme\Elementary\Tests\` → `tests/php/`. Text domain `elementary-theme`. Entry: `functions.php` → `Autoloader` → `Main::get_instance()`. Block config in `theme.json`, `templates/`, `parts/`, `patterns/`, `styles/`. + +Flatter than a plugin: `Main::CLASSES` lists Core/Module classes directly (no `AbstractModule` grouping yet; add one only when a domain grows several related classes). `inc/Core/` (`ThemeSetup`, `Menu`, `Assets`) implement `Registrable` directly; modules in `inc/Modules/`. + +**Scaffolding:** the `inc/Modules/` classes are demos shipped to show the pattern. A real project removes unused ones and adds its own per requirements; do not assume a specific module file exists, nor flag one as "missing". `inc/Core/` infra is real and stays. Framework abstracts always exist in `vendor/`; which you extend is requirement-driven. + +To add a feature: create the class implementing `Registrable` (or extending a framework abstract, e.g. an options page → `AbstractSettingsPage`), then add its `::class` to `Main::CLASSES`. + +## Flag + +- 🚩 New `Registrable` class not added to `Main::CLASSES` → it never loads. +- 🚩 Markup/styling hardcoded in PHP where `theme.json` / a block pattern / template part belongs. +- 🚩 Options page or other registration hand-rolled where a framework `Abstract*` fits. diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 00000000..91cd3845 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,26 @@ +# AGENTS.md — Theme Elementary + +Tool-agnostic brief for AI coding agents (Claude Code, Copilot coding agent, Codex). A custom WordPress **block theme** built on `rtcamp/wp-framework` (in `vendor/`). + +## Authoritative rules + +The review rules ARE the coding rules: the same files Copilot reviews against. Follow them when writing code; they hold the full detail: + +- `.github/instructions/framework-php.instructions.md`: framework architecture, security, testing, and the do/don't flags. Shipped from `rtcamp/wp-framework`, generated locally by `npm run sync-ai` (absent until then). +- `.github/instructions/structure.instructions.md`: theme layout and wiring. +- `.github/copilot-instructions.md`: overview + conventions. + +## Key principles (full detail in the files above) + +- **TDD**: write the failing PHPUnit test first (`tests/php/` mirrors `inc/`), then code. +- **Don't default to Singleton.** Hook WordPress via the framework `Loader` + `Registrable`; use `Shareable` only when an instance must be retrieved later via `get_shared()`. `Singleton` is for `Main` only. +- **Extend the framework abstracts** (e.g. an options page → `AbstractSettingsPage`) instead of hand-rolling registration. +- **Prefer `theme.json` / block patterns / template parts** over hardcoded markup or styling in PHP. +- **WordPress security**: escape/sanitize, nonce + `current_user_can()` before mutations, `$wpdb->prepare()`, a real REST `permission_callback`. +- `declare( strict_types = 1 );`, full types, `@package`/`@since`, `static::` not `self::`, PSR-4 (namespace === dir). + +## Structure + +`inc/` (PSR-4 `rtCamp\Theme\Elementary\`): `Main.php` (boot), `Core/` (`ThemeSetup`/`Menu`/`Assets`, implement `Registrable`), `Modules/`. `Main::CLASSES` lists classes directly (no `AbstractModule` grouping yet). Block config in `theme.json`, `templates/`, `parts/`, `patterns/`, `styles/`. Text domain `elementary-theme`. `tests/php/` mirrors `inc/`; `inc/Modules/` classes are scaffolding; delete unused. + +To add a feature: write the test, implement `Registrable` (or extend an abstract), add its `::class` to `Main::CLASSES`. diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 00000000..91fa3416 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,6 @@ +# CLAUDE.md + +Read **[AGENTS.md](AGENTS.md)**: it is the source of truth for this project's conventions, shared across all AI tools (Claude Code, Copilot, Codex). The detailed rules it points to live in `.github/instructions/`. + +Claude-specific notes: +- _(none currently; keep Claude overrides here if they ever diverge from AGENTS.md)_ diff --git a/bin/init.js b/bin/init.js index e3bbcf8a..97c85793 100755 --- a/bin/init.js +++ b/bin/init.js @@ -451,7 +451,6 @@ const initTheme = ( themeInfo ) => { const getAllFiles = ( dir ) => { const dirOrFilesIgnore = [ '.git', - '.github', 'node_modules', 'vendor', ]; diff --git a/bin/sync-ai.js b/bin/sync-ai.js new file mode 100755 index 00000000..4eb2361d --- /dev/null +++ b/bin/sync-ai.js @@ -0,0 +1,23 @@ +#! /usr/bin/env node + +/* eslint no-console: 0 */ + +/** + * Thin wrapper around the framework's sync tool. + * + * Runs vendor/rtcamp/wp-framework/bin/sync-ai-instructions.js if the framework + * is installed, otherwise skips cleanly (for example before `composer install` + * has fetched it). Forwards any arguments, so `npm run sync-ai -- --check` works. + */ + +const fs = require( 'fs' ); +const { spawnSync } = require( 'child_process' ); + +const script = 'vendor/rtcamp/wp-framework/bin/sync-ai-instructions.js'; + +if ( ! fs.existsSync( script ) ) { + console.log( 'sync-ai: rtcamp/wp-framework is not installed yet; run `composer install` first. Skipping.' ); + process.exit( 0 ); +} + +process.exit( spawnSync( 'node', [ script, ...process.argv.slice( 2 ) ], { stdio: 'inherit' } ).status ?? 0 ); diff --git a/composer.lock b/composer.lock index aaef4d22..5115ccfb 100644 --- a/composer.lock +++ b/composer.lock @@ -12,12 +12,12 @@ "source": { "type": "git", "url": "https://github.com/rtCamp/wp-framework.git", - "reference": "f4c25118d32f9e3fe7a7b55396dc8465ebc989c1" + "reference": "daf0208ab2e1d9835bf5e664e6c5ffb4e12112d2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/rtCamp/wp-framework/zipball/f4c25118d32f9e3fe7a7b55396dc8465ebc989c1", - "reference": "f4c25118d32f9e3fe7a7b55396dc8465ebc989c1", + "url": "https://api.github.com/repos/rtCamp/wp-framework/zipball/daf0208ab2e1d9835bf5e664e6c5ffb4e12112d2", + "reference": "daf0208ab2e1d9835bf5e664e6c5ffb4e12112d2", "shasum": "" }, "require": { @@ -74,7 +74,7 @@ "issues": "https://github.com/rtCamp/wp-framework/issues", "source": "https://github.com/rtCamp/wp-framework" }, - "time": "2026-06-04T13:16:13+00:00" + "time": "2026-06-07T18:15:35+00:00" } ], "packages-dev": [ @@ -1208,11 +1208,11 @@ }, { "name": "phpstan/phpstan", - "version": "2.2.1", + "version": "2.2.2", "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/dea9c8f2d25cc849391042b71e429c1a4bf82660", - "reference": "dea9c8f2d25cc849391042b71e429c1a4bf82660", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/e5cc34d491a90e79c216d824f60fe21fd4d93bd6", + "reference": "e5cc34d491a90e79c216d824f60fe21fd4d93bd6", "shasum": "" }, "require": { @@ -1268,7 +1268,7 @@ "type": "github" } ], - "time": "2026-05-28T14:44:12+00:00" + "time": "2026-06-05T09:00:01+00:00" }, { "name": "phpstan/phpstan-phpunit", diff --git a/package.json b/package.json index 9c008bea..37f82f34 100644 --- a/package.json +++ b/package.json @@ -57,7 +57,8 @@ "scripts": { "build:dev": "wp-scripts start --no-watch --experimental-modules", "build:prod": "wp-scripts build --experimental-modules", - "init": "./bin/init.js", + "init": "./bin/init.js && npm run sync-ai", + "sync-ai": "node bin/sync-ai.js", "lint:all": "npm-run-all --parallel lint:*", "lint:css": "wp-scripts lint-style ./src", "lint:css:fix": "npm run lint:css -- --fix",