# Managed Destination Takeover Implementation Roadmap This is the active staged implementation plan for `docs/roadmap/takeover.md`. The feature roadmap defines the target policy and end-state semantics; this document defines the implementation sequence for an LLM coding agent to follow stage by stage. Future behavior must remain under `docs/roadmap/` until each stage is implemented. Preparatory internal stages should not update user-facing current docs. Current-behavior docs should be updated when takeover behavior is wired for operator-facing use. ## Current Baseline `distributor` already supports local, SSH/SFTP, S3, and HTTP upload source workflows, destination state schemas for single-owner and shared-root state, path mapping, link generation, reconciliation, transfer policy, explicit `--force`, and text/JSON run output. Current destination comparison is strict: - same source manifest skips; - same source id with older destination state normally replaces; - same source id with newer destination state normally skips; - same source id and same creation time with different digest conflicts; - different source id, pipeline id, destination id, or shared-root output owner conflicts unless explicit force policy applies; - unmanaged content and invalid state do not become normal managed replacement cases. The target feature adds destination-level `takeover.mode`, defaulting to `same_pipeline`, so valid managed content can be replaced without `--force` when the configured policy says that ownership/source takeover is expected. ## Implementation Principles - Preserve public behavior until the stage that explicitly changes it. - Keep source manifests unchanged; takeover is destination configuration and publish planning policy. - Keep state comparison pure. State comparison may expose structured conflict details, but publish planning decides whether takeover is allowed. - Keep adapters thin. No local, SSH, or S3 adapter should know takeover policy. - Keep unmanaged content and invalid destination state outside normal takeover. - Keep `reconciliation.mode`, `transfer`, `state.mode`, `path_mapping.mode`, and `--force` as separate concepts. - Prefer narrow behavior-preserving refactors over broad publication rewrites. - Update implemented-behavior docs in the same stage as the behavior change. ## Active Implementation Stages ## Stage 1: Config Model And Validation Goal: add the destination config field, defaults, and validation without changing publish behavior. Source roadmap reference: - `docs/roadmap/takeover.md`: Configuration, Policy Semantics, Safety Rules. Implementation scope: - Add destination-level `TakeoverPolicy` to `internal/config`. - Add constants for: - `same_pipeline` - `same_source` - `any_managed` - `never` - Default `takeover.mode` to `same_pipeline` in config defaults. - Validate accepted values with clear field context such as `pipelines[0].destinations[0].takeover.mode`. - Preserve strict YAML unknown-field behavior. - Thread the defaulted policy into existing destination config views or helper structures if those are used by app/publish request construction. - Do not change publish planning, execution, CLI output, or docs outside roadmap files in this stage. Tests: - `go test ./internal/config` - Add config tests for omitted `takeover`, each accepted mode, invalid mode, and unknown nested fields. - Add or update example-loading tests only if examples are touched, which should not be necessary in this stage. Completion criteria: - Every destination has a defaulted `takeover.mode`. - Invalid values fail during config validation. - No run behavior changes because publish planning does not consume the policy yet. ## Stage 2: Structured State Comparison Details Goal: expose enough structured comparison detail for publish planning to decide takeover eligibility without parsing human-readable reason strings. Source roadmap reference: - `docs/roadmap/takeover.md`: Policy Semantics, Relationship To Existing Policies. Implementation scope: - Extend `internal/state.Comparison` or add a package-local structured detail type so callers can distinguish: - pipeline id mismatch; - destination id mismatch; - different source id; - same-created digest conflict; - destination newer; - invalid state; - unmanaged content; - absent shared-root owner; - shared-root managed output owner conflicts, if those are currently reported outside `internal/state`. - Keep existing outcome names and reason strings stable where practical. - Keep comparison functions pure. They should report facts about existing state, not consult `takeover.mode`, `transfer`, `reconciliation`, or `force`. - Do not add new persisted state fields. - Do not change publish actions in this stage. Tests: - `go test ./internal/state ./internal/publish` - Add state tests for structured details on identity mismatch and different source id. - Add shared-root tests for owner absence and output ownership conflict detail, either in `internal/state` or `internal/publish` depending on where the conflict is currently detected. - Preserve existing comparison outcome tests. Completion criteria: - Publish planning can make takeover decisions from structured data. - Existing behavior remains unchanged because no takeover mapping is applied yet. ## Stage 3: Single-Owner Takeover Planning And Execution Goal: implement `takeover.mode` for single-owner destination state. Source roadmap reference: - `docs/roadmap/takeover.md`: Policy Semantics, Publish Planning, Destination State Results, Safety Rules. Implementation scope: - Add `TakeoverPolicy` or equivalent values to `internal/publish.Request`. - Pass destination takeover policy from app-level run planning into publish. - Add publish action `replace_takeover`. - Map eligible single-owner conflicts to `replace_takeover`: - `same_pipeline`: existing state pipeline id equals current pipeline id, regardless of destination id or source id; - `same_source`: existing state source id equals current source id; - `any_managed`: existing state is valid distributor-managed state; - `never`: no identity/source takeover. - Keep these cases failing by default unless existing force behavior applies: - unmanaged content; - invalid state; - same-created digest conflict; - destination newer for the same source id unless `transfer` plus `--force` already permits replacement; - conflicts not allowed by `takeover.mode`. - Execute `replace_takeover` through bounded managed replacement mechanics. - For cross-source takeover, do not retain omitted outputs through `reconciliation.mode: merge`; treat the affected single-owner state as a managed replacement so old-source outputs are not attributed to the new source. - Preserve existing `replace_older`, `skip_same`, `skip_destination_newer`, `fail_conflict`, `fail_unmanaged`, and `force_replace` behavior. - Keep adapters unchanged. Current-behavior documentation updates: - Do not update user docs yet unless CLI output changes in this stage. Prefer deferring user docs to Stage 5 so the behavior, output, and docs land together. - If this stage changes visible dry-run or run output enough that tests require new wording, document only the implemented single-owner behavior and clearly leave shared-root takeover out until Stage 4. Tests: - `go test ./internal/publish ./internal/app` - Default `same_pipeline` replaces a different source id from the same pipeline. - Default `same_pipeline` replaces state with a different destination id under the same pipeline. - Default `same_pipeline` refuses a different pipeline. - `same_source` allows same source id and refuses different source id. - `any_managed` replaces valid state from a different pipeline. - `never` refuses identity/source takeover. - Invalid state and unmanaged content still fail without force. - Cross-source takeover with `reconciliation.mode: merge` does not retain omitted outputs from the previous source. - Dry-run plans `replace_takeover` without writing. - Existing force tests still pass. Completion criteria: - Single-owner latest-style destinations can be updated by different bundle ids from the same configured pipeline without `--force`. - No unmanaged or invalid-state path becomes a normal takeover path. ## Stage 4: Shared-Root Takeover Planning And Execution Goal: apply the same takeover vocabulary to shared-root owner and output-path conflicts. Source roadmap reference: - `docs/roadmap/takeover.md`: Policy Semantics, Publish Planning, Destination State Results, Safety Rules. Implementation scope: - Extend shared-root planning so planned output collisions with existing managed owners are eligible for takeover according to `takeover.mode`. - Apply policy as follows: - `same_pipeline`: current owner may take over output paths owned by another destination under the same pipeline; - `same_source`: current owner may take over output paths whose owner records the same source id as the current source; - `any_managed`: current owner may take over output paths owned by any valid shared-root owner; - `never`: preserve current owner conflict behavior. - Preserve unrelated owner records and non-conflicting output records. - Keep unmanaged path collisions failing without force. - Keep compatible single-owner-to-shared-root migration behavior intact. - For cross-source takeover, do not retain omitted outputs from the previous source under the taking-over owner when `reconciliation.mode: merge` is set. - Keep shared-root forced replacement behavior explicit and bounded as it is today. Current-behavior documentation updates: - Defer broad documentation updates to Stage 5 unless shared-root behavior must be documented immediately to keep tests or generated docs consistent. Tests: - `go test ./internal/state ./internal/publish ./internal/app` - `same_pipeline` permits taking over a path owned by another destination in the same pipeline. - `same_pipeline` refuses a path owned by another pipeline. - `same_source` permits only matching source-id ownership transfer. - `any_managed` permits cross-pipeline managed ownership transfer. - `never` preserves current shared-root conflict behavior. - Reconciliation `replace` and `merge` handle omitted outputs according to the feature roadmap, including the cross-source no-retain rule. - Shared-root migration from compatible single-owner state still works. - Unmanaged collisions still fail without force. Completion criteria: - Single-owner and shared-root destinations use one consistent takeover policy. - Shared-root takeover changes only affected owners/outputs and preserves unrelated managed state. ## Stage 5: Run Output, Summaries, And Documentation Goal: make takeover behavior visible to operators and document the implemented feature. Source roadmap reference: - `docs/roadmap/takeover.md`: Documentation Impact, Publish Planning, Relationship To Existing Policies. Implementation scope: - Add `replace_takeover` to text run output. - Add `replace_takeover` to JSON run action output. - Add a distinct summary counter for takeover replacements in text and JSON run summaries, using the field name `replace_takeover` for JSON. - Include takeover mode and conflict reason in action projection where useful and consistent with existing output style. - Update fixed-path dry-run warnings when takeover will replace the destination root. - Update current-behavior docs: - `docs/config.md` - `docs/operations.md` - `docs/troubleshooting.md` - `docs/integrations/destination-state.md` - `docs/internal/publish.md` - `docs/internal/state.md` - `docs/internal/config.md` - `docs/policy/architecture.md` - `docs/policy/development.md` - Keep docs concise and link to canonical references rather than duplicating full state semantics in every file. - Do not document any unimplemented future takeover extensions outside `docs/roadmap/`. Tests: - `go test ./internal/app ./internal/cli ./internal/config` - Text dry-run output includes `replace_takeover`. - JSON output records `replace_takeover` with stable field names. - Summary counters are deterministic. - Fixed-path warnings remain deterministic. - Config docs examples, if changed or added, load through existing config tests. - Troubleshooting text matches actual error/action wording. Completion criteria: - Operators can distinguish ordinary same-source replacement, takeover replacement, and explicit forced replacement. - Current docs describe implemented takeover behavior outside roadmap files. ## Stage 6: Cross-Backend Regression And Roadmap Closeout Goal: prove takeover behavior is backend-agnostic and clean up roadmap state after implementation. Source roadmap reference: - `docs/roadmap/takeover.md`: Goals, Non-Goals, Safety Rules. Implementation scope: - Add app-level coverage showing takeover planning/execution works with storage abstraction rather than adapter-specific logic. - Cover local destinations directly. - Cover SSH and S3 through fake-backed app tests where possible; add adapter integration tests only if existing test infrastructure already supports opt-in remote credentials. - Add tests for `path_mapping.mode: fixed` with different source ids under the same pipeline. - Add tests for archive-style `preserve_relative` destinations using `takeover.mode: same_source` where strict source identity is desired. - Run final consistency searches. - Once behavior and docs are complete, either remove `docs/roadmap/takeover.md` or reduce it to future-only material according to the documentation policy. If no future takeover work remains, remove the roadmap file. Tests: - `go test ./internal/config` - `go test ./internal/state` - `go test ./internal/publish` - `go test ./internal/app ./internal/cli` - `go test ./...` Documentation and consistency checks: ```sh rg -n "takeover|replace_takeover|same_pipeline|same_source|any_managed" docs README.md examples rg -n "destination source id differs from source|pipeline id .* does not match|destination id .* does not match" docs rg -n "Managed Destination Takeover Roadmap|future takeover|planned takeover" docs README.md examples --glob '!docs/roadmap/**' ``` Completion criteria: - Full test suite passes. - The default `same_pipeline` behavior applies only to valid distributor-managed takeover cases. - Unmanaged content and invalid state remain protected. - Current docs no longer describe implemented takeover behavior as future work. ## Refactors To Avoid - Do not build a generic ownership engine outside `internal/state` and `internal/publish`. - Do not move destination comparison policy into storage adapters. - Do not change the source manifest schema. - Do not add takeover fields to HTTP upload requests or producer APIs. - Do not make `--force` persistent config. - Do not merge `takeover`, `transfer`, and `reconciliation` into one broad policy object. - Do not silently adopt unmanaged content. ## Open Questions No open questions are known. The feature roadmap selects the default mode (`same_pipeline`), the accepted modes, the safety boundary, and the relationship to existing state, reconciliation, transfer, and force policies.