TenantAtlas/specs/267-artifact-lifecycle-retention/data-model.md
ahmido 6bf8e7f76b feat: 267-artifact-lifecycle-retention → platform-dev (#323)
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
2026-05-03 20:30:51 +00:00

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_id and tenant_id remain 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_access is not a lifecycle state in this slice. It belongs to retention.
  • suspended_read_only is not a lifecycle state in this slice. It remains commercial lifecycle from Spec 251.
  • A family may map directly from existing repo truth to historical without 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:

  • hold wins over deletion_requested for visible truth and progression rules.
  • expired_direct_access does not imply purge, provider deletion, or removal of the immutable reference.
  • suspended_read_only must never be used as a retention-state proxy.
  • Stored reports may stay retained or 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_successor remains separate from may_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_lifecycle remains 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

  • hold wins over deletion_requested for user-visible truth and action blocking.
  • expired_direct_access blocks direct download or open actions only where the current family already uses expires_at or equivalent access expiry.
  • suspended_read_only blocks new generation or lifecycle mutation actions, but it does not change lifecycle or retention state by itself.
  • ReviewPackDownloadController remains the canonical enforcement point for ready-pack direct download: entitlement, ready status, unexpired access, and file existence must all remain true.
  • StoredReport current-versus-historical selection should prefer latest report per tenant and report type, with older fingerprint rows remaining historical until pruned.
  • FindingExceptionDecision rows 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

  • retained to hold
  • hold to retained
  • retained to deletion_requested
  • deletion_requested to retained

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.