feat(apply): cross-repo blast-radius in the plan preview

§7 M3 (part 2): planning a change to a GROUP node now reports the descendant
apps owned by OTHER projects that the change fans out to — the cross-repo
signal an operator needs before touching shared config.

- OwnershipPreview gains `blast_radius: Vec<ProjectImpact>`; `group_blast_radius`
  walks the group's subtree (recursive CTE) and groups descendant apps by their
  nearest-claimed-ancestor project, excluding the planning project. Ancestor
  resolution is memoized per group (one walk per distinct descendant group, not
  per app).
- `pic plan` renders `blast_radius` rows (mode-agnostic).
- the apply_ownership journey gains a plan-preview case pinning both the
  ownership annotation (claim / conflict) and the blast radius (two other
  projects' apps surfaced) end to end.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
MechaCat02
2026-07-06 21:17:15 +02:00
parent b4eb226d20
commit 30fe160f16
4 changed files with 209 additions and 7 deletions

View File

@@ -74,6 +74,15 @@ fn render_tree(plan: &TreePlanDto, mode: OutputMode) {
o.owner.clone().unwrap_or_default(),
String::new(),
]);
for b in &o.blast_radius {
table.row([
node.clone(),
"blast_radius".to_string(),
b.project.clone(),
format!("{} app(s)", b.apps),
String::new(),
]);
}
}
let groups: [(&str, &Vec<ChangeDto>); 8] = [
("script", &n.scripts),
@@ -203,6 +212,14 @@ fn render(plan: &PlanDto, mode: OutputMode) {
o.owner.clone().unwrap_or_default(),
String::new(),
]);
for b in &o.blast_radius {
table.row([
"blast_radius".to_string(),
b.project.clone(),
format!("{} app(s)", b.apps),
String::new(),
]);
}
}
let groups: [(&str, &Vec<ChangeDto>); 8] = [
("script", &plan.scripts),