# IM-Press Dev Skill v0.2

**Status:** Draft
**Audience:** AI agents and developers writing PHP that extends IM-Press
**Scope:** New modules, REST endpoints, bootstrap wiring, database schema, admin pages, render callbacks, and deployment verification
**Principle:** Every rule in this document exists because IM-Press already broke that way once.

---

## Purpose

The Content Skill answers: **how should an agent generate IM-Press-compatible page output?**

This Dev Skill answers: **how should an agent write IM-Press PHP without repeating the bugs we already paid for?**

Use this when modifying plugin bootstrap, adding classes, wiring hooks, registering REST routes, creating tables, or declaring an IM-Press change “done.”

---

## Operating stance

1. Match the existing class architecture before adding code.
2. Prefer proven IM-Press patterns over generic WordPress guesses.
3. Do not declare success from source inspection alone.
4. Verify the live runtime that WordPress is actually serving.
5. If a rule here conflicts with an assumption in your prompt, trust the rule and re-check the source.

---

# Bootstrap & init wiring

## Rule 1: Never put IM-Press self-init hooks at file scope

**Why:** v3.0.0 shipped with self-init code at the bottom of class files using `add_action( 'plugins_loaded', [ __CLASS__, 'init' ] )`. Outside a class body, `__CLASS__` resolves to an empty string. That produced fatal bootstrap failures across new modules and had to be centralized back into `im-press.php`.

**Source:** `teaching/lessons/session52-impress-v300-deployment.md`

**Anti-pattern:**
```php
// At file scope, below the class definition
add_action( 'plugins_loaded', array( __CLASS__, 'init' ) );
```

**Correct pattern:**
```php
// In im-press.php bootstrap, after includes
IMPress_SEO_V2::init();
IMPress_Blocks_V2::instance()->init();
IMPress_Forms_V2::get_instance()->init_hooks();
```

**Check:** if a class file contains executable `add_action()` or `new ClassName()` at file scope, stop and move that wiring into the bootstrap.

---

## Rule 2: Match each class’s real init pattern — never assume one bootstrap style fits all

**Why:** v3.0.0 introduced multiple init styles: `::init()`, `::instance()->init()`, `::get_instance()->init_hooks()`, and `new + ->init()`. Agent-generated bootstrap code assumed one uniform pattern and miswired classes, causing partial load failures and misleading debugging because the class files themselves were valid.

**Source:** `teaching/lessons/session52-impress-v300-deployment.md`; `memory/2026-03-31-IMPRESS-V32-CURRENT-STATUS.md#L328-L366`

**Anti-pattern:**
```php
IMPress_Forms_V2::instance()->init(); // guessed pattern
IMPress_Admin_Dashboard_V2::init();   // guessed pattern
```

**Correct pattern:**
```php
IMPress_SEO_V2::init();
IMPress_Blocks_V2::instance()->init();
IMPress_Forms_V2::get_instance()->init_hooks();
$dashboard = new IMPress_Admin_Dashboard_V2();
$dashboard->init();
```

**Check:** before wiring a class, inspect how the class exposes its entrypoint. Do not infer from a neighboring file.

---

## Rule 3: Hook callbacks must be public if WordPress will call them

**Why:** IM-Press shipped hook and REST callback methods marked `private`, then WordPress attempted to call them from outside the class. That caused inaccessible-method failures during bootstrap and route registration. This bit both v3 deploy work and later REST fixes.

**Source:** `teaching/lessons/session52-impress-v300-deployment.md`; `teaching/lessons/session54-impress-v341-rest-route-fix.md`; `memory/2026-03-31-IMPRESS-V32-CURRENT-STATUS.md#L328-L366`

**Anti-pattern:**
```php
private static function register_rest_routes() {
    register_rest_route( 'imp-foo/v1', '/bar', array( ... ) );
}

public static function init() {
    add_action( 'rest_api_init', array( __CLASS__, 'register_rest_routes' ) );
}
```

**Correct pattern:**
```php
public static function register_rest_routes() {
    register_rest_route( 'imp-foo/v1', '/bar', array( ... ) );
}

public static function init() {
    add_action( 'rest_api_init', array( __CLASS__, 'register_rest_routes' ) );
}
```

**Check:** any method referenced in `add_action`, `add_filter`, `register_rest_route`, shortcode registration, or metabox callbacks must be callable from WordPress.

---

