Files
nexarch/internal/migration/auditcontinuity/continuity.go
T
patrickandClaude Sonnet 5.5 5b8c20777d refactor: Pruefgate-Pakete nach Funktion benannt, migration* gruppiert
- dms/ai/workflow: qa0x-Pakete nach internal/gates/<thema> verschoben, Paketname = Thema
- internal/migration{acl,audit,...} -> internal/migration/{acl,audit,...}
- Importpfade, Gate-Skripte und zwei relative Migrationspfade angepasst
- keine Logikaenderung; mail-Gates bewusst unveraendert (pfadabhaengig)

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-27 12:44:21 +01:00

91 lines
2.9 KiB
Go

// Package migrationauditcontinuity implementiert VAL-04: den
// Kontinuitaetsabgleich des Audit-Logs je Tenant nach COR-05
// (internal/migration/audit) — aeltester/neuester Eintrag und Gesamtzahl
// zwischen legacy_audit_log_staging (Quelle) und audit_events (Ziel)
// duerfen weder eine Luecke noch eine Zeitverschiebung zeigen.
package migrationauditcontinuity
import (
"context"
"fmt"
"time"
)
// AuditStats ist Anzahl sowie aeltester/neuester Zeitstempel der
// Audit-Eintraege eines Tenants auf einer Seite (Quelle oder Ziel).
type AuditStats struct {
TenantSlug string
Count int64
Oldest time.Time
Newest time.Time
}
// StatsProvider liefert AuditStats fuer einen Tenant.
type StatsProvider interface {
Stats(ctx context.Context, tenantSlug string) (AuditStats, error)
}
// ContinuityReport ist der Kontinuitaetsbericht eines Tenants
// (Akzeptanzkriterium 1).
type ContinuityReport struct {
TenantSlug string
SourceCount int64
TargetCount int64
SourceOldest time.Time
SourceNewest time.Time
TargetOldest time.Time
TargetNewest time.Time
}
// Luecke ist true, wenn die Eintragszahl zwischen Quelle und Ziel abweicht.
func (r ContinuityReport) Luecke() bool {
return r.SourceCount != r.TargetCount
}
// Zeitverschiebung ist true, wenn aeltester oder neuester Eintrag im Ziel
// nicht exakt mit der Quelle uebereinstimmt.
func (r ContinuityReport) Zeitverschiebung() bool {
return !r.SourceOldest.Equal(r.TargetOldest) || !r.SourceNewest.Equal(r.TargetNewest)
}
// OffenerBefund ist true bei Luecke oder Zeitverschiebung
// (Akzeptanzkriterium 2: jede erkannte Abweichung ist blockierend, keine
// Erklaerungs-Ausnahme wie bei VAL-01).
func (r ContinuityReport) OffenerBefund() bool {
return r.Luecke() || r.Zeitverschiebung()
}
func (r ContinuityReport) String() string {
status := "OK"
if r.OffenerBefund() {
status = "OFFENER BEFUND"
}
return fmt.Sprintf("%s: quelle(n=%d, aeltester=%s, neuester=%s) ziel(n=%d, aeltester=%s, neuester=%s) status=%s",
r.TenantSlug, r.SourceCount, r.SourceOldest, r.SourceNewest, r.TargetCount, r.TargetOldest, r.TargetNewest, status)
}
// Run vergleicht Quelle und Ziel je Tenant (Akzeptanzkriterium 1).
func Run(ctx context.Context, tenantSlugs []string, source, target StatsProvider) ([]ContinuityReport, error) {
var reports []ContinuityReport
for _, slug := range tenantSlugs {
sc, err := source.Stats(ctx, slug)
if err != nil {
return nil, fmt.Errorf("migrationauditcontinuity: quellstatistik fuer tenant %q: %w", slug, err)
}
tc, err := target.Stats(ctx, slug)
if err != nil {
return nil, fmt.Errorf("migrationauditcontinuity: zielstatistik fuer tenant %q: %w", slug, err)
}
reports = append(reports, ContinuityReport{
TenantSlug: slug,
SourceCount: sc.Count,
TargetCount: tc.Count,
SourceOldest: sc.Oldest,
SourceNewest: sc.Newest,
TargetOldest: tc.Oldest,
TargetNewest: tc.Newest,
})
}
return reports, nil
}