# Destination State Contract Audience: operators, integrators, and maintainers who inspect or reason about destination `.distributor.json` files. Each managed destination bundle path contains `.distributor.json`. This file is the destination sentinel and state record used for comparison, skip, replacement, and recovery decisions. ## Single-Owner State Schema Current state written by `run` uses schema version `2`. ```json { "schema_version": 2, "distributor_version": "dev", "pipeline_id": "reports", "destination_id": "archive", "published_at": "2026-06-04T12:00:00Z", "created_at": "2026-06-04T12:00:00Z", "updated_at": "2026-06-04T12:00:00Z", "state": { "mode": "single_owner" }, "reconciliation": { "mode": "replace" }, "source": { "manifest": { "schema_version": 1, "id": "reports.example.2026-06-04", "digest": "sha256:...", "created": "2026-06-04T11:55:00Z", "files": [ {"path": "report.md", "sha256": "sha256:...", "size": 1234} ] } }, "links": { "primary_url": "https://reports.example.com/archive/report.html" }, "outputs": [ { "path": "report.html", "kind": "generated", "source_path": "report.md", "transform": "markdown_to_html", "url": "https://reports.example.com/archive/report.html", "sha256": "sha256:...", "size": 2345, "created_at": "2026-06-04T12:00:00Z", "updated_at": "2026-06-04T12:00:00Z" } ] } ``` Required fields: - `schema_version`: must be `2` for newly written state. - `pipeline_id`: configured pipeline id that wrote the state. - `destination_id`: configured destination id that wrote the state. - `published_at`: RFC3339 publication timestamp. - `created_at`: RFC3339 timestamp for when this destination state record was first created. - `updated_at`: RFC3339 timestamp for the latest state update. - `state.mode`: must be `single_owner`. - `reconciliation.mode`: `replace` or `merge`. - `source.manifest`: embedded source bundle manifest. - `outputs`: output records array; empty is allowed, but the field is required. Optional fields: - `distributor_version`: application version string when available. - `links.primary_url`: absolute HTTP or HTTPS URL selected by destination link policy. ## Output Records Each output record has: - `path`: destination-relative output path. - `kind`: `source` or `generated`. - `source_path`: source manifest path used for the output. - `transform`: required for `generated` outputs; omitted for copied source outputs. - `url`: optional absolute HTTP or HTTPS URL for the output. - `sha256`: lowercase `sha256:<64 hex>` digest of the output bytes. - `size`: output byte size, zero or greater. - `created_at`: RFC3339 timestamp for when this output path was first recorded as managed. - `updated_at`: RFC3339 timestamp for when this output path was last written or updated in state. Output paths must be unique and use clean relative slash-separated path rules. For replacement updates, newly planned outputs are written into state. For merge updates, retained output records preserve both timestamps, overwritten managed output records preserve `created_at` and receive a new `updated_at`, and new output records receive the current publication time for both fields. ## Reconciliation Semantics Destination reconciliation applies when destination state is older than the source and transfer policy permits replacement. - `replace`: delete managed output paths recorded in `outputs` plus `.distributor.json`, require the destination bundle path to be empty afterward, write the newly planned outputs, and write state whose `outputs` are exactly that new planned set. - `merge`: retain managed output paths omitted from the new plan, overwrite planned paths only when they are already recorded in existing state, fail when a newly planned path exists in storage but is not recorded as managed, and write state whose `outputs` are the cumulative managed set. Top-level `links.primary_url` is selected from the newly planned outputs for the current publication. Retained outputs keep their existing per-output URL metadata. If a merge publication fails after writing outputs, cleanup removes only newly created outputs from that failed attempt. Previously managed overwritten paths remain managed and are not removed by failed-attempt cleanup. ## Comparison Semantics `distributor` compares the current source manifest to destination state before writing: - No state and no content: publish new outputs. - No state and existing content: treat the destination as unmanaged. - Matching embedded source manifest: skip. - Same source id with older `created`: replace if policy allows. - Same source id with newer `created`: skip by default. - Same source id and same `created` with different digest: conflict. - Different source id, pipeline id, or destination id: conflict. - Invalid state JSON or invalid state fields: conflict. Normal replacement deletes only managed output paths recorded in `outputs` plus `.distributor.json`. Merge publication retains omitted managed outputs. Forced replacement deletes the bounded destination bundle path. ## Compatibility `distributor` can read schema version `1` destination state for compatibility. When v1 state is read, it is treated as single-owner state with `reconciliation.mode: replace`. Missing top-level `created_at` and `updated_at` are inferred from `published_at`, and missing per-output timestamps are also inferred from `published_at`. Newly written destination state from publish execution uses schema version `2`. ## Shared-Root State Schema `distributor` can parse and validate shared-root destination state with schema version `3`. Publish execution currently writes single-owner state. ```json { "schema_version": 3, "distributor_version": "dev", "created_at": "2026-06-04T12:00:00Z", "updated_at": "2026-06-04T12:10:00Z", "state": { "mode": "shared_root" }, "owners": [ { "pipeline_id": "reports", "destination_id": "archive", "reconciliation": { "mode": "merge" }, "source": { "manifest": { "schema_version": 1, "id": "reports.example.2026-06-04", "digest": "sha256:...", "created": "2026-06-04T11:55:00Z", "files": [ {"path": "report.md", "sha256": "sha256:...", "size": 1234} ] } }, "links": { "primary_url": "https://reports.example.com/archive/report.html" } } ], "outputs": [ { "path": "report.html", "kind": "generated", "source_path": "report.md", "transform": "markdown_to_html", "url": "https://reports.example.com/archive/report.html", "sha256": "sha256:...", "size": 2345, "pipeline_id": "reports", "destination_id": "archive", "source_id": "reports.example.2026-06-04", "source_digest": "sha256:...", "source_created": "2026-06-04T11:55:00Z", "created_at": "2026-06-04T12:00:00Z", "updated_at": "2026-06-04T12:10:00Z" } ] } ``` Shared-root required fields: - `schema_version`: must be `3`. - `created_at`: RFC3339 timestamp for when this shared-root state record was first created. - `updated_at`: RFC3339 timestamp for the latest shared-root state update. - `state.mode`: must be `shared_root`. - `owners`: owner records keyed by `pipeline_id` and `destination_id`; each owner records its latest source manifest and reconciliation mode. - `outputs`: output records for every managed path under the shared destination root. Shared-root optional fields: - `distributor_version`: application version string when available. - `owners[].links.primary_url`: absolute HTTP or HTTPS URL selected by that owner's destination link policy. Shared-root output records carry the same `path`, `kind`, `source_path`, `transform`, `url`, `sha256`, `size`, `created_at`, and `updated_at` fields as single-owner outputs. They also include the owner `pipeline_id` and `destination_id`, plus compact source identity fields `source_id`, `source_digest`, and `source_created`. ## Boundaries Destination state is internal managed state written by `distributor`. Operators may inspect it during recovery, but normal workflows should not edit it by hand. Source `manifest.json` is not copied as destination state. ## Tests Before changing this contract, inspect and run: ```sh go test ./internal/state ./internal/publish ```