requireOwnedProjectId() was copy-pasted identically in CardController
and CardStatusController; ProjectController's own
requireOwnedProject() was the same lookup, just returning the full
row instead of the id. New abstract ProjectScopedController (extends
Controller) holds one copy of both, and all three controllers now
extend it instead of Controller directly, forwarding their
ProjectRepository to its constructor.
Separately, CardController::reorder() (card_ids) and
CardStatusController::reorder() (status_ids) each had the same inline
'must be an array of ids, no duplicates' check. Both now call a new
Validator::intIdArray(), which does the same shape check once.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>