--- id: ARTIFACT-STORE-WP-0003 type: workplan title: "Retention Lifecycle: Defaults, Extensions, Holds, Deletion Eligibility" repo: artifact-store domain: stack status: active owner: codex topic_slug: stack planning_priority: high planning_order: 3 created: "2026-05-15" updated: "2026-05-16" state_hub_workstream_id: "84930f4c-3bcf-415e-a94c-bfa854a15871" --- # ARTIFACT-STORE-WP-0003: Retention Lifecycle ## Purpose Implement the retention engine. By the end of this workplan, every package has a computed `expires_at`, operators can extend retention or apply / release holds, and the system can mark expired packages as eligible for deletion — without actually deleting bytes (GC is WP-0006). ## Constraints - ADR-0002 (every retention change is an event). - `docs/ARCHITECTURE-BLUEPRINT.md` retention sections. ## Prerequisites - WP-0001 done (`retention_classes` seeded, `retention_state` view exists). - WP-0002 done (HTTP surface exists to attach the new endpoints to). ## D3.1 - Default Retention Application ```task id: ARTIFACT-STORE-WP-0003-T001 status: cancelled priority: high state_hub_task_id: "2d6cbd83-c348-45ad-a223-7870a3412225" ``` Acceptance: - On `POST /packages`, the requested `retention_class` is validated and the `v1.retention.default_applied` event is written with the computed `expires_at`. - Default durations per class are operator-configurable via a config file (TOML); the file path is documented in `OPERATOR.md`. - `permanent-record` packages have `expires_at = NULL` and `eligible_for_deletion = false`. ## D3.2 - Retention Extensions ```task id: ARTIFACT-STORE-WP-0003-T002 status: in_progress priority: high state_hub_task_id: "66576e53-af4c-48dc-8dc3-cf8223a821c7" ``` Acceptance: - `POST /packages/{id}/retention/extensions` accepts `{new_expires_at, reason}`. The new value must be strictly later than the current; reason is mandatory. - Each extension writes a `v1.retention.extended` event; `retention_state.current_expires_at` updates on the same transaction. - A package's full extension history is recoverable from `events`. ## D3.3 - Holds (Apply And Release) ```task id: ARTIFACT-STORE-WP-0003-T003 status: in_progress priority: high state_hub_task_id: "8164e448-0e90-41aa-a973-77f8f607a0b3" ``` Acceptance: - `POST /packages/{id}/retention/holds` records a hold with a reason and actor; emits `v1.retention.hold_applied`. - A package with at least one active hold is never `eligible_for_deletion` regardless of `expires_at`. - `POST /packages/{id}/retention/holds/{hold_id}/release` requires a reason; emits `v1.retention.hold_released`. - Test: hold applied → expiry passes → eligibility stays `false`; hold released → eligibility flips to `true`. ## D3.4 - Deletion Eligibility Sweeper ```task id: ARTIFACT-STORE-WP-0003-T004 status: in_progress priority: medium state_hub_task_id: "fe13cd0d-aab7-4e0a-a7df-e6e535d4099b" ``` Acceptance: - A scheduled task (cron-style configurable interval; default 1 hour) scans packages whose `expires_at` has passed and no active hold exists, and emits `v1.retention.deletion_eligible` events. - The sweeper is idempotent: events are emitted at most once per package per eligibility transition. - The sweeper is invokable as a CLI subcommand for tests: `artifactstore retention sweep`. ## D3.5 - Audit Surface For Retention ```task id: ARTIFACT-STORE-WP-0003-T005 status: in_progress priority: medium state_hub_task_id: "7dce0c92-76d6-4bfc-bbc5-8e18b96139d2" ``` Acceptance: - `GET /packages/{id}/retention/history` returns the ordered list of retention events for a package. - The default response is the JCS projection; CBOR is available via `Accept: application/cbor`. ## Success criteria - A guide-board run can be ingested, given `release-evidence`, later extended once, held for a quarter, released, swept, and marked eligible — all visible through both `retention_state` and the event log. - No bytes are deleted by this workplan; that is WP-0006.