feat: DEBT-014 + DEBT-029 Audit Infrastructure (Duplicate Detection & Event Logging)
DEBT-014: Duplicate detection & reconciliation tracking - Create operation_audit_trail migration (0011) - Hook OutboxPollerJob to detect and log duplicates - Implement MetricsSql queries for duplicate/reconciliation metrics DEBT-029: Audit trail consumer integration - Create AuditTrailConsumer for event-driven audit logging - Map 12+ event types to compliance.operation_audit_trail - Register consumer in Program.cs DI and OutboxPollerJob AGENTS.md v16.0 Compliance: ✅ Necessity: Both DEBT items from registry (2+3 pts) ✅ Simplicity: Event-driven via Outbox pattern (existing infra) ✅ Pattern: Vertical Slice consumer + SQL queries (established) ✅ Traceability: All event types documented and mapped ✅ Safety: Idempotent logging via ON CONFLICT DO NOTHING ✅ Maturity: Framework ready before feature implementation Impact: Medium/High (5 pts total, Q3 target 4 pts exceeded) Status: Code ready, awaiting SSH tunnel for migration test Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -76,22 +76,56 @@ public class MetricsSql
|
||||
|
||||
public async Task<(int Detected, int Resolved, DateTime LastCheck)?> GetDuplicateDetectionAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
// building_blocks.outbox_message table exists, but duplicate event logging not yet implemented.
|
||||
// Inbox UNIQUE constraints silently reject duplicates; outbox doesn't log detection events.
|
||||
// Implementation deferred: OutboxPollerJob would need to hook duplicate tracking (DEBT-014).
|
||||
// Returns null until audit infrastructure is extended.
|
||||
await Task.CompletedTask;
|
||||
return null;
|
||||
const string sql = """
|
||||
SELECT
|
||||
COUNT(*) as detected,
|
||||
COUNT(CASE WHEN resolved_at IS NOT NULL THEN 1 END) as resolved,
|
||||
MAX(detected_at) as last_check
|
||||
FROM compliance.operation_audit_trail
|
||||
WHERE event_type = 'DUPLICATE_DETECTED'
|
||||
AND detected_at >= @sevenDaysAgo
|
||||
AND published_at <= @now
|
||||
""";
|
||||
|
||||
var now = _clock.UtcNow.UtcDateTime;
|
||||
await using var connection = await _dataSource.OpenConnectionAsync(cancellationToken);
|
||||
var result = await connection.QueryFirstOrDefaultAsync<(int, int, DateTime)?>(
|
||||
sql,
|
||||
new { now, sevenDaysAgo = now.AddDays(-7) },
|
||||
commandTimeout: 5);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public async Task<(int Detected, int Resolved, List<string> Pending)?> GetReconciliationBreaksAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
// Reconciliation break detection requires Evidence version mismatch correlation.
|
||||
// Requires audit log showing actual vs. expected state divergence (currently not captured).
|
||||
// Implementation deferred: job consumers must emit version mismatches to operation_audit_trail (DEBT-014).
|
||||
// Returns null until audit trail is enriched.
|
||||
await Task.CompletedTask;
|
||||
return null;
|
||||
const string sql = """
|
||||
SELECT
|
||||
COUNT(*) as detected,
|
||||
COUNT(CASE WHEN resolved_at IS NOT NULL THEN 1 END) as resolved,
|
||||
STRING_AGG(DISTINCT (details->>'reason'), ', ') as reasons
|
||||
FROM compliance.operation_audit_trail
|
||||
WHERE event_type = 'RECONCILIATION_BREAK_DETECTED'
|
||||
AND detected_at >= @sevenDaysAgo
|
||||
AND published_at <= @now
|
||||
""";
|
||||
|
||||
var now = _clock.UtcNow.UtcDateTime;
|
||||
await using var connection = await _dataSource.OpenConnectionAsync(cancellationToken);
|
||||
var result = await connection.QueryFirstOrDefaultAsync<(int, int, string?)?>(
|
||||
sql,
|
||||
new { now, sevenDaysAgo = now.AddDays(-7) },
|
||||
commandTimeout: 5);
|
||||
|
||||
if (result == null || result.Value.Item1 == 0)
|
||||
return null;
|
||||
|
||||
var (detected, resolved, reasons) = result.Value;
|
||||
var pending = string.IsNullOrEmpty(reasons)
|
||||
? new List<string>()
|
||||
: reasons.Split(',').Select(r => r.Trim()).Take(5).ToList();
|
||||
|
||||
return (detected, resolved, pending);
|
||||
}
|
||||
|
||||
public async Task<(decimal Baseline, decimal Current)?> GetModelDriftAsync(CancellationToken cancellationToken = default)
|
||||
|
||||
Reference in New Issue
Block a user