## Rule 4: Define bootstrap constants before any class expects them

**Why:** v3.0.0 classes referenced `IM_PRESS_FILE`, `IM_PRESS_URL`, and `IM_PRESS_VERSION`, but those aliases did not exist in the older bootstrap. The plugin source looked coherent, but the runtime failed because classes loaded before their expected constants existed.

**Source:** `teaching/lessons/session52-impress-v300-deployment.md`; `memory/2026-03-31-IMPRESS-V32-CURRENT-STATUS.md#L328-L366`

**Anti-pattern:**
```php
require_once IMPRESS_PLUGIN_DIR . 'includes/class-seo-v2.php';
// ... later
if ( ! defined( 'IM_PRESS_VERSION' ) ) {
    define( 'IM_PRESS_VERSION', '3.0.0' );
}
```

**Correct pattern:**
```php
if ( ! defined( 'IM_PRESS_FILE' ) ) {
    define( 'IM_PRESS_FILE', __FILE__ );
}
if ( ! defined( 'IM_PRESS_URL' ) ) {
    define( 'IM_PRESS_URL', IMPRESS_PLUGIN_URL );
}
if ( ! defined( 'IM_PRESS_VERSION' ) ) {
    define( 'IM_PRESS_VERSION', IMPRESS_VERSION );
}

require_once IMPRESS_PLUGIN_DIR . 'includes/class-seo-v2.php';
```

**Check:** if a class references an alias constant, verify the bootstrap defines it first.

---

## Rule 5: Feature-flagged modules are allowed to 404 — do not “fix” disabled code paths

**Why:** the v3 agent layer was later feature-flagged off by default. Its REST endpoints returning 404 was expected behavior, not a regression. Without that context, an agent can waste hours debugging a feature that is intentionally not loaded.

**Source:** `teaching/lessons/session55-impress-v342-comprehensive-guide.md`

**Anti-pattern:**
```php
// Sees 404 on /imp-workflows/v1 and starts rewriting route registration
IMPress_Agent_Workflows::init();
```

**Correct pattern:**
```php
if ( defined( 'IMPRESS_AGENT_LAYER' ) && IMPRESS_AGENT_LAYER ) {
    IMPress_Agent_Workflows::init();
}
```

**Check:** before treating a missing route as broken, confirm the owning module is supposed to load in this build.

---

## Rule 6: Don't `add_action()` to the hook you're already inside, even at a higher priority

**Why:** v5.0.1 attempted to wire schema self-heal via `add_action( 'init', [ __CLASS__, 'maybe_upgrade_schema' ], 20 )` from inside `Bookings_Legacy::init()`. But `Bookings_Legacy::init()` is itself called from the bootstrap's `add_action( 'init', function() { ... } )` at default priority 10. The new priority-20 callback was registered while the same `init` action was already firing — and on this WP version it did not fire during that invocation. The schema repair never ran on first-pageload-after-upgrade, the option stayed unset, and the legacy `service_id` SQL errors continued. Fixed by calling `self::maybe_upgrade_schema()` directly inline — we were already in `init` context, so the timing was correct without needing the hook indirection. The option-gate inside the method preserves one-shot semantics on subsequent requests.

**Source:** `~/CLAUDE.md` decisions log entry 2026-05-05; `teaching/lessons/im-press-v500-skill.md` v5.0.1 schema self-heal section

**Anti-pattern:**
```php
public static function init() {
    add_action( 'rest_api_init', [ __CLASS__, 'register_rest_routes' ] );

    // ❌ Trying to register a higher-priority callback for the SAME hook
    // we're currently inside. WP does not reliably fire it.
    add_action( 'init', [ __CLASS__, 'maybe_upgrade_schema' ], 20 );
}
// init() is called from inside add_action('init', function() { ... }) at priority 10
// → maybe_upgrade_schema NEVER fires on this request
```

**Correct pattern:**
```php
public static function init() {
    add_action( 'rest_api_init', [ __CLASS__, 'register_rest_routes' ] );

    // ✅ We're already inside the init action — call directly.
    // The option-gate inside maybe_upgrade_schema makes this one-shot.
    self::maybe_upgrade_schema();
}

public static function maybe_upgrade_schema() {
    $stored = get_option( 'imp_foo_schema_version', '0' );
    if ( version_compare( $stored, self::SCHEMA_VERSION, '>=' ) ) {
        return; // single get_option lookup on subsequent requests
    }
    // ...do the work, then update_option to gate future runs
}
```

