fix(apply): bound the blast-radius subtree down-walk CTE

`group_blast_radius`'s `app_chain` up-walk is depth-capped (`ac.depth < 64`,
added by 76926de to stop a should-be-impossible `groups.parent_id` cycle from
spinning the recursive CTE forever), but the sibling `subtree` down-walk in the
same query was left unbounded. A `parent_id` cycle would loop `subtree`
indefinitely and hang the plan — the exact exposure 76926de set out to close,
left half-done on the descendant side.

Add a `depth` column + `WHERE s.depth < 64` to `subtree`, mirroring `app_chain`.
Defense-in-depth: the reparent cycle-guard already prevents `parent_id` cycles,
so this hardens against an unreachable state — but that is precisely the bar
76926de applied. Surfaced by a post-adoption audit of the §7/§6 ownership code.

Verified: 416 manager-core lib tests; 12 ownership/create/divergence/approval
journeys (incl. plan_previews_ownership_and_blast_radius, which drives this
query); clippy -D warnings + fmt clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
MechaCat02
2026-07-11 13:25:06 +02:00
parent 87292fc4e9
commit bbfdb1bf47

View File

@@ -2739,9 +2739,10 @@ impl ApplyService {
// `exclude` (the planning project) or by nothing are dropped.
let rows: Vec<(String, i64)> = sqlx::query_as(
"WITH RECURSIVE subtree AS (
SELECT id FROM groups WHERE id = $1
SELECT id, 0 AS depth FROM groups WHERE id = $1
UNION ALL
SELECT g.id FROM groups g JOIN subtree s ON g.parent_id = s.id
SELECT g.id, s.depth + 1 FROM groups g JOIN subtree s ON g.parent_id = s.id
WHERE s.depth < 64
),
app_chain AS (
SELECT a.id AS app_id, a.group_id AS gid, 0 AS depth