# Claude Standards (Cross-Plugin Coding Rules)

Rules Claude (and humans) should follow when writing code in any BW plugin. These apply across every plugin in this repo.

## Naming & prefixing

- **Slugs:** kebab-case, starting with `bw-`. `bw-source-capture`, `bw-ai-schema-pro`.
- **Display names:** "BW \<Title Case\>". "BW Source Capture", "BW AI Schema Pro".
- **Text domain:** match the slug exactly. `bw-source-capture`.
- **Functions:** `bw_<slug_underscored>_<verb>()`. `bw_source_capture_register_assets()`.
- **Classes:** `BW_<Slug_Pascal>_<Thing>`. `BW_Source_Capture_Admin`. File: `class-bw-source-capture-admin.php`.
- **Constants:** `BW_<SLUG_UPPER>_<NAME>`. `BW_SOURCE_CAPTURE_VERSION`.
- **Options:** `bw_<slug_underscored>_<name>`. `bw_source_capture_settings`.
- **Post meta keys:** `_bw_<slug_underscored>_<name>` (leading underscore to hide from Custom Fields UI).
- **Hooks:** `bw_<slug_underscored>_<name>`. Filter `bw_source_capture_allowed_sources`.
- **Custom tables:** `{$wpdb->prefix}bw_<slug_underscored>_<name>`.

## File structure

- Every PHP file starts with: `<?php` then `defined( 'ABSPATH' ) || exit;`.
- One class per file, named `class-<slug-pascal-kebab>-<thing>.php`.
- Main plugin file is thin: just header, constants, require()s, bootstrap hook.

## Security

- **Output escaping:** always. `esc_html()`, `esc_attr()`, `esc_url()`, `esc_textarea()`, `wp_kses_post()` for rich content.
- **Input sanitization:** always. `sanitize_text_field()`, `sanitize_email()`, `absint()`, `wp_unslash()` before anything else.
- **SQL:** always use `$wpdb->prepare()` for anything with variables. Never concatenate.
- **Nonces:** `wp_verify_nonce()` on every state-changing admin action. `check_ajax_referer()` on AJAX.
- **Capabilities:** `current_user_can()` on every admin action. Don't rely on `is_admin()`.
- **File ops:** never pass `$_GET/$_POST` directly to `file_get_contents()`, `include`, `require`, `fopen()`.
- **Forbidden:** `eval`, `assert($var)`, `create_function`, raw `base64_decode` (without a documented reason in `.security-allowlist`).
- **HTTP:** prefer `wp_remote_get()` / `wp_remote_post()` over `curl_*` or `file_get_contents()`. Never disable TLS verification.

## Data persistence

- **Options:** use `get_option()` / `update_option()`. Group related settings into a single option array; don't create a new row per setting.
- **Transients:** use for cache. Expire appropriately.
- **Custom tables:** only if options won't scale. Create/update via `dbDelta()` in an activation hook. Drop in `uninstall.php`.
- **Post meta:** prefix with underscore to hide from admin UI unless users should edit it.

## WordPress idioms

- Load translations: `load_plugin_textdomain()` on `plugins_loaded`.
- Register assets with `wp_register_style()` / `wp_register_script()`, enqueue with `wp_enqueue_*()` at the right hook.
- Use `plugin_dir_path()`, `plugin_dir_url()`, `plugins_url()`.
- Use `admin_url()`, `home_url()`, `rest_url()` — never hardcode paths.
- Register REST routes on `rest_api_init`.
- Register shortcodes on `init`.
- Register post types and taxonomies on `init`.

## PHP

- **Minimum version:** PHP 7.4 (matches WordPress 6.0 requirement).
- **Strict types:** not required, but welcome in new code (`declare(strict_types=1);`).
- **Namespaces:** not required (WP ecosystem is mostly prefixed globals), but fine in includes/ for new plugins.
- **Composer autoload:** avoid unless necessary; it adds a deploy step. Prefer WordPress's SPL autoloader pattern or manual `require_once`s.
- **No hard dependencies on other plugins** without defensive checks (`class_exists()`, `function_exists()`).

## JavaScript / CSS

- Enqueue via WordPress, never inline `<script>` or `<link>` tags in output.
- No jQuery unless necessary for legacy integration; prefer vanilla JS.
- Version assets with the plugin version constant so cache busts on release.
- Minify for production (Phase 2 — not required initially).

## Documentation

- Every plugin must have `README.md`, `CLAUDE.md`, `CHANGELOG.md`, `docs/SPEC.md`, `docs/ARCHITECTURE.md`, `docs/TESTING.md`, `LICENSE`.
- Update `CHANGELOG.md` for every user-visible change.
- Update `docs/ARCHITECTURE.md` when adding major components.
- Update `docs/SESSION-LOG.md` at the end of substantive work sessions.

## Release

- Never bypass `cleanup-scan`, `security-scan`, or `test-plugin`.
- Version must be bumped via `tools/bump-version.sh` before release.
- `CHANGELOG.md` must have a real entry (not just `[Unreleased]`) before release.

## Commit discipline (when git arrives)

- Conventional Commits: `feat(bw-<slug>): ...`, `fix(bw-<slug>): ...`, `docs(...)`, `chore(...)`.
- One logical change per commit.
- No "WIP" / "fix typo" / "asdf" commits in final history.

## Debug cruft

These will fail `cleanup-scan` and block release:

- `var_dump()`, `print_r()` in PHP source
- `console.log()`, `console.debug()`, `debugger;` in JS source
- Hardcoded `localhost`, `.test`, `.local`, `192.168.*` URLs
- Debug files at plugin root: `debug.php`, `test.php`, `scratch.php`, `debug-*.php`, etc.
- Markers: `TODO: REMOVE`, `REMOVE BEFORE RELEASE`, `DO NOT SHIP`, `@TEMP`, `@HACK`
- Empty `catch` blocks
- `define('WP_DEBUG', ...)` (plugins must never set this)

If you have a legitimate reason to keep one (e.g. an admin-gated diagnostics tool), add a justified entry to `bw-<slug>/.cleanup-allowlist`.

## When you're unsure

Look at an existing BW plugin in this repo for a reference implementation. If still unsure, ask rian.
