Automated PR to merge `267-artifact-lifecycle-retention` into `platform-dev`. Created by Copilot. Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de> Reviewed-on: #323
11 KiB
Data Model: Governance Artifact Lifecycle & Retention v1
Scope
This slice adds one shared derived contract over existing governance artifacts. It does not introduce a generic artifact table, a new browsing console, or a new persistence layer. Any persisted lifecycle or retention markers that v1 truly requires must stay on the existing owning tables or aggregate roots for the concrete artifact families involved.
Artifact Family Matrix
| Artifact family | Owning record | Current immutable or historical anchors | Current lifecycle anchors | Current retention or access anchors | Current operator surfaces | First-slice adoption |
|---|---|---|---|---|---|---|
| Evidence snapshot | EvidenceSnapshot |
id, fingerprint, generated_at |
status, isCurrent(), latest vs older snapshot truth in the current artifact presenter |
expires_at; explicit expire mutation via EvidenceSnapshotService |
EvidenceSnapshotResource, linked review and pack detail |
Visible contract adoption on existing resource and linked surfaces |
| Review pack | ReviewPack |
id, fingerprint, previous_fingerprint, sha256, generated_at |
status, relation to tenant_review_id, current_export_review_pack_id through the review |
expires_at; signed download allowed only when ready, unexpired, and entitled |
ReviewPackResource, ReviewPackDownloadController, CustomerReviewWorkspace, tenant review detail |
Visible contract adoption on existing resource, download, and workspace-linked surfaces |
| Stored report | StoredReport |
id, report_type, fingerprint, previous_fingerprint, created_at |
Latest-per-tenant-and-type vs older fingerprint rows is the current or historical distinction | created_at plus tenantpilot.stored_reports.retention_days; pruning via PruneStoredReportsCommand |
No dedicated operator resource; evidence sources, widgets, support diagnostics, telemetry | Headless contract adoption only |
| Accepted-risk decision record | FindingExceptionDecision inside the FindingException aggregate |
id, decision_type, decided_at, append-only history, parent current_decision_id |
Parent exception status, current_validity_state, and current-vs-historical decision relationship |
expires_at, review_due_at, and current validity on the parent exception; no hold or delete contract today |
DecisionRegister, ViewFindingException, finding-governance projections |
Headless contract adoption on aggregate models and current findings-service seams only; current register and detail surfaces stay unchanged in this slice |
TenantReview remains a consumer and context surface for linked artifact truth in this slice. Its publication, archive, and successor states must not become retention proxies for the shared contract.
Shared Derived Contract
GovernanceArtifactReference
Derived object emitted by the shared lifecycle or truth path.
| Field | Description | Notes |
|---|---|---|
family |
One of evidence_snapshot, review_pack, stored_report, finding_exception_decision |
No synthetic family for a generic registry |
workspace_id |
Owning workspace scope | Required for all current families |
tenant_id |
Owning tenant scope | Required for all current in-scope artifact families |
record_type |
Current model or aggregate owner | Derived from the owning record, not a new registry enum |
record_id |
Existing primary key | Stable inside the current family |
display_reference |
Human-readable immutable reference shown on current surfaces | Built from family plus existing identifiers already owned by the record |
integrity_anchor |
Existing fingerprint, SHA, or append-only decision anchor when available | Nullable; never invent a synthetic hash when the family has no current one |
source_context |
Existing related artifact or aggregate context | Examples: evidence snapshot, current export pack, parent finding exception |
Validation rules:
workspace_idandtenant_idremain required scope anchors for every current in-scope family.- The shared contract may normalize display labels, but it must not replace the owning record's real ID or fingerprint.
- A direct-access expiry or hold state must not erase the artifact reference from historical views.
GovernanceArtifactLifecycleState
Derived state that explains whether the artifact is the current artifact in active circulation or a retained historical record.
| Value | Meaning | Repo-grounded examples |
|---|---|---|
current |
Current artifact intended for active operator or customer use | Active evidence snapshot, current export review pack, current decision on the exception aggregate |
historical |
Retained historical artifact that remains readable without implying a newer replacement path | Older stored reports by fingerprint, prior evidence snapshots still referenced by later reviews |
superseded |
Historical artifact explicitly replaced by a newer current artifact | Prior export review pack after a newer export becomes current, prior decision row after a newer decision becomes current |
Rules:
expired_direct_accessis not a lifecycle state in this slice. It belongs to retention.suspended_read_onlyis not a lifecycle state in this slice. It remains commercial lifecycle from Spec 251.- A family may map directly from existing repo truth to
historicalwithout needing a second new state value.
GovernanceArtifactRetentionState
Bounded retention state family used by the shared contract.
| Value | Meaning | Repo-grounded anchor or note |
|---|---|---|
retained |
Artifact remains retained and readable under normal entitlement rules | Default state for current evidence, packs, stored reports before prune, and historical decisions |
hold |
Artifact is retained under an explicit hold and cannot progress into deletion | New v1 mutation state only if current-table persistence can stay bounded |
deletion_requested |
Explicit reversible request to remove the artifact from normal circulation later | New v1 mutation state only if current-table persistence can stay bounded |
expired_direct_access |
Direct access has expired even though historical reference or audit truth may still remain | Current repo-real pattern for review packs and evidence snapshots with expires_at |
Rules:
holdwins overdeletion_requestedfor visible truth and progression rules.expired_direct_accessdoes not imply purge, provider deletion, or removal of the immutable reference.suspended_read_onlymust never be used as a retention-state proxy.- Stored reports may stay
retainedor become effectively unavailable through prune, but prune itself remains a separate retained-history follow-up boundary rather than a generic purge workflow in this spec.
GovernanceArtifactActionDecision
Derived action contract emitted alongside lifecycle and retention truth.
| Field | Description |
|---|---|
may_view |
Whether current entitlement and retention state still allow detail access |
may_download |
Whether current entitlement, retention state, and artifact state still allow direct download of an already-generated artifact |
may_generate_successor |
Whether a new successor artifact may be generated now |
may_mutate_lifecycle |
Whether hold, release-hold, or deletion-request actions may execute now |
blocked_reason |
Customer- or operator-readable reason for the blocked action |
audit_action |
Stable audit event family for the action or mutation |
Rules:
may_generate_successorremains separate frommay_download. The repo already proves that a workspace can block new review-pack starts while still allowing ready-pack downloads.- The shared contract should reuse existing policy and capability checks before adding any family-local lifecycle gating.
- If a family does not pass the bounded current-owner persistence gate in v1,
may_mutate_lifecycleremains false or blocked while the contract still exposes truthful retention semantics.
Likely Persistence Touch Points
| Current table or aggregate | Current fields already in repo | Likely v1 additions only if required | Guardrail |
|---|---|---|---|
evidence_snapshots |
status, expires_at, fingerprint, generated_at |
Family-local hold or deletion-request markers and actor or reason metadata | Do not add a generic artifact foreign key |
review_packs |
status, expires_at, fingerprint, previous_fingerprint, sha256, generated_at |
Family-local hold or deletion-request markers and actor or reason metadata | Keep download truth on the current record and controller path |
tenant_reviews |
status, published_at, archived_at, superseded_by_review_id, current_export_review_pack_id |
No new retention state; use only to point review surfaces at linked artifact truth | Review lifecycle remains review-owned context, not a shared artifact engine |
stored_reports |
report_type, fingerprint, previous_fingerprint, created_at, payload |
Prefer derived-only in v1; keep pruning command-owned | No new operator UI surface in this slice |
finding_exceptions plus finding_exception_decisions |
current_decision_id, parent status, current_validity_state, review_due_at, expires_at, append-only decision rows |
If hold or deletion-request semantics are needed, prefer aggregate-level state on finding_exceptions rather than mutation of append-only decision history |
Do not break append-only history |
Query and Precedence Rules
holdwins overdeletion_requestedfor user-visible truth and action blocking.expired_direct_accessblocks direct download or open actions only where the current family already usesexpires_ator equivalent access expiry.suspended_read_onlyblocks new generation or lifecycle mutation actions, but it does not change lifecycle or retention state by itself.ReviewPackDownloadControllerremains the canonical enforcement point for ready-pack direct download: entitlement, ready status, unexpired access, and file existence must all remain true.StoredReportcurrent-versus-historical selection should prefer latest report per tenant and report type, with older fingerprint rows remaining historical until pruned.FindingExceptionDecisionrows remain historically addressable even when the parent exception points to a newer current decision.
State Transitions In Scope
Existing repo-real transitions to preserve
- Evidence snapshot: current active snapshot to expired direct access through
EvidenceSnapshotService::expire(...) - Review pack: queued to generating to ready or failed, and ready to expired direct access
- Finding exception decision history: requested to approved or renewed, rejected, or revoked through append-only decision creation on the parent aggregate
- Stored report: retained-by-age to pruned-by-command through
PruneStoredReportsCommand
New bounded transitions this slice may add
retainedtoholdholdtoretainedretainedtodeletion_requesteddeletion_requestedtoretained
Guardrails:
- No transition in this slice may claim purge or irreversible deletion.
- No transition may depend on provider presence or workspace commercial state as its primary meaning.
- If a requested transition needs a cross-family executor, scheduler, or export-before-delete workflow, it belongs to a follow-up spec instead of this slice.
- If no current family can support these transitions without widening scope, v1 stops at read-only lifecycle truth plus existing download audit and does not add new mutation persistence.