Lunar PR Review
Overview
Lunar is a Laravel-based headless commerce package distributed as a monorepo (packages/core, packages/admin, plus payment/search adapters). PRs target the 1.x branch. This skill runs a deterministic review pass tuned to the patterns that actually exist in this codebase, surfaces findings as path:line — issue — fix, and groups them by severity. It does not make changes.
When to Activate
- Slash command
/lunar:pr-review (with optional base branch arg).
- Natural-language requests: "review this PR", "review the branch", "review the diff", "check my changes before I push".
- Skip if the user wants a brand-new implementation — this skill is read-only review.
Scope
- In scope: changed files between
HEAD and origin/1.x (or a user-supplied base).
- Out of scope: unchanged files, generated docs (
packages/*/resources/views/vendor), vendor/, lock files beyond noting they changed.
Workflow
- Determine base branch. Default
1.x. If the user passed an arg (e.g. /lunar:pr-review main), use that. Run git fetch origin <base> --quiet.
- Collect the diff.
- Files:
git diff --name-status origin/<base>...HEAD
- Commits:
git log --oneline origin/<base>..HEAD
- If empty, report "no changes vs
<base>" and stop.
- Classify each changed file into buckets:
migrations, translations, models, filament, observers/events, tests, composer, factories, other.
- Run category checks (see below). Build a findings list with
severity, path, line (when known), issue, suggested fix.
- Render report grouped as Blockers → Should fix → Nits. End with a "Run before merge" footer.
Categories
Each check below states what to look for and the severity to assign. See references/checklist.md for full rationale and good/bad examples.
Translations (Blocker on missing, Should-fix on stale)
Run php .claude/skills/lunar-pr-review/scripts/missing-translations.php from the repo root. The script flattens nested array keys to dot notation and diffs en/ against ar bg de es fa fr hr hu mn nl pl pt_BR ro tr vi for every packages/*/resources/lang/<locale>/<file>.php.
[missing] … → key exists in en/ but not in target locale → Blocker if the PR added the key; Should-fix if pre-existing.
[stale] … → key in target locale no longer in en/ → Should-fix.
[missing-file] … → whole file absent in target locale → Should-fix.
Also scan added strings in PHP/Blade for hardcoded user-facing English that should be __('lunarpanel::…').
Tests (Pest) — Should-fix
For each newly added class under packages/*/src/:
- Models, services, actions, observers, jobs, and events should have a matching Pest test under
tests/{core,admin,opayo,paypal,shipping,stripe,search}/.
- New
Eloquent models additionally require a factory under packages/*/database/factories/.
- Use
php artisan make:test --pest <Name>Test (do not write to tests/Feature/ paths directly).
Migrations — Blocker
For each migration in packages/*/database/migrations/:
- Must
extend Lunar\Base\Migration (not Illuminate\Database\Migrations\Migration directly).
- Must use
$this->prefix when referencing table names (Schema::table($this->prefix.'orders', …)).
- Must implement
down() symmetrically — every Schema::create / column-add in up() is reversed.
- Foreign keys use
foreignId(...)->constrained() with explicit onDelete/onUpdate where the parent model uses soft deletes.
- Destructive column drops on existing tables require a justification comment or a paired data migration.
- File name follows
YYYY_MM_DD_HHMMSS_<verb>_<thing>.php.
Filament resources & admin — Should-fix
For files under packages/admin/src/Filament/Resources/:
protected static ?string $model = …Contract::class; — must reference an interface from Lunar\Models\Contracts\*, not a concrete model.
- Labels:
getLabel, getPluralLabel, getNavigationGroup, form/column labels must come from __('lunarpanel::…'); flag any hardcoded English strings.
- New resources set
protected static ?string $permission.
- Navigation icons resolved via
FilamentIcon::resolve('lunar::…').
Model contracts (type-hinting) — Blocker
Eloquent models in packages/*/src/Models/ are bound to interfaces in Lunar\Models\Contracts\* via the model manifest, so consumers can swap in their own subclass. Code that type-hints the concrete model defeats that extension point.
Anywhere outside the model class itself and its factory/seeder, type hints, return types, property types, and PHPDoc references must use the contract, not the concrete model:
- Method parameters & return types:
public function handle(CartContract $cart, Closure $next): CartContract.
- Promoted/declared properties:
protected ?OrderContract $order = null;.
- PHPDoc:
@param Closure(OrderContract): mixed $next, @return Collection<int, ProductContract>.
- DI / container resolution:
app(CartContract::class), not app(Cart::class) or new Cart.
Exemptions (concrete class is correct here):
- The model class itself, its relations, scopes, and casts.
- Factories, seeders, and migrations.
- Tests creating fixtures (
Product::factory()->create()).
instanceof checks against the concrete class only when there is a documented reason (otherwise prefer the contract).
Flag as Blocker in new/changed code: use Lunar\Models\Foo; followed by a type hint Foo $foo in a service, action, manager, pipeline, observer, listener, event, or job. Suggest the fix as use Lunar\Models\Contracts\Foo as FooContract; and rename the hint to FooContract.
PHP conventions — Should-fix / Nit
Per repo CLAUDE.md:
- Constructor property promotion required for public constructor params; flag empty zero-param
__construct().
- Explicit return types and typed parameters on every method.
- Curly braces around every control-structure body, even one-liners.
- Prefer PHPDoc blocks over inline comments; array shape types in PHPDoc.
- Never call
env() outside config/.
- Enum case keys in
TitleCase.
Static analysis & style — Nit (reminder)
- If changed files fall inside paths excluded by
phpstan.neon.dist, note that PHPStan will not catch issues there.
- Remind the user to run
vendor/bin/pint --dirty --format agent before pushing.
Composer / dependencies — Blocker (requires user confirmation)
Any diff in composer.json or any packages/*/composer.json:
- Flag added/removed/upgraded dependencies as a Blocker that needs explicit user sign-off per
CLAUDE.md.
- Note PHP version, Laravel version, or Filament version bumps separately — they affect downstream consumers.
Channel scoping — Blocker
Lunar is multi-channel. Models using Lunar\Base\Traits\HasChannels (Product, Collection, Discount, …) expose scopeChannel() for filtering.
- New queries on these models in admin lists, storefront-facing endpoints, or scheduled jobs should call
->channel($channel) (or be explicitly justified as cross-channel).
- New columns/relations on a channelled model that drive visibility need the channel scope wired in, not just the column added.
Reuse existing scopes & traits — Should-fix
Before reviewing any new Eloquent query, check whether the related trait/model already exposes a scope for the same condition. Hand-rolled ->where(...)->orWhere(...) chains that duplicate a trait scope are a Should-fix.
Lunar\Base\Traits\HasCustomerGroups exposes a scope for customer-group eligibility (enabled/visible/starts_at/ends_at windowing). New ->customerGroups()->where('enabled', true)… chains in resources, widgets, validators, or pipelines should use that scope.
Lunar\Base\Traits\HasChannels — see Channel scoping above; use ->channel(...), not ->channels()->where('enabled', true)….
Lunar\Base\Traits\HasUrls — use the default() / active scope rather than re-checking columns inline.
- Status enums on
Product, Order, etc. — prefer ->whereIn('status', SomeStatus::active()) or the existing helper over string-literal comparisons.
When flagging, name the specific trait/scope the author should use, not just "use a scope". If the scope doesn't exist yet but the same chain is repeated 2+ times in the diff, suggest adding it to the trait.
Control-flow simplification — Nit (Should-fix when stacked)
Lunar's Filament resources and validators have accumulated long ladders of if ($x) { return true; } if ($y) { return true; } inside closures (notably Shout::make(...)->hidden(fn ...)). Reviewers consistently push back on these — flag them.
- Multiple sequential
if (cond) { return $literal; } returning the same literal collapse into one if (a || b || c) { return $literal; } or a single boolean expression on the return.
- A trailing
if (! $x) { return true; } return (bool) $x; is just return ! $x; (or the equivalent expression).
- Guard clauses are fine and preferred over nested
if/else, but each one should branch to a different outcome — back-to-back guards returning the same value are noise.
- Closures that grow past ~5 statements doing query composition should be extracted to a named private method or, better, an Eloquent scope (see above).
Treat a single instance as a Nit; flag as Should-fix when the same closure stacks 3+ early returns or when the pattern repeats across sibling components in the same file.
Money & price handling — Blocker
Money in Lunar lives in Lunar\DataTypes\Price (integer minor units + Currency) and the Lunar\Base\Casts\Price cast.
- Flag new monetary columns as
decimal/float — they should be unsignedInteger/bigInteger and cast through Price.
- Flag float arithmetic on money:
* 0.01, (float), round() on raw totals, +/-/* between mixed-currency Price instances.
- New tax/discount/shipping totals must go through the existing pipeline calculation, not be set directly on the model.
Search indexing (Scout/Meilisearch) — Should-fix
Search documents are built by packages/core/src/Search/*Indexer.php (ProductIndexer, CollectionIndexer, CustomerIndexer, OrderIndexer, ProductOptionIndexer).
- Adding a queryable/filterable attribute to an indexed model? The corresponding
*Indexer must include it in the document.
- Removing an attribute? Note that indexed documents are now stale and need a re-index step (
php artisan scout:import "<Model>").
- New filterable/sortable attributes likely need the Meilisearch
filterableAttributes / sortableAttributes config updated.
Pipelines — Blocker on calculation paths
Cart, cart line, cart prune, and order pipelines live under packages/core/src/Pipelines/. Lunar resolves them from config so consumers can extend or replace them.
- Adding a new field to
CartLine/Cart/Order that affects price, tax, shipping, or eligibility means adding (or extending) the relevant pipeline stage — not just persisting the field.
- New pipeline stages must be registered in
config/lunar/cart.php / config/lunar/orders.php and have a Pest test.
- Reordering existing stages is a behavior change — flag loudly.
Event payload stability — Blocker
Event classes under packages/*/src/Events/ and packages/*/src/Base/Events/ are public API on 1.x. Listeners depend on the constructor signature and public properties.
- Renaming/removing/retyping constructor params or public properties on an existing event is breaking.
- Adding new optional params at the end of a constructor is acceptable; new public properties are acceptable.
- New events that should be dispatched from observers/actions but aren't — flag as Should-fix.
Public API surface — Blocker
1.x is a stable line. The following count as breaking changes and must be flagged loudly:
- Any change in
packages/*/src/Models/Contracts/ (added/removed/renamed methods, changed signatures).
- Public method signature changes on classes that implement a
Contracts/* interface.
- Removed or renamed events under
packages/*/src/Base/Events/ or packages/*/src/Events/.
- Removed config keys in
packages/*/config/*.php.
- Migrations that rename existing tables/columns without a backwards-compatible accessor.
Output Format
# Lunar PR Review — <branch> vs <base>
<N commits, M files changed>
## Blockers (must fix before merge)
- `path/to/file.php:L42` — <issue> — <suggested fix>
- …
## Should fix
- `path/to/file.php:L11` — <issue> — <suggested fix>
- …
## Nits
- `path/to/file.php` — <issue>
- …
## Run before merge
- `vendor/bin/pint --dirty --format agent`
- `php artisan test --compact`
- `vendor/bin/phpstan analyse`
If there are no findings in a section, omit it. If there are zero findings overall, output a single line: No findings vs <base> across <N> files.
Do / Don't
Do:
- Run the translations script — do not eyeball 16 locales.
- Quote line numbers from
git diff --unified=0 when available so the user can jump to them.
- Recommend
php artisan make:* commands for any missing scaffolding rather than describing hand-rolled code.
Don't:
- Don't edit any files.
- Don't push, force-push, rebase, or reset.
- Don't run
composer update, composer require, npm install, or migrations.
- Don't claim a file is fine without actually reading the changed hunks.
- Don't duplicate findings — each
path:line appears once at its highest severity.
References
references/checklist.md — full rubric with good/bad examples drawn from the codebase. Load when a category needs deeper explanation.
scripts/missing-translations.php — run from repo root, exits 0, prints [missing]/[stale]/[missing-file] lines + summary.
1---2name: lunar-pr-review3description: Review a Lunar pull request or branch against the 1.x main branch. Activates on /lunar:pr-review, or when the user asks to "review this PR/branch/diff" inside the Lunar monorepo. Checks translation completeness across 16 locales, missing tests/factories, migration safety, Filament contract usage, PHP conventions, and breaking-change risk on the public contract surface.4---56# Lunar PR Review78## Overview910Lunar is a Laravel-based headless commerce package distributed as a monorepo (`packages/core`, `packages/admin`, plus payment/search adapters). PRs target the `1.x` branch. This skill runs a deterministic review pass tuned to the patterns that actually exist in this codebase, surfaces findings as `path:line — issue — fix`, and groups them by severity. It does not make changes.1112## When to Activate1314- Slash command `/lunar:pr-review` (with optional base branch arg).15- Natural-language requests: "review this PR", "review the branch", "review the diff", "check my changes before I push".16- Skip if the user wants a brand-new implementation — this skill is read-only review.1718## Scope1920- **In scope**: changed files between `HEAD` and `origin/1.x` (or a user-supplied base).21- **Out of scope**: unchanged files, generated docs (`packages/*/resources/views/vendor`), `vendor/`, lock files beyond noting they changed.2223## Workflow24251. **Determine base branch.** Default `1.x`. If the user passed an arg (e.g. `/lunar:pr-review main`), use that. Run `git fetch origin <base> --quiet`.262. **Collect the diff.**27 - Files: `git diff --name-status origin/<base>...HEAD`28 - Commits: `git log --oneline origin/<base>..HEAD`29 - If empty, report "no changes vs `<base>`" and stop.303. **Classify** each changed file into buckets: `migrations`, `translations`, `models`, `filament`, `observers/events`, `tests`, `composer`, `factories`, `other`.314. **Run category checks** (see below). Build a findings list with `severity`, `path`, `line` (when known), `issue`, `suggested fix`.325. **Render report** grouped as **Blockers** → **Should fix** → **Nits**. End with a "Run before merge" footer.3334## Categories3536Each check below states what to look for and the severity to assign. See `references/checklist.md` for full rationale and good/bad examples.3738### Translations (Blocker on missing, Should-fix on stale)3940Run `php .claude/skills/lunar-pr-review/scripts/missing-translations.php` from the repo root. The script flattens nested array keys to dot notation and diffs `en/` against `ar bg de es fa fr hr hu mn nl pl pt_BR ro tr vi` for every `packages/*/resources/lang/<locale>/<file>.php`.4142- `[missing] …` → key exists in `en/` but not in target locale → **Blocker** if the PR added the key; **Should-fix** if pre-existing.43- `[stale] …` → key in target locale no longer in `en/` → **Should-fix**.44- `[missing-file] …` → whole file absent in target locale → **Should-fix**.4546Also scan added strings in PHP/Blade for hardcoded user-facing English that should be `__('lunarpanel::…')`.4748### Tests (Pest) — Should-fix4950For each newly added class under `packages/*/src/`:51- Models, services, actions, observers, jobs, and events should have a matching Pest test under `tests/{core,admin,opayo,paypal,shipping,stripe,search}/`.52- New `Eloquent` models additionally require a factory under `packages/*/database/factories/`.53- Use `php artisan make:test --pest <Name>Test` (do not write to `tests/Feature/` paths directly).5455### Migrations — Blocker5657For each migration in `packages/*/database/migrations/`:58- Must `extend Lunar\Base\Migration` (not `Illuminate\Database\Migrations\Migration` directly).59- Must use `$this->prefix` when referencing table names (`Schema::table($this->prefix.'orders', …)`).60- Must implement `down()` symmetrically — every `Schema::create` / column-add in `up()` is reversed.61- Foreign keys use `foreignId(...)->constrained()` with explicit `onDelete`/`onUpdate` where the parent model uses soft deletes.62- Destructive column drops on existing tables require a justification comment or a paired data migration.63- File name follows `YYYY_MM_DD_HHMMSS_<verb>_<thing>.php`.6465### Filament resources & admin — Should-fix6667For files under `packages/admin/src/Filament/Resources/`:68- `protected static ?string $model = …Contract::class;` — must reference an interface from `Lunar\Models\Contracts\*`, not a concrete model.69- Labels: `getLabel`, `getPluralLabel`, `getNavigationGroup`, form/column labels must come from `__('lunarpanel::…')`; flag any hardcoded English strings.70- New resources set `protected static ?string $permission`.71- Navigation icons resolved via `FilamentIcon::resolve('lunar::…')`.7273### Model contracts (type-hinting) — Blocker7475Eloquent models in `packages/*/src/Models/` are bound to interfaces in `Lunar\Models\Contracts\*` via the model manifest, so consumers can swap in their own subclass. Code that type-hints the concrete model defeats that extension point.7677Anywhere outside the model class itself and its factory/seeder, type hints, return types, property types, and PHPDoc references must use the contract, not the concrete model:7879- Method parameters & return types: `public function handle(CartContract $cart, Closure $next): CartContract`.80- Promoted/declared properties: `protected ?OrderContract $order = null;`.81- PHPDoc: `@param Closure(OrderContract): mixed $next`, `@return Collection<int, ProductContract>`.82- DI / container resolution: `app(CartContract::class)`, not `app(Cart::class)` or `new Cart`.8384Exemptions (concrete class is correct here):85- The model class itself, its relations, scopes, and casts.86- Factories, seeders, and migrations.87- Tests creating fixtures (`Product::factory()->create()`).88- `instanceof` checks against the concrete class only when there is a documented reason (otherwise prefer the contract).8990Flag as **Blocker** in new/changed code: `use Lunar\Models\Foo;` followed by a type hint `Foo $foo` in a service, action, manager, pipeline, observer, listener, event, or job. Suggest the fix as `use Lunar\Models\Contracts\Foo as FooContract;` and rename the hint to `FooContract`.9192### PHP conventions — Should-fix / Nit9394Per repo `CLAUDE.md`:95- Constructor property promotion required for public constructor params; flag empty zero-param `__construct()`.96- Explicit return types and typed parameters on every method.97- Curly braces around every control-structure body, even one-liners.98- Prefer PHPDoc blocks over inline comments; array shape types in PHPDoc.99- Never call `env()` outside `config/`.100- Enum case keys in `TitleCase`.101102### Static analysis & style — Nit (reminder)103104- If changed files fall inside paths excluded by `phpstan.neon.dist`, note that PHPStan will not catch issues there.105- Remind the user to run `vendor/bin/pint --dirty --format agent` before pushing.106107### Composer / dependencies — Blocker (requires user confirmation)108109Any diff in `composer.json` or any `packages/*/composer.json`:110- Flag added/removed/upgraded dependencies as a Blocker that needs explicit user sign-off per `CLAUDE.md`.111- Note PHP version, Laravel version, or Filament version bumps separately — they affect downstream consumers.112113### Channel scoping — Blocker114115Lunar is multi-channel. Models using `Lunar\Base\Traits\HasChannels` (`Product`, `Collection`, `Discount`, …) expose `scopeChannel()` for filtering.116117- New queries on these models in admin lists, storefront-facing endpoints, or scheduled jobs should call `->channel($channel)` (or be explicitly justified as cross-channel).118- New columns/relations on a channelled model that drive visibility need the channel scope wired in, not just the column added.119120### Reuse existing scopes & traits — Should-fix121122Before reviewing any new Eloquent query, check whether the related trait/model already exposes a scope for the same condition. Hand-rolled `->where(...)->orWhere(...)` chains that duplicate a trait scope are a Should-fix.123124- `Lunar\Base\Traits\HasCustomerGroups` exposes a scope for customer-group eligibility (enabled/visible/`starts_at`/`ends_at` windowing). New `->customerGroups()->where('enabled', true)…` chains in resources, widgets, validators, or pipelines should use that scope.125- `Lunar\Base\Traits\HasChannels` — see Channel scoping above; use `->channel(...)`, not `->channels()->where('enabled', true)…`.126- `Lunar\Base\Traits\HasUrls` — use the `default()` / active scope rather than re-checking columns inline.127- Status enums on `Product`, `Order`, etc. — prefer `->whereIn('status', SomeStatus::active())` or the existing helper over string-literal comparisons.128129When flagging, name the specific trait/scope the author should use, not just "use a scope". If the scope doesn't exist yet but the same chain is repeated 2+ times in the diff, suggest adding it to the trait.130131### Control-flow simplification — Nit (Should-fix when stacked)132133Lunar's Filament resources and validators have accumulated long ladders of `if ($x) { return true; } if ($y) { return true; }` inside closures (notably `Shout::make(...)->hidden(fn ...)`). Reviewers consistently push back on these — flag them.134135- Multiple sequential `if (cond) { return $literal; }` returning the same literal collapse into one `if (a || b || c) { return $literal; }` or a single boolean expression on the return.136- A trailing `if (! $x) { return true; } return (bool) $x;` is just `return ! $x;` (or the equivalent expression).137- Guard clauses are fine and preferred over nested `if/else`, but each one should branch to a *different* outcome — back-to-back guards returning the same value are noise.138- Closures that grow past ~5 statements doing query composition should be extracted to a named private method or, better, an Eloquent scope (see above).139140Treat a single instance as a Nit; flag as Should-fix when the same closure stacks 3+ early returns or when the pattern repeats across sibling components in the same file.141142### Money & price handling — Blocker143144Money in Lunar lives in `Lunar\DataTypes\Price` (integer minor units + `Currency`) and the `Lunar\Base\Casts\Price` cast.145146- Flag new monetary columns as `decimal`/`float` — they should be `unsignedInteger`/`bigInteger` and cast through `Price`.147- Flag float arithmetic on money: `* 0.01`, `(float)`, `round()` on raw totals, `+`/`-`/`*` between mixed-currency `Price` instances.148- New tax/discount/shipping totals must go through the existing pipeline calculation, not be set directly on the model.149150### Search indexing (Scout/Meilisearch) — Should-fix151152Search documents are built by `packages/core/src/Search/*Indexer.php` (`ProductIndexer`, `CollectionIndexer`, `CustomerIndexer`, `OrderIndexer`, `ProductOptionIndexer`).153154- Adding a queryable/filterable attribute to an indexed model? The corresponding `*Indexer` must include it in the document.155- Removing an attribute? Note that indexed documents are now stale and need a re-index step (`php artisan scout:import "<Model>"`).156- New filterable/sortable attributes likely need the Meilisearch `filterableAttributes` / `sortableAttributes` config updated.157158### Pipelines — Blocker on calculation paths159160Cart, cart line, cart prune, and order pipelines live under `packages/core/src/Pipelines/`. Lunar resolves them from config so consumers can extend or replace them.161162- Adding a new field to `CartLine`/`Cart`/`Order` that affects price, tax, shipping, or eligibility means adding (or extending) the relevant pipeline stage — not just persisting the field.163- New pipeline stages must be registered in `config/lunar/cart.php` / `config/lunar/orders.php` and have a Pest test.164- Reordering existing stages is a behavior change — flag loudly.165166### Event payload stability — Blocker167168Event classes under `packages/*/src/Events/` and `packages/*/src/Base/Events/` are public API on `1.x`. Listeners depend on the constructor signature and public properties.169170- Renaming/removing/retyping constructor params or public properties on an existing event is breaking.171- Adding new optional params at the end of a constructor is acceptable; new public properties are acceptable.172- New events that should be dispatched from observers/actions but aren't — flag as Should-fix.173174### Public API surface — Blocker175176`1.x` is a stable line. The following count as breaking changes and must be flagged loudly:177- Any change in `packages/*/src/Models/Contracts/` (added/removed/renamed methods, changed signatures).178- Public method signature changes on classes that implement a `Contracts/*` interface.179- Removed or renamed events under `packages/*/src/Base/Events/` or `packages/*/src/Events/`.180- Removed config keys in `packages/*/config/*.php`.181- Migrations that rename existing tables/columns without a backwards-compatible accessor.182183## Output Format184185```186# Lunar PR Review — <branch> vs <base>187188<N commits, M files changed>189190## Blockers (must fix before merge)191- `path/to/file.php:L42` — <issue> — <suggested fix>192- …193194## Should fix195- `path/to/file.php:L11` — <issue> — <suggested fix>196- …197198## Nits199- `path/to/file.php` — <issue>200- …201202## Run before merge203- `vendor/bin/pint --dirty --format agent`204- `php artisan test --compact`205- `vendor/bin/phpstan analyse`206```207208If there are no findings in a section, omit it. If there are zero findings overall, output a single line: `No findings vs <base> across <N> files.`209210## Do / Don't211212Do:213- Run the translations script — do not eyeball 16 locales.214- Quote line numbers from `git diff --unified=0` when available so the user can jump to them.215- Recommend `php artisan make:*` commands for any missing scaffolding rather than describing hand-rolled code.216217Don't:218- Don't edit any files.219- Don't push, force-push, rebase, or reset.220- Don't run `composer update`, `composer require`, `npm install`, or migrations.221- Don't claim a file is fine without actually reading the changed hunks.222- Don't duplicate findings — each `path:line` appears once at its highest severity.223224## References225226- `references/checklist.md` — full rubric with good/bad examples drawn from the codebase. Load when a category needs deeper explanation.227- `scripts/missing-translations.php` — run from repo root, exits 0, prints `[missing]`/`[stale]`/`[missing-file]` lines + summary.