- 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>
91 lines
2.9 KiB
Go
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
|
|
}
|