**Alternative correct pattern** (when you need to defer to a *later* hook the parent action does not fire):
```php
public static function init() {
    // ✅ Hook a hook that fires AFTER init completes
    add_action( 'wp_loaded', [ __CLASS__, 'maybe_upgrade_schema' ] );
}
```

**Check:** before calling `add_action( 'X', ... )` inside another callback that is itself running on hook `X`, ask: am I already inside that hook? If yes, call the function directly (option-gated) or use a *different* later hook. Never expect WP to "circle back" to a higher priority of the same action mid-firing.

---

# REST API registration

## Rule 7: Never call `register_rest_route()` outside `rest_api_init`

**Why:** v3.4.0 had two modules calling `register_rest_route()` directly during `init`. On WP 6.9.4, that prematurely triggered `rest_api_init`, so modules loaded later never got their own REST callbacks registered. Result: whole groups of IM-Press endpoints silently 404’d across all sites.

**Source:** `teaching/lessons/session54-impress-v341-rest-route-fix.md`; `teaching/lessons/session55-impress-v342-comprehensive-guide.md`; `memory/2026-03-31-IMPRESS-V32-CURRENT-STATUS.md#L328-L366`

**Anti-pattern:**
```php
public static function init() {
    self::register_rest_routes();
}

private static function register_rest_routes() {
    register_rest_route( 'imp-foo/v1', '/bar', array( ... ) );
}
```

**Correct pattern:**
```php
public static function init() {
    add_action( 'rest_api_init', array( __CLASS__, 'register_rest_routes' ) );
}

public static function register_rest_routes() {
    register_rest_route( 'imp-foo/v1', '/bar', array( ... ) );
}
```

**Check:** grep for `register_rest_route(` and confirm every call is inside a function hooked from `rest_api_init`.

---

## Rule 8: Public form and public-submit routes must be tested in the same auth context they will actually run under

**Why:** IM-Press Forms V2 looked healthy when tested through admin/internal dispatch, but the real browser submission path failed with `rest_cookie_invalid_nonce` because the frontend JS was sending `X-WP-Nonce` to a public unauthenticated endpoint. Internal admin-context testing hid the real bug.

**Source:** `memory/2026-04-15.md#L652-L676`

**Anti-pattern:**
```js
fetch('/wp-json/imp-forms/v2/submit', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'X-WP-Nonce': window.wpApiSettings.nonce
  },
  body: JSON.stringify(payload)
});
```

**Correct pattern:**
```js
fetch('/wp-json/imp-forms/v2/submit', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json'
  },
  body: JSON.stringify(payload)
});
```

**Check:** if the route is intended for anonymous visitors, test it anonymously in a browser-like context before declaring it fixed.

---

## Rule 9: Don’t rely on one route family’s success to infer that another route family is healthy

**Why:** IM-Press had cases where older route families still worked while newer ones were broken by timing, auth, or load-order mistakes. The v3.4.1 REST bug was hard to find partly because v3.0 modules still looked healthy while later modules all failed.

**Source:** `teaching/lessons/session54-impress-v341-rest-route-fix.md`; `memory/2026-04-15.md#L443-L454`

**Anti-pattern:**
```text
/imp-email/v1 works, therefore /imp-forms/v2 and /imp-lightbox/v1 are probably fine.
```

**Correct pattern:**
```text
Verify each affected namespace or feature route directly:
- public route
- authenticated route
- browser submission route if frontend JS is involved
```

**Check:** validate the exact route family you changed, not a nearby one.

---

# Database schema & options

## Rule 10: Use canonical option names and never invent a new prefix on the fly

**Why:** IM-Press had repeated option-name drift: SMTP settings, license keys, and other settings were saved under mismatched names. That caused silent failures where UI and runtime both looked valid but different code paths were reading different options.

**Source:** `~/CLAUDE.md` decisions log entries for 2026-03-25 and 2026-03-26; `teaching/lessons/session55-impress-v342-comprehensive-guide.md`

**Anti-pattern:**
```php
update_option( 'im_press_license_key', $key );
update_option( 'imp_smtp_username', $user );
```

**Correct pattern:**
```php
update_option( 'impress_license_key', $key );
update_option( 'imp_smtp_user', $user );
update_option( 'imp_smtp_pass', $pass );
```

