134 lines
11 KiB
Markdown
134 lines
11 KiB
Markdown
# 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. |