Layering-rule deviation found in a sweep against the "route|action → service → DAL" rule (CLAUDE.md). Not a functional bug — behaviour is correct; this is maintainability/tech-debt. This is the documented economy-explorer deviation (the rule notes it explicitly), filed so it's tracked rather than just noted.
Problem: the app has no discrete service layer — Server Components and server actions call lib/sql/* directly — and lib/sql/* (which the rule says should be Kysely query functions + row types only) carries non-DAL logic. Concrete spots in economy-explorer/lib/sql/group.ts:
setGroupCapabilities (111–120): multi-step orchestration (DELETE all caps, then INSERT-loop) inside a transaction — write orchestration in the DAL.getGroup (71–74): calls listGroups() then filters in JS rather than querying by id — business logic / inefficiency in the DAL.resolvePlayerUuid (81–92): input validation + UUID-vs-name branching — logic that belongs above the DAL.Fix (incremental, not a rewrite): introduce a thin service module (e.g. lib/services/*) that owns orchestration/validation and calls a pure lib/sql/* DAL; have pages/actions call the service. Start with the group RBAC path above, then apply the pattern as other areas are touched. Keep new lib/sql/* functions to single queries + row types.
Discovered alongside the CLAUDE.md layering-rules addition; filing for a later pass.