**Check:** before creating or reading an option, verify the canonical name already used by IM-Press. Never introduce `im_press_`.

---

## Rule 11: WordPress activation hooks are not enough for deployed schema changes

**Why:** IM-Press was deployed through SSH/plugin swaps where activation hooks did not fire. That left required custom tables missing even though the plugin code was present. Forms V2 appeared “installed” while its storage tables were absent, forcing later self-heal work and one-shot repair scripts.

**Source:** `~/CLAUDE.md` decisions log entries for 2026-03-25 and 2026-04-15; `memory/2026-04-15.md#L652-L676`

**Anti-pattern:**
```php
register_activation_hook( __FILE__, array( __CLASS__, 'create_tables' ) );
// Assume tables now exist on every deployed site
```

**Correct pattern:**
```php
register_activation_hook( __FILE__, array( __CLASS__, 'create_tables' ) );

public static function maybe_upgrade_schema() {
    $installed = get_option( 'imp_forms_schema_version', '' );
    if ( $installed !== self::SCHEMA_VERSION ) {
        self::create_tables();
        update_option( 'imp_forms_schema_version', self::SCHEMA_VERSION );
    }
}

add_action( 'plugins_loaded', array( __CLASS__, 'maybe_upgrade_schema' ), 20 );
```

**Check:** any module that owns tables should have a runtime schema repair path, not only an activation path.

---

## Rule 12: `dbDelta()` SQL must be syntactically exact, especially before `KEY` lines

**Why:** v3.4.5 shipped SQL with missing commas before `KEY` lines. `dbDelta()` surfaced MySQL errors into output, which then broke header sending and took wp-admin down across all sites.

**Source:** `teaching/lessons/session56b-impress-v345-namespace-sql-hotfix.md`

**Anti-pattern:**
```sql
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
KEY product_id (product_id),
```

**Correct pattern:**
```sql
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
KEY product_id (product_id),
```

**Check:** review every CREATE TABLE block as SQL, not just as PHP strings. Missing punctuation here is runtime-fatal, not cosmetic.

---

# PHP language traps

## Rule 13: Never use `{$self::$prop}` inside double-quoted strings

**Why:** IM-Press shipped dozens of `{$self::$prop}` interpolations. PHP interprets `$self` as a variable, not as the `self` keyword, so the intended table or property name never resolves correctly.

**Source:** `teaching/lessons/session52-impress-v300-deployment.md`; `teaching/lessons/session55-impress-v342-comprehensive-guide.md`; `memory/2026-03-31-IMPRESS-V32-CURRENT-STATUS.md#L328-L366`

**Anti-pattern:**
```php
$sql = "SELECT * FROM {$self::$tables['keys']} WHERE id = %d";
```

**Correct pattern:**
```php
$sql = "SELECT * FROM " . self::$tables['keys'] . " WHERE id = %d";
```

**Check:** if you need a static property inside SQL or markup strings, concatenate it.

---

## Rule 14: In namespaced IM-Press files, use namespace-aware class references

**Why:** v3.4.5 crashed wp-admin because a namespaced file instantiated the wrong class name. Inside `namespace IMPress;`, `new IMPressAdmin_Dashboard()` resolved to `\IMPress\IMPressAdmin_Dashboard`, which does not exist. Using `\Admin_Dashboard()` would also be wrong because that points to global namespace. Same-namespace classes should use the bare class name.

**Source:** `teaching/lessons/session56b-impress-v345-namespace-sql-hotfix.md`

**Anti-pattern:**
```php
namespace IMPress;

new IMPressAdmin_Dashboard();
new \Admin_Dashboard();
```

**Correct pattern:**
```php
namespace IMPress;

new Admin_Dashboard();
```

**Check:** before instantiating a class, inspect the target file’s namespace and resolve accordingly.

---

## Rule 15: Avoid arrow functions in code that WordPress will store or render from post content

**Why:** WordPress encoded `=>` to `=&gt;` in stored content contexts, breaking inline JavaScript used in IM-Press-managed pages. The pattern was seen repeatedly in page JS and had to be replaced with classic `function()` syntax.

**Source:** `teaching/lessons/session55-impress-v342-comprehensive-guide.md`; `~/CLAUDE.md` decisions log entries for 2026-03-26 and 2026-04-16

**Anti-pattern:**
```html
<script>
document.querySelectorAll('.card').forEach((card) => {
  card.addEventListener('click', () => {
    openCard(card);
  });
});
</script>
```

