Add prune retention planning

This commit is contained in:
2026-06-08 19:26:48 +00:00
parent 2abd09bde3
commit c67ecf86a9
12 changed files with 628 additions and 7 deletions

View File

@@ -286,6 +286,7 @@ destinations:
- `links`: optional public URL metadata policy.
- `state`: optional destination state ownership policy.
- `reconciliation`: optional managed-output reconciliation policy.
- `retention`: optional managed-output retention policy.
- `transfer`: optional destination comparison action policy.
Destination ids must be unique within a pipeline.
@@ -448,6 +449,24 @@ destinations:
`links.primary_url` is selected from the newly planned outputs for the current run. Retained outputs keep their prior output metadata and timestamps.
## Retention Policy
```yaml
retention:
prune:
enabled: false
older_than: 168h
keep_latest: 3
```
- `retention.prune.enabled`: optional boolean. Default is `false`.
- `retention.prune.older_than`: optional duration. When pruning is enabled, outputs older than this duration are eligible in prune planning.
- `retention.prune.keep_latest`: optional non-negative integer. When pruning is enabled, this many newest managed outputs are preserved before age-based pruning is considered.
When `retention.prune.enabled` is `true`, at least one of `older_than` or `keep_latest` is required. `older_than` must be greater than zero, and `keep_latest` must be zero or greater.
Prune planning uses managed output `updated_at` timestamps from destination state. It is owner-scoped for shared-root state. The current implementation parses, validates, and plans pruning from this policy; it does not delete destination files.
## Transfer Policy
```yaml
@@ -504,6 +523,7 @@ Defaults are applied after YAML decoding and before validation:
- `links.primary: auto` when a `links` block is present and `primary` is omitted
- `state.mode: single_owner`
- `reconciliation.mode: replace`
- `retention.prune.enabled: false`
- `transfer.on_destination_same: skip`
- `transfer.on_destination_older: replace`
- `transfer.on_destination_newer: skip`

View File

@@ -4,13 +4,13 @@ Audience: developers and LLM coding agents changing `internal/app`.
## Purpose
`internal/app` owns top-level application use cases: run, single-pipeline run, staged-source run, validate, inspect, manifest creation, reconcile-state planning/repair, and HTTP upload serving. It coordinates config loading, secret resolution, backend construction, source discovery, destination selection, publish planning/execution, state repair reporting, notification handoff, output projection, and upload coordination.
`internal/app` owns top-level application use cases: run, single-pipeline run, staged-source run, validate, inspect, manifest creation, reconcile-state planning/repair, prune planning, and HTTP upload serving. It coordinates config loading, secret resolution, backend construction, source discovery, destination selection, publish planning/execution, state repair reporting, retention plan reporting, notification handoff, output projection, and upload coordination.
## Inputs And Outputs
Inputs include app option structs, contexts, config paths, pipeline ids, local source roots, dry-run/force flags, output format, stdout writers, HTTP requests, and optional notifier implementations.
Outputs include `RunReport`, `ReconcileStateReport`, validate/inspect/manifest results, CLI text/JSON projections, HTTP upload responses, upload status records, and errors. Destination-scoped failures can return a partial run report plus an aggregated error; fatal setup failures return before a complete report exists.
Outputs include `RunReport`, `ReconcileStateReport`, `PrunePlanReport`, validate/inspect/manifest results, CLI text/JSON projections, HTTP upload responses, upload status records, and errors. Destination-scoped failures can return a partial run report plus an aggregated error; fatal setup failures return before a complete report exists.
## Boundaries
@@ -20,7 +20,7 @@ User-facing command parsing stays in `internal/cli`, including `reconcile-state`
## Config Fields Used
The package consumes the loaded `config.Config`: `server.http`, `secrets.directory`, pipeline ids, source and destination backend fields, validation policy, publish policy, transform policy, path mapping, links, reconciliation policy, and transfer policy.
The package consumes the loaded `config.Config`: `server.http`, `secrets.directory`, pipeline ids, source and destination backend fields, validation policy, publish policy, transform policy, path mapping, links, state policy, reconciliation policy, retention policy, and transfer policy.
Config fields are validated and defaulted by `internal/config` before app workflows use them.
@@ -36,6 +36,8 @@ Run workflows discover and validate source bundles through `internal/bundle`. De
Reconcile-state workflows load one configured pipeline/destination selector, open that destination root, parse the root `.distributor.json`, and report missing managed output records plus unmanaged storage entries. Managed output existence checks use storage `Stat`; unmanaged reporting uses bounded storage `Walk` and excludes `.distributor.json` plus all paths already recorded as managed. Apply mode removes missing managed output records from state and rewrites valid state only; dry-run reports the same repair without writing. Text output reports `changed`, `would_change`, or `unchanged`; JSON output uses the shared app envelope. It does not validate output digests, delete destination files, adopt unmanaged files, or rewrite invalid or mismatched state.
Prune planning consumes a parsed destination state document and a validated retention prune policy, then returns owner-scoped managed output records that would be pruned or preserved. Planning uses output `updated_at` timestamps, applies `keep_latest` before `older_than` when both are configured, and does not open storage, delete files, or rewrite state.
HTTP uploads stage and validate archives before enqueueing a pipeline run with a local staged source root. Go producers can use the public `pkg/upload` package to create client-side gzip tar uploads for this server contract; `internal/app` remains the server-side orchestration boundary and does not import that producer package.
Upload idempotency is owned by the upload coordinator. Optional `Idempotency-Key` values are scoped to token id, pipeline id, and key. The coordinator reserves a key while staging is in progress, records the accepted run id with the validated source manifest identity after staging succeeds, returns the original accepted record for the same scoped key and same manifest, and rejects the same scoped key with a different manifest as a conflict.
@@ -57,6 +59,7 @@ HTTP upload startup fails if upload tokens are missing, empty, or duplicated. Up
## Tests To Inspect
- `internal/app/*_test.go`
- `internal/app/prune_test.go`
- `internal/cli/reconcile_state_test.go`
- `internal/cli/root_test.go`
- `internal/config/*_test.go`
@@ -73,3 +76,4 @@ HTTP upload startup fails if upload tokens are missing, empty, or duplicated. Up
- Idempotent upload retries compare normalized source manifest identity, not archive bytes.
- Runtime backend registration remains app-owned.
- Reconcile-state repairs state records only; it never deletes or adopts destination files.
- Prune planning is report-only until execution code applies a plan.

View File

@@ -18,7 +18,7 @@ The canonical user-facing config reference is `docs/config.md`.
## Config Fields Used
The package defines all user-visible config fields: `server.http`, `secrets`, `pipelines`, source and destination backend fields, validation policy, publish policy, transform policy, path mapping, links, and transfer policy.
The package defines all user-visible config fields: `server.http`, `secrets`, `pipelines`, source and destination backend fields, validation policy, publish policy, transform policy, path mapping, links, state policy, reconciliation policy, retention policy, and transfer policy.
## Adapters Used
@@ -26,7 +26,7 @@ No external storage adapters are used directly. The package exposes normalized c
## State And Manifest Behavior
The package does not parse source manifests or destination state. It validates config values that later affect manifest validation and destination state, such as publish/transform combinations, links, transfer policy, backend roots, S3 prefix shape, and HTTP upload source settings.
The package does not parse source manifests or destination state. It validates config values that later affect manifest validation and destination state, such as publish/transform combinations, links, state policy, reconciliation policy, retention policy, transfer policy, backend roots, S3 prefix shape, and HTTP upload source settings.
## Skip And Resume Behavior

View File

@@ -36,9 +36,9 @@ Shared-root publish conversion is explicit. Compatible single-owner state for th
Embedded source manifests are parsed and validated through `internal/bundle`, which delegates source manifest semantics to `pkg/bundle`. Output records require clean paths, `source` or `generated` kind, valid source paths, lowercase SHA-256 digests, non-negative sizes, created and updated timestamps, and transform ids for generated outputs. Stored URLs must pass `internal/link` validation.
The package also provides helpers for finding output records by path, projecting planned publish outputs into timestamped state outputs, merging retained and newly planned output records, computing managed output paths from single-owner state, and removing missing managed output records from single-owner state.
The package also provides helpers for finding output records by path, projecting planned publish outputs into timestamped state outputs, merging retained and newly planned output records, computing managed output paths from single-owner state, removing missing managed output records from single-owner state, and building prune candidates from managed outputs.
For shared-root state, helpers parse either state shape, identify the current owner scope, return an owner's latest source manifest, list managed paths for one owner or all owners, detect path ownership conflicts, project planned owner outputs, merge one owner's planned outputs while preserving unrelated owners, replace one owner's outputs by removing that owner's omitted outputs, and remove missing managed output records for either the current owner or every owner.
For shared-root state, helpers parse either state shape, identify the current owner scope, return an owner's latest source manifest, list managed paths for one owner or all owners, detect path ownership conflicts, project planned owner outputs, merge one owner's planned outputs while preserving unrelated owners, replace one owner's outputs by removing that owner's omitted outputs, remove missing managed output records for either the current owner or every owner, and build owner-scoped prune candidates.
Shared-root helper projections preserve output `created_at` for existing managed paths and use the current publication time for rewritten `updated_at`. Root-level `created_at` preservation is owned by publish execution.
@@ -50,6 +50,8 @@ Shared-root comparison is owner-scoped. It compares only the owner keyed by the
Missing-output removal helpers are pure state transformations used by app-level state repair. They remove matching output records only and leave storage inspection, timestamp updates, validation, and state rewrites to callers.
Prune planning helpers are pure. They select managed output candidates, sort deterministically by `updated_at` and path, preserve the newest `keep_latest` candidates before evaluating `older_than`, and return planned prune/preserve lists without mutating state.
## Failure Behavior
Parsing rejects invalid JSON, trailing data, missing required fields, invalid timestamps, invalid state mode, invalid reconciliation mode, invalid embedded manifests, duplicate owners, duplicate outputs, invalid output paths, unsupported output kinds, missing generated transforms, invalid URLs, invalid digests, negative sizes, and shared-root outputs whose owner is not registered.
@@ -58,6 +60,7 @@ Parsing rejects invalid JSON, trailing data, missing required fields, invalid ti
- `internal/state/distributor_test.go`
- `internal/state/shared_root_test.go`
- `internal/state/prune_test.go`
- `internal/state/compare_test.go`
- `internal/app/reconcile_state_test.go`
- `internal/cli/reconcile_state_test.go`
@@ -73,6 +76,7 @@ Parsing rejects invalid JSON, trailing data, missing required fields, invalid ti
- Schema version `3` shared-root state is parsed and validated without converting unrelated single-owner state.
- Shared-root owner updates preserve unrelated owners and reject planned path collisions with other owners.
- Missing-output repair helpers preserve unrelated owner records and outputs.
- Prune planning uses output `updated_at` and preserves unrelated shared-root owners.
- Generated outputs always record a transform id.
- Output records always carry created and updated timestamps after parsing.
- Stored URLs are optional and must be absolute HTTP or HTTPS URLs when present.