Four cleanups from the code review of the §7 ownership track, no behavior
change to any passing test (416 manager-core units + 133 CLI journeys green):
- #3 app-ownership read parity: `check_app_owner_single` now resolves the
nearest-claimed ancestor WITHIN the apply tx (`nearest_claimed_in_tx`, the
same mechanism the tree path uses) instead of on a separate pool connection,
so the check and this apply's writes see one consistent snapshot. (Fully
serializing against a concurrent claim on an out-of-tree ancestor stays part
of the documented pool→tx follow-up.)
- #4 blast-radius N+1 → one query: `group_blast_radius` replaced the
per-descendant-group `ancestors()` round-trips with a single recursive
`subtree → app_chain → nearest` CTE that groups + counts server-side. Cost is
now one query regardless of subtree size.
- #5 one ownership policy, two projections: `decide_group_claim` /
`decide_app_owner` are generic over the identity key, and `preview_ownership`
(plan side) now DERIVES its label from them (keyed on slugs, takeover-agnostic)
rather than re-encoding the arms — plan and apply can no longer drift.
- #6 shared slug validator: `validate_project_slug` delegates to a new
`groups_api::validate_slug_format`, which `groups_api::validate_slug` also now
uses — the format rule lives once. Projects intentionally skip the
reserved-word check (a project slug is never routed).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>