Policy retention and record classes

Verified against source commit 8536b424f3de04af06c27c8528eef6ce6291349d for the PGF-6 recovery boundary; the original RFC-2 classification baseline is d92bf94e40ac9905a00f069884e34b7d60576b82.

RFC-2 classifies Policy’s stored state from its writers, readers and disposal mechanisms. The source inventory is separate from deployment acceptance. Re-verify with the five commands in the retention guide and the PolicyRetentionTests suite in StellaOps.Policy.Engine.Tests.

Retained records and computed projections

Migration 013 gives 40 tables authoritative-record classes: authored packs/rules and versions, risk profiles and overrides, exceptions and human approvals, receipts, captured explanations and snapshots, control-register history, export artifacts, trust configuration, risk-budget accounting and recorded source/unknown-state observations. Each has a creating identity and a distinct disposal boundary in the hash-bound inventory. A foreign-key cascade describes the consequence of deleting a parent; it does not authorize deletion.

console_export_jobs stores a query, schedule, destination and signing configuration. It is an operator-authored job definition. Executions and exported bundle bytes are separate records. Risk-budget entries and their ledger are business accounting, not storage-capacity counters.

Eight tables are computed projections:

TablesReconstruction path and retained input
exploit_evidence, exploit_epss, exploit_evidence_syncExploitEvidenceSynchronizer.SyncAsync replaces the local replica from the canonical exploit/EPSS snapshot and records its generation.
violation_fusion_resultsSeverityFusionService.FuseAsync reads retained violation events and replaces the result set for a snapshot.
vuln_gate_globalVulnGateProjector replays certified relevant owner pages and hub events with the installation default, without tenant overlays: one shared outcome per required issue scope; exact configured default-only keys remain global without tenant copies. A repeated release scope is folded into the unscoped row (PGF-4, migration025). Common reset/clear revoke default completion while retaining per-key safety records for committed reapply (DIP-27).
vuln_gate_currentVulnGateProjector derives a tenant’s override rows with the global rows, from the same input read and the tenant’s overlays; a row exists only where the overlays change the global outcome. Migration 024 truncated the former per-tenant copies for a rebuild.
vuln_gate_environment_currentThe same fenced projector derives environment preference outcomes from the global inputs and retained tenant overlays; readers take an environment row before the tenant or global row.
vuln_tenant_overlayVulnGateProjector.ApplyOverlayAsync materializes ledger overlay events, preserving their event/anchor references.
vuln_system_default_overlayEvery VulnGateProjector derivation records the statement of the installation configuration PolicyEngine:VulnGateProjection:SystemDefaultOverlay that an issue’s rows carry; VulnGateProjector.ApplySystemDefaultOverlayAsync re-derives every recorded and configured key after a rebuild (SPRINT_20261007_062 DIP-27).

These classes do not schedule projection deletion. Reconstruction requires the source inputs and applicable algorithm version. An input hash alone is not a reconstruction mechanism.

PGF-6 adds an explicit consensus retention-gap rebuild, documented in the Policy architecture. It clears the three derived gate tables in bounded, fenced transactions before a complete corpus seed, preserving vuln_tenant_overlay, authored inputs and overlay feed receipts. The existing eventing checkpoint store holds one recovery fence for vuln.consensus; it is removed atomically with successful completion. Gate readiness is false throughout cleanup, partial seed and retries. This is a reconstruction boundary, not an age-based retention rule or a new copy of source data. Recovery uses the live hub owner export. A mounted section has no owner-stream coverage certificate, so the host refuses to certify it at a live head and retains the fence and unavailable readiness across idle polls and restart. A larger mount generation is not freshness proof.

The review does not claim that every declared record has a running producer. In particular, the advisory-source repositories read stored impact/conflict observations without a demonstrated reconstruction writer; evidence-hook and recheck-policy configuration, submitted evidence and the seeded EPSS thresholds remain schema contracts. They are retained records, without a claim that their missing configuration/ingestion workflows are implemented.

Running cleanup and budgets

The Policy host registers PolicyRetentionService alongside its owned persistence. It runs on startup after migration convergence, then daily. Its PolicyRetentionStore operates only on Policy’s own database. Maintenance is distinct from a tenant’s public report-purge request.

StateRule
gate_decisionsReclaim records older than 90 days on evaluated_at, preserving the existing declared audit window.
replay_auditReclaim records older than 90 days on replayed_at, preserving the existing declared audit window.
attestation_reportsReclaim the verification cache after expires_at. The service and direct PostgreSQL store use seven days when no expiry is supplied. Signed attestations and verification policies remain separate records.
orchestrator_jobs, risk_scoring_jobs, worker_resultsOn each successful pass, reclaim only completed, failed or cancelled rows with a completion timestamp that are older than 90 days or beyond the newest 10,000 terminal rows per tenant.

Queued, pending, running and unknown-status work survives both limits, including an active row carrying an old completion timestamp. Terminal rows with no completion time also survive and produce a Doctor finding. Counts are per tenant, so one tenant cannot consume another’s retained terminal allowance. A candidate whose tenant or completion time changes during cleanup is preserved and reconsidered on the next pass. Failures are logged and retried; pruning is idempotent.

Configuration is under Policy:Retention:

Policy:
  Retention:
    RetentionDays: 90
    TerminalRowsPerTenant: 10000
    Interval: "1.00:00:00"

All three settings must be positive. The SQL function independently rejects null or nonpositive days/budgets. RetentionDays governs the two audit histories and the three terminal job stores; report expiry remains row-specific.

Doctor and tenant isolation

GET /doctor/policy-engine/checks requires the ops.health scope. The doctor.policy-engine.retention.job-budgets check reports physical relation bytes, row counts, maximum per-tenant row count, expired terminal rows and terminal rows without timestamps. It reports capacity pressure while preserving active work. It returns no tenant identifiers or job payloads. The configured row figure is also the warning threshold for the total per-tenant backlog; active work is never deleted to meet it. Database/query failures become unhealthy checks through the shared Doctor runner.

The public attestation-report purge now resolves the authenticated tenant and includes that tenant in the delete predicate. Missing tenant context fails closed. A caller cannot purge another tenant’s expired reports. Migration 012 fills earlier null expiries in both the column and the stored report payload using the existing seven-day default, then requires an expiry. Caller-set expiries are preserved.

Guarded retirement

Migration 011 removes five earlier tables with no application storage callers: epss_history, epss_scores, risk_profile_history, risk_score_history and risk_scores. It also removes their unused epss_current and high_risk_vulns views. A read-only deployment query found all five tables empty. Every retirement candidate is locked and checked before any drop; unexpected rows stop convergence with SQLSTATE 55000 for explicit preservation/review. No cascading drop hides an unreviewed dependency.

The current EPSS replica is exploit_epss. RiskProfileRepository stores profile versions in risk_profiles itself. Their behavior remains separate from the unused tables. Migration 012 also removes the two unused predecessor audit-purge helpers after adding the keeper’s single maintenance function. Applied migrations 001–010 remain unchanged.