feat(audit-log): add site SQLite PurgeExpiredAsync honoring the ForwardState invariant

Retention purge deletes Forwarded/Reconciled rows older than the cutoff and
reclaims pages via incremental_vacuum; Pending rows are never purged on age
alone (would lose audit data that never reached central). Temp-table purge set
avoids the SQLite 999-param limit; runs under _writeLock. (PLAN-04 Task 1, S1/U2)
This commit is contained in:
Joseph Doherty
2026-07-09 06:11:54 -04:00
parent 1586e791f7
commit 731ee69797
3 changed files with 227 additions and 3 deletions
@@ -142,4 +142,25 @@ public interface ISiteAuditQueue
/// <param name="ct">Cancellation token.</param>
/// <returns>A task that resolves to a point-in-time snapshot of the site audit queue's pending count, oldest timestamp, and on-disk file size.</returns>
Task<SiteAuditBacklogSnapshot> GetBacklogStatsAsync(CancellationToken ct = default);
/// <summary>
/// Retention purge: permanently deletes rows that have already left the site
/// (<see cref="ZB.MOM.WW.ScadaBridge.Commons.Types.Enums.AuditForwardState.Forwarded"/> or
/// <see cref="ZB.MOM.WW.ScadaBridge.Commons.Types.Enums.AuditForwardState.Reconciled"/>)
/// whose <see cref="AuditEvent.OccurredAtUtc"/> is strictly older than
/// <paramref name="olderThanUtc"/>, and reclaims the freed pages.
/// </summary>
/// <remarks>
/// <b>Hard <see cref="ZB.MOM.WW.ScadaBridge.Commons.Types.Enums.AuditForwardState"/> invariant:</b>
/// a row still in <see cref="ZB.MOM.WW.ScadaBridge.Commons.Types.Enums.AuditForwardState.Pending"/>
/// is NEVER purged on age alone — it has not yet reached central, so dropping it
/// would silently lose audit data. Only Forwarded/Reconciled rows are eligible.
/// Idempotent: a second call with the same cutoff removes nothing further and
/// returns 0. The site store is ephemeral (~7-day retention), so this bounds its
/// unbounded growth (arch-review 04, S1/U2).
/// </remarks>
/// <param name="olderThanUtc">Exclusive upper bound on <see cref="AuditEvent.OccurredAtUtc"/>; rows at or after this instant are retained.</param>
/// <param name="ct">Cancellation token.</param>
/// <returns>A task that resolves to the number of canonical rows deleted.</returns>
Task<int> PurgeExpiredAsync(DateTime olderThanUtc, CancellationToken ct = default);
}