`upsert_project_tx` defaulted a missing `[project].name` to the slug and then
`ON CONFLICT (slug) DO UPDATE SET name = EXCLUDED.name` wrote that fabricated
value unconditionally — so a re-apply from a clone whose manifest omits `name`
silently clobbered the stored display name back to the slug (visible in
`pic projects ls` / `pic groups ls`).
Bind the raw optional name once and guard both sides on it:
`VALUES ($1, COALESCE($2, $1), $3)` (first apply falls back to the slug for the
NOT NULL column) and `DO UPDATE SET name = COALESCE($2, projects.name)` (a
re-apply updates the name only when the manifest actually declares one). An
omitted optional field now preserves persisted data instead of mutating it.
Pinned by a new `reapply_without_name_preserves_project_name` journey.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
§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>
The apply_ownership journey gains an attach-ceiling case: a group node below
the attach point applies; the attach point itself and a sibling subtree are
both refused (422, message names the attach point). Design doc §7 + CLAUDE.md
record M2 shipped and re-point 'Next' at M3.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
End-to-end coverage of the M1 ownership claim through the real CLI + server:
claim-on-first-apply + owner column, idempotent re-apply by the owner, a
second project refused (409, naming the owner), --takeover by an admin,
app-inheritance from the nearest claimed ancestor (owning project ok; foreign
or absent project refused), and the capability gate — a member with only an
editor GROUP role can reconcile but is refused --takeover (needs group-admin),
with ownership unchanged after the failed takeover.
- tests/apply_ownership.rs (registered in cli.rs); a grant_group_membership
helper in tests/common/member.rs (group-level, mirroring the app one).
- design doc §7 gains an M1-shipped status note; CLAUDE.md records §7 M1 and
re-points 'Next' at M2 (attach ceiling) + M3 (blast-radius / pic projects ls).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>