feat(apply): §6 M2 — structural-divergence detection + declarative reparent

With the declared parent on the wire (M1), `apply_tree` now compares each
EXISTING group node's server parent to the manifest's (directory-nesting)
parent and resolves a divergence per a request `StructureMode` (default
`Refuse` — a pre-M2 CLI never reshapes the tree):

- `Refuse` → `ApplyError::StructuralDivergence` (422), naming the flags.
- `ForceLocal` → reparent to the declared parent IN the apply tx via the
  extracted `group_repo::reparent_group_tx` (cycle guard + structure_version
  bump), §5.6-gated (`GroupAdmin` on node + source + destination) and
  attach-ceilinged.
- `AdoptServer` → keep the server shape, reconcile content in place.

M1's `create_missing_groups_tx` generalized to `reconcile_group_structure_tx`
(create + reparent share the coarse-lock / attach-ceiling / RBAC path; both are
recorded `structurally_changed` so the pool-based attach re-check skips their
uncommitted rows). `pic plan` previews the drift (`divergence_preview` → a
`structure` row naming server + manifest parents). CLI:
`--force-local-structure` / `--adopt-server-structure` (mutually exclusive) →
the `structure_mode` wire field; the 422 surfaces verbatim. A concurrent
`pic groups reparent` between plan and apply still trips `StateMoved` (the tree
token already folds structure_version).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
MechaCat02
2026-07-07 20:23:10 +02:00
parent dcc387c345
commit cb3d458cfd
7 changed files with 402 additions and 148 deletions

View File

@@ -261,6 +261,14 @@ struct ApplyArgs {
/// top of the base manifest before applying.
#[arg(long)]
env: Option<String>,
/// §6 M2: reparent a group whose server parent diverges from the
/// manifest's directory nesting (default: refuse on divergence).
#[arg(long, conflicts_with = "adopt_server_structure")]
force_local_structure: bool,
/// §6 M2: accept the server's group placement on a structural
/// divergence and reconcile content in place (no reparent).
#[arg(long)]
adopt_server_structure: bool,
}
#[derive(Args)]
@@ -1433,12 +1441,20 @@ async fn main() -> ExitCode {
Cmd::Whoami => cmds::whoami::run(mode).await,
Cmd::Apply(args) => match &args.dir {
Some(dir) => {
let structure_mode = if args.force_local_structure {
"force-local"
} else if args.adopt_server_structure {
"adopt-server"
} else {
"refuse"
};
cmds::apply::run_tree(
dir,
args.prune,
args.yes,
args.force,
args.takeover,
structure_mode,
args.env.as_deref(),
mode,
)