architecture-check
Mechanical audit of layer boundaries, bindings, and controller purity across modules
SKILL.md.claude/skills/architecture-check/SKILL.md
GitHubArchitecture Check
Mechanical sweep of modules/. Scope to $ARGUMENTS if a module is named, otherwise all.
Rules live in .claude/rules/architecture.md — this skill only runs the checks. For judgment calls (module placement, boundary design), delegate to the hexagonal-architect agent instead.
1. Domain purity
grep -rn "^use Modules\\\\.*\\\\Infrastructure" modules/*/Domain/ || echo "OK: no Infrastructure imports in Domain"
grep -rn "^use Illuminate\\\\\(Database\|Foundation\|Routing\|Console\|Queue\|Cache\|Auth\)" modules/*/Domain/ || echo "OK: no Laravel runtime in Domain"
grep -rn "^use Illuminate\\\\Support\\\\Facades" modules/*/Domain/ || echo "OK: no facades in Domain"
Infrastructure imports are a hard violation. Laravel runtime/facades are violations in new code; App\Models\* and Collection/Paginator are the accepted legacy trade.
2. Eloquent confined to repositories
grep -rnE '::(where|find|create|firstOrCreate|updateOrCreate|all)\(|DB::' \
modules/*/Application/ modules/*/Infrastructure/Http/ || echo "OK: no queries outside repositories"
Expect 3 known hits (HistoryService.php:48, ProfileController.php:21,25). Anything new is a violation.
3. Cross-module coupling
Every import of another module must be a Domain\...FacadeInterface:
grep -rn "^use Modules\\\\" modules/ \
| grep -v "Domain" \
| grep -vE '^modules/([A-Za-z]+)/.*use Modules\\\1\\'
4. Interface ↔ binding
Portable (no grep -P — BSD grep on macOS):
find modules -name "*Interface.php" -path "*/Domain/*" -exec basename {} .php \; | sort -u > /tmp/sats-ifaces
grep -rho "[A-Za-z]*Interface::class" modules/*/[A-Z]*ServiceProvider.php | sed 's/::class//' | sort -u > /tmp/sats-bound
comm -23 /tmp/sats-ifaces /tmp/sats-bound
Anything printed is a Domain interface with no $singletons entry — that fails at runtime, not in phpstan. Two kinds of false positive, check before reporting:
- DTO/value contracts implemented by data objects that are constructed directly, never resolved from the container — e.g.
BlockchainDataInterface. These are correctly unbound. - Dormant facades — an interface nobody injects yet. Harmless today, breaks the moment someone injects it.
Confirm which kind before flagging:
grep -rn "<Name>Interface" modules/ app/ --include="*.php" | grep -v "implements\|^.*Domain/"
Known dormant today: FaqFacadeInterface, NostrFacadeInterface, PaymentFacadeInterface — declared and implemented, neither bound nor injected. ChatFacadeInterface and OpenAIFacadeInterface show the correct wired pattern.
5. Provider registration
ls modules/*/[A-Z]*ServiceProvider.php | wc -l
grep -c "ServiceProvider::class" bootstrap/providers.php
Counts should match (RouteServiceProvider included). A missing entry means the module silently never boots.
6. Controller purity
Read each modules/*/Infrastructure/Http/Controller/*.php. A controller may only: accept a FormRequest → call an Action/Facade → return a response. Flag any query, domain conditional, or external HTTP call.
7. Gate
composer phpstan && composer test
Report
Per violation: file:line, the rule it breaks, the concrete fix. Separate new violations from the known debt listed in CLAUDE.md — only new ones block. Finish with pass/fail per module.