Files
distributor/docs/roadmap/implementation.md

400 lines
16 KiB
Markdown

# Catalog Follow-Up Implementation Roadmap
This is the active staged plan for closing the remaining catalog-state
implementation gaps found after the schema version `4` catalog workflow landed.
It supersedes the earlier catalog implementation plan.
The current code already supports destination `workflow: additive` and
`workflow: replacement`, writes catalog state schema version `4`, rejects legacy
destination policy YAML fields, and routes publish execution through catalog
planning for normal runs. The remaining work is to make the implemented behavior
release-ready: restore a meaningful catalog `--force` path, synchronize current
docs and examples, and remove legacy single-owner/shared-root implementation
debt.
## Current Baseline
- Destination config accepts `workflow: additive` and `workflow: replacement`.
- Omitted `workflow` defaults to `additive`.
- Destination YAML fields `state`, `reconciliation`, `takeover`, and `transfer`
are rejected by strict YAML decoding.
- Successful publish writes `.distributor.json` schema version `4` with
`state.mode: catalog`.
- Schema versions `1`, `2`, and `3` are treated as superseded legacy state for
publish planning.
- `run --force` remains in the CLI and docs, but catalog publish planning does
not currently select `force_replace`.
- Current docs and examples still describe legacy config/state behavior in
several places.
- Legacy single-owner and shared-root state types, tests, and execution helpers
remain in the codebase even though normal publish planning no longer uses
them.
## Implementation Principles
- Preserve the catalog state shape defined in `docs/roadmap/catalog.md`.
- Keep `workflow` as runtime config policy; do not persist it in
`.distributor.json`.
- Keep unmanaged content protected by default.
- Keep `--force` explicit, per-run, dry-runnable, and bounded to the configured
destination bundle path.
- Do not reintroduce legacy config aliases or compatibility migration.
- Keep backend adapters unaware of catalog, workflow, and force policy.
- Update current-behavior docs in the same stage that makes the described
behavior true.
## Active Implementation Stages
## Stage 1: Catalog Force Planning And Execution
Goal: make `run --force` meaningful for catalog workflows while keeping normal
catalog safety conservative.
Implementation scope:
- Update `internal/publish` planning so `req.Force` can select
`force_replace` for exceptional destructive catalog cases.
- Force should apply to these cases:
- no valid `.distributor.json` and destination bundle path has unmanaged
content;
- planned path collision with storage content not recorded in valid catalog
state;
- invalid destination state JSON or invalid destination state fields;
- unsupported future destination state schema, if the operator explicitly
chooses force replacement after dry-run review.
- Force should not be needed for ordinary valid catalog-managed upserts or
replacements. Additive and replacement workflow behavior remains normal managed
behavior.
- `force_replace` must delete only the bounded destination bundle path through
the storage abstraction, then write planned outputs and schema version `4`
catalog state.
- For fixed-path destinations, the bounded destination bundle path is the
configured backend root. Dry-run output must make that clear.
- Preserve default behavior without `--force`:
- unmanaged planned path collisions fail as `fail_unmanaged`;
- invalid or future state fails as `fail_conflict`;
- no-state non-empty destinations fail as unmanaged.
- Ensure `force_replace` state creation uses the same catalog output projection
as normal publish planning.
- Keep `Force` out of config. There must be no persistent force default.
- Remove or update skipped force tests that were disabled during catalog
implementation.
Current-behavior documentation updates:
- Update only the force-related sections of:
- `docs/cli.md`;
- `docs/operations.md`;
- `docs/troubleshooting.md`;
- `docs/internal/publish.md`;
- backend integration docs where they describe forced deletion boundaries.
- Document force as an exceptional catalog recovery/replacement workflow, not as
a normal way to handle valid managed catalog state.
Tests:
- `go test ./internal/publish`
- `go test ./internal/app ./internal/cli`
- `go test ./internal/adapters/local ./internal/storage ./internal/storage/fake`
- Planning tests:
- no-state non-empty destination returns `fail_unmanaged` without force and
`force_replace` with force;
- unmanaged planned path collision returns `fail_unmanaged` without force and
`force_replace` with force;
- invalid state returns `fail_conflict` without force and `force_replace` with
force;
- future schema returns `fail_conflict` without force and `force_replace` with
force;
- valid catalog additive and replacement actions do not become
`force_replace`.
- Execution tests:
- `force_replace` clears only the destination bundle path;
- sibling paths outside the bundle path survive for local, fake S3, and fake
SSH-style backends;
- fixed-path force clears the configured backend root and is reported as such;
- resulting state is schema version `4` catalog state.
- CLI/app tests:
- dry-run `--force` reports `force_replace` without writing;
- normal `--force` executes and increments only the `force_replace` counter;
- JSON output includes stable `force_replace` action and summary fields.
Completion criteria:
- `run --force` has observable, tested catalog behavior.
- Force is still unnecessary for normal managed catalog replacement/upsert.
- Unmanaged/invalid/future-state replacement remains impossible without explicit
`--force`.
## Stage 2: Current Documentation And Example Synchronization
Goal: make all current-behavior docs and copyable examples match catalog schema
version `4` and the `workflow` config model.
Implementation scope:
- Rewrite `docs/config.md` so destination behavior is described through:
- `workflow: additive`;
- `workflow: replacement`;
- `publish`;
- `transform`;
- `path_mapping`;
- `links`;
- `retention`.
- Remove current-behavior documentation for destination config fields:
- `state`;
- `reconciliation`;
- `takeover`;
- `transfer`.
- Rewrite `docs/integrations/destination-state.md` around schema version `4`
catalog state:
- top-level catalog fields;
- output record fields;
- source identity fields;
- generated-output-only `source_path` and `transform`;
- optional per-output `url`;
- superseded legacy schema handling for publish planning.
- Update `docs/operations.md`:
- additive workflow semantics;
- replacement workflow semantics;
- catalog `force_replace`;
- prune and reconcile-state catalog behavior;
- dry-run action labels.
- Update `docs/troubleshooting.md`:
- replace legacy conflict guidance with workflow/catalog guidance;
- describe rejected legacy config fields;
- describe force-only exceptional cases.
- Update `docs/cli.md`:
- run summary counters now use `publish_new`, `upsert_additive`,
`replace_catalog`, `skip_same`, `force_replace`, `fail_unmanaged`, and
`fail_conflict`;
- remove legacy action labels from current command reference.
- Update relevant internal docs:
- `docs/internal/app.md`;
- `docs/internal/config.md`;
- `docs/internal/publish.md`;
- `docs/internal/state.md`.
- Update policy docs where current architectural text still describes
single-owner/shared-root behavior as current:
- `docs/policy/architecture.md`;
- `docs/policy/development.md`.
- Update examples:
- remove or rewrite `examples/shared-root.yml`;
- remove or rewrite `examples/merge-reconciliation.yml`;
- ensure examples use `workflow` where workflow intent matters;
- keep examples valid, secret-free, and copyable.
- Do not document any future catalog selectors, state migration, or compatibility
aliases outside `docs/roadmap/`.
Tests and checks:
- `go test ./internal/config ./internal/cli`
- `go test ./...`
- Run the config/example checks already used by the test suite.
- Manual smoke checks:
- `go run ./cmd/distributor run --config examples/local-publish.yml --dry-run`
- `go run ./cmd/distributor run --config examples/archive-and-latest.yml --dry-run`
- smoke any rewritten replacement/additive examples.
- Consistency searches:
- `rg -n "state:|reconciliation:|takeover:|transfer:" examples docs --glob '!docs/roadmap/**'`
- `rg -n "single_owner|shared_root|schema version \`2\`|schema version \`3\`" docs --glob '!docs/roadmap/**'`
- `rg -n "replace_older|replace_newer|replace_conflict|replace_takeover|takeover_mode" docs README.md examples --glob '!docs/roadmap/**'`
- `rg -n "workflow: additive|workflow: replacement|schema_version.*4" docs examples`
Completion criteria:
- Current docs describe implemented catalog behavior only.
- Copyable examples load successfully.
- No current doc tells users to configure rejected legacy fields.
- Destination state documentation is schema version `4` first.
## Stage 3: Catalog Idempotent Skip
Goal: make `skip_same` a real catalog no-op optimization instead of a stale
legacy action label.
Implementation scope:
- Implement `skip_same` when every planned output is already catalog-managed
with matching:
- pipeline id;
- destination id;
- source id;
- source digest;
- source created timestamp;
- output path;
- output kind;
- output digest;
- output size;
- generated output `source_path`;
- generated output `transform`;
- output URL metadata.
- Do not read destination bytes for this optimization. Trust valid catalog
metadata; bitrot detection remains a separate future concern.
- For `workflow: additive`, allow `skip_same` when all planned outputs match and
no planned output needs to be written. Unrelated catalog outputs are ignored
for the skip decision and remain retained.
- For `workflow: replacement`, allow `skip_same` only when all planned outputs
match and there are no current-owner catalog outputs that replacement workflow
would delete.
- Never return `skip_same` for superseded legacy state, invalid state, unmanaged
content, future state, or force replacement.
- `skip_same` must not write outputs, rewrite state, delete files, or notify.
- Count `skip_same` in text and JSON summaries.
- Keep behavior deterministic and identical across local, SSH, and S3
destinations.
Current-behavior documentation updates:
- Update `docs/cli.md`, `docs/operations.md`, and `docs/internal/publish.md` to
describe catalog `skip_same`.
- Document that `skip_same` is metadata-based and does not validate destination
bytes.
Tests:
- `go test ./internal/publish ./internal/app ./internal/cli`
- Repeated additive publish with identical catalog metadata returns
`skip_same`.
- Repeated replacement publish with identical catalog metadata and no omitted
current-owner outputs returns `skip_same`.
- Changed source digest, output digest, URL, transform mode, owner, output size,
or generated source path prevents skip.
- Replacement workflow does not skip when it would delete omitted current-owner
outputs.
- `skip_same` does not write outputs or state and does not notify.
- `go test ./...`
Completion criteria:
- Catalog action vocabulary has no stale action label.
- Repeated publish behavior is intentional, documented, and tested.
## Stage 4: Legacy Publish And State Code Removal
Goal: remove dead or near-dead single-owner/shared-root write and planning code
after catalog force behavior, docs, and skip semantics are settled.
Implementation scope:
- Remove legacy publish actions that are no longer planned or documented:
- `replace_older`;
- `replace_newer`;
- `replace_conflict`;
- `replace_takeover`;
- `skip_destination_newer`.
- Keep only catalog-era actions:
- `publish_new`;
- `upsert_additive`;
- `replace_catalog`;
- `skip_same`;
- `force_replace`;
- `fail_unmanaged`;
- `fail_conflict`.
- Remove legacy fields from `publish.Plan`:
- `StateMode`;
- `Reconciliation`;
- `TakeoverMode`;
- `ExistingState`;
- `ExistingSharedRoot`;
- shared-root owner/output carry fields retained only for old execution.
- Remove single-owner and shared-root execution branches from
`internal/publish/execute.go`.
- Remove unused single-owner/shared-root helper functions from `internal/publish`
once no tests or callers use them.
- Remove legacy destination config internals from `internal/config`:
- `StatePolicy`;
- `ReconciliationPolicy`;
- `TakeoverPolicy`;
- `TransferPolicy`;
- legacy constants and defaults that are no longer referenced.
- In `internal/state`, keep only what is needed for:
- parsing schema version `4` catalog state;
- identifying schema versions `1`, `2`, and `3` as superseded legacy by
schema number;
- validating and writing catalog state;
- pruning and reconciling catalog outputs.
- Delete or rewrite tests that assert legacy state parsing, validation,
comparison, shared-root ownership, or single-owner output behavior.
- Preserve test fixtures only where they are used to create superseded legacy
state for first catalog-run behavior. Prefer small local helpers over keeping
broad legacy state builders.
- Remove stale skipped tests that only represent old behavior. If a skipped test
still represents current expected behavior, unskip and update it.
Current-behavior documentation updates:
- No new user docs should be needed if Stage 2 is complete.
- Update internal docs only if removal changes internal package contracts beyond
what Stage 2 already documented.
Tests:
- `go test ./internal/state`
- `go test ./internal/publish`
- `go test ./internal/app ./internal/cli`
- `go test ./internal/config`
- `go test ./...`
- Consistency searches:
- `rg -n "ActionReplaceOlder|ActionReplaceNewer|ActionReplaceConflict|ActionReplaceTakeover|ActionSkipDestinationNewer" internal`
- `rg -n "StateModeSingleOwner|StateModeSharedRoot|SharedRootState|DistributorState|ParseSharedRoot|ReconciliationPolicy|TakeoverPolicy|TransferPolicy" internal --glob '!**/*_test.go'`
- `rg -n "Retained while the executor is migrated to catalog state" internal`
Completion criteria:
- Production publish execution has one catalog code path plus explicit
`force_replace`.
- Legacy config policy types are gone from production config structs/defaults.
- State package no longer exposes full v2/v3 implementation machinery unless it
is required by tests that generate superseded legacy fixtures.
- Full test suite passes without skipped tests that mask catalog cleanup work.
## Stage 5: Roadmap Closeout
Goal: leave `docs/roadmap/` in a clean state after the follow-up work is
implemented.
Implementation scope:
- Remove or rewrite completed roadmap material:
- this `implementation.md`;
- `docs/roadmap/catalog.md`, if catalog behavior is fully documented in
current docs;
- any future roadmap entries that still describe completed catalog cleanup as
pending work.
- Keep only genuinely future work in `docs/roadmap/future.md` or another active
roadmap file.
- Ensure future work remains out of current behavior docs.
Tests and checks:
- `go test ./...`
- `git status --short`
- `rg -n "future catalog|planned catalog|single_owner|shared_root|takeover|reconciliation|transfer" docs README.md examples`
- `rg -n "workflow: additive|workflow: replacement|schema_version.*4" docs README.md examples`
Completion criteria:
- Current docs, examples, and code describe the same implemented behavior.
- Roadmap docs contain only future work.
- Full test suite passes.
## Refactors To Avoid
- Do not reintroduce `state`, `reconciliation`, `takeover`, or `transfer` YAML
fields as deprecated aliases.
- Do not migrate legacy destination state into catalog state.
- Do not add top-level `owners` or `sources` to catalog state.
- Do not persist `workflow` in `.distributor.json`.
- Do not add new prune/reconcile selectors as part of this cleanup.
- Do not move catalog or force policy into storage adapters.
- Do not redesign the run JSON envelope while cleaning action labels.
## Open Questions
No open questions remain. This roadmap locks the follow-up decisions:
catalog `--force` will be implemented for exceptional destructive replacement,
current docs and examples will be synchronized to catalog schema version `4`,
catalog `skip_same` will be implemented as a metadata-only no-op optimization,
and legacy single-owner/shared-root implementation debt will be removed after
the catalog action vocabulary is complete.