**Correct pattern:**
```html
<script>
document.querySelectorAll('.card').forEach(function(card) {
  card.addEventListener('click', function() {
    openCard(card);
  });
});
</script>
```

**Check:** if the JS will live inside WordPress-managed content, default to `function()` syntax.

---

# Live verification

## Rule 16: Source code is not proof — verify the deployed runtime WordPress is actually serving

**Why:** IM-Press repeatedly hit source-vs-deployed drift and stale-runtime traps: a local source tree was not the same as the working deployed ZIP, and later PHP fixes appeared “applied” while Hostgator’s web runtime still served stale OPcache. There were also staging cases where health checks lied because they validated the wrong path. Looking at source alone was not enough.

**Source:** `~/CLAUDE.md` decisions log entries for 2026-03-18, 2026-03-26, and 2026-04-05; `memory/2026-04-15.md#L443-L454`

**Anti-pattern:**
```bash
# Edit source locally
# Deploy ZIP
ssh hostgator 'php -r "opcache_reset();"'
# Assume live site now uses the new code
```

**Correct pattern:**
```bash
# 1. Verify the exact deployed artifact / version
# 2. Reset OPcache in HTTP context if PHP files changed
# 3. Hit the live endpoint or page the feature uses
# 4. Confirm the behavior, not just the file contents
```

**Check:** never say “done” until you have evidence from the live runtime path.

---

## OPcache verification rule

Hostgator’s web PHP and CLI PHP do not share the same OPcache.

**What does not prove a PHP fix is live:**
- `touch file.php`
- replacing files over SSH
- CLI `php -r "opcache_reset();"`

**Reliable pattern:**
```php
<?php
opcache_reset();
echo 'OK';
unlink(__FILE__);
```

Trigger that file over HTTP, then re-test the actual feature.

**Source:** `teaching/lessons/session56b-impress-v345-namespace-sql-hotfix.md`; `~/CLAUDE.md` decision log 2026-04-05

---

## Health-check interpretation rule

IM-Press internal health checks that call back into the site over HTTP can be misleading on managed hosts with Cloudflare or ModSecurity in front. A failing self-fetch is not automatically a failing public page.

**Preferred order:**
1. Validate locally inside WordPress when checking internal state.
2. Validate externally over HTTP only when the feature truly depends on external HTTP behavior.
3. When those differ, document the difference instead of flattening them into one verdict.

**Source:** `memory/2026-04-15.md#L443-L454`

---

# Verification before declaring done

Run this checklist before saying an IM-Press PHP change is complete.

1. **Bootstrap check** — Confirm every new or changed class is wired with its real init pattern.
2. **File-scope check** — Confirm no executable self-init code was added at file scope.
3. **Callback visibility check** — Confirm every hook or REST callback WordPress invokes is `public`.
4. **Hook re-entry check** — Confirm no `add_action( 'X', ... )` is registered while the calling code is itself already running on hook `X`. Either call inline (option-gated) or hook a *different* later action.
5. **REST timing check** — Confirm no `register_rest_route()` call happens outside a `rest_api_init` callback.
6. **Canonical naming check** — Confirm option names and constants match established IM-Press conventions.
7. **Schema check** — If tables are involved, confirm there is a runtime schema repair path, not just activation-hook creation.
8. **SQL syntax check** — Review all CREATE TABLE statements for commas, keys, and dbDelta-safe formatting.
9. **PHP trap check** — Scan for `{$self::$...}`, bad namespace references, and other known anti-patterns from this document.
10. **Artifact check** — Confirm the source you changed is the artifact that was actually deployed.
11. **OPcache check** — If PHP changed on Hostgator, reset OPcache through HTTP context before testing.
12. **Exact-route check** — Test the exact endpoint, admin page, shortcode, or render path you changed.
13. **Real-context check** — If the feature is public-facing, test it as a public visitor. If it is admin-only, test it in admin context. Do not let one context stand in for the other.
14. **Cross-signal check** — If internal dispatch and public HTTP disagree, record both and resolve the mismatch before declaring success.
15. **Version proof** — Capture a version/health signal or direct output proving the live runtime matches the intended build.
16. **Done check** — Only declare done when the live behavior matches the intended fix.

---

## Short form for agent priming

