Files
distributor/docs/roadmap/implementation.md

16 KiB

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.