> When writing IM-Press PHP, never add self-init hooks at file scope, always match each class’s real init pattern, make all WordPress-invoked callbacks public, and never `add_action()` to a hook you’re already inside (call inline with an option-gate, or hook a later action). Never call `register_rest_route()` outside `rest_api_init`. Use canonical IM-Press option names, add runtime schema repair for custom tables, avoid `{$self::$prop}` interpolation and bad namespace references, and treat source edits as unproven until the live runtime is verified. On Hostgator, reset OPcache via HTTP context, not CLI. Test the exact route or render path you changed in the same auth context real users will hit.

---

# Appendix: Source-file references

One current-source reference per rule. These are examples of the correct pattern in the live plugin tree, not extra rules.

1. **Rule 1 (centralized bootstrap, no class-file self-init):** `im-press.php` line ~219, the bootstrap-owned `add_action( 'init', function() { ... } )` initializes modules centrally instead of letting class files self-register at file scope.
2. **Rule 2 (match the class’s real init pattern):** `includes/class-forms-v2.php` line ~88, `Forms_V2::init()` delegates to `static::get_instance()` because that class initializes through its singleton constructor and `init_hooks()`.
3. **Rule 3 (public WordPress callbacks):** `includes/class-safe-deploy.php` line ~214, `public function safe_update( \WP_REST_Request $request )` is the public callback registered by `register_routes()`.
4. **Rule 4 (constants before includes):** `im-press.php` line ~20, `IMPRESS_VERSION`, `IMPRESS_PLUGIN_DIR`, `IMPRESS_PLUGIN_URL`, and `IMPRESS_PLUGIN_FILE` are defined before any class files are required.
5. **Rule 5 (feature-gated modules may no-op):** `includes/class-products.php` line ~32, `Products::init()` checks `Licensing::is_pro()` and returns early instead of registering disabled feature routes and UI.
6. **Rule 6 (same-action priority):** `includes/class-bookings-legacy.php` line ~45, `init()` calls `self::maybe_upgrade_schema()` inline because bootstrap already invokes it during `init`.
7. **Rule 7 (REST route timing):** `includes/class-skill.php` line ~38, `Skill::init()` hooks `register_routes()` onto `rest_api_init`; the route declarations start inside that callback at line ~41.
8. **Rule 8 (test public routes as public):** `includes/class-email-v2.php` line ~206, the public `/subscribe` endpoint uses `rest_permission_public`, making the unauthenticated context explicit.
9. **Rule 9 (test each route family independently):** `includes/class-email-v2.php` line ~198, `register_rest_routes()` declares legacy `imp-email/v1` routes and newer `imp-email/v2` routes separately instead of treating one family as proof for the other.
10. **Rule 10 (canonical option names):** `includes/class-site-config.php` line ~25, `const OPTION_KEY = 'imp_site_config';` centralizes the established option key rather than inventing a new prefix.
11. **Rule 11 (runtime schema repair beyond activation):** `im-press.php` line ~165, the `plugins_loaded` self-heal block calls module `maybe_create_tables()` methods during runtime, guarded by `class_exists()`/`is_callable()`.
12. **Rule 12 (dbDelta-safe SQL):** `includes/class-bookings-legacy.php` line ~77, the `CREATE TABLE` statement includes `PRIMARY KEY` and `KEY` lines cleanly before the final `dbDelta( $sql )` call at line ~87.
13. **Rule 13 (avoid static-property interpolation traps):** `includes/class-bookings-legacy.php` line ~70, the table name is assigned to `$blocked_table` before interpolation, avoiding fragile `{$self::$prop}`-style double-quoted strings.
14. **Rule 14 (namespace-aware class references):** `includes/class-safe-deploy.php` line ~230, the namespaced class returns `new \WP_REST_Response(...)` with a leading slash for the global WordPress class.
15. **Rule 15 (classic JS in stored/rendered content):** `includes/class-blocks-v2.php` line ~1094, rendered inline tab JavaScript uses `document.addEventListener("DOMContentLoaded", function() { ... })` and nested `forEach(function(btn) { ... })` callbacks instead of arrow functions.
16. **Rule 16 (runtime proof over source inspection):** `includes/class-skill.php` line ~97, the Skill endpoint reads the runtime `IMPRESS_VERSION` constant and returns it in the response payload, giving agents a live version signal to verify against source expectations.

---

**Version:** Dev Skill v0.2 (draft)
**Companion:** `docs/SKILL.md` (Content Skill)
