mirror of
https://github.com/luxfi/aml.git
synced 2026-09-24 00:25:20 +00:00
Subject.Valid is the single place that decides what an unusable subject is — unknown kind, no tenant, no identifier — and the scope that builds subjects and both stores that consume them all defer to it. Each of those failures has the same consequence and it is the worst one available: a subject naming nobody yields an empty window, and a rule over an empty window sees no activity, which is indistinguishable from a customer who did nothing. A rule that reaches no verdict now records review regardless of the action it was configured with. A failing flag-only rule escalates, because nobody looks at a flag; a failing block rule does not block, because an evidence outage would otherwise decline every payment. Reference data refuses rather than guesses: an unloaded or undated jurisdiction listing fails loudly instead of answering not-listed for every country on earth. Dropped the empty-expression check, which the parser already makes. 22 of 22 mutations to the engine, history and reference guards are killed by the suite; 58 of 58 across measure, sanctions and engine together. Co-authored-by: Hanzo Dev <[email protected]>
90 lines
2.2 KiB
Go
90 lines
2.2 KiB
Go
package history
|
|
|
|
import (
|
|
"context"
|
|
"sort"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
// Memory is an in-process Store.
|
|
//
|
|
// It exists so the detection logic can be exercised at the level a reviewer cares
|
|
// about — given this sequence of transactions, does the rule fire — without
|
|
// standing up a database. It applies the same tenant scoping and the same
|
|
// timestamp semantics as the persistent store, because a test against a store
|
|
// with weaker rules than production proves nothing about production.
|
|
type Memory struct {
|
|
mu sync.RWMutex
|
|
events []Event
|
|
orgs []string
|
|
now func() time.Time
|
|
}
|
|
|
|
// NewMemory builds an empty in-process store. now supplies the reference instant
|
|
// for the lookback; nil means the wall clock.
|
|
func NewMemory(now func() time.Time) *Memory {
|
|
return &Memory{now: now}
|
|
}
|
|
|
|
// Append records an event for an organisation.
|
|
func (m *Memory) Append(orgID string, e Event) {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
m.events = append(m.events, e)
|
|
m.orgs = append(m.orgs, orgID)
|
|
}
|
|
|
|
// Window returns the subject's events over the lookback, most recent first.
|
|
func (m *Memory) Window(_ context.Context, subj Subject, lookback time.Duration) ([]Event, error) {
|
|
if err := subj.Valid(); err != nil {
|
|
return nil, err
|
|
}
|
|
field, _ := identify(subj.Kind)
|
|
|
|
since := m.reference().Add(-lookback)
|
|
|
|
m.mu.RLock()
|
|
defer m.mu.RUnlock()
|
|
|
|
var out []Event
|
|
for i, e := range m.events {
|
|
if m.orgs[i] != subj.OrgID {
|
|
continue
|
|
}
|
|
if field(e) != subj.ID {
|
|
continue
|
|
}
|
|
if e.At.Before(since) {
|
|
continue
|
|
}
|
|
out = append(out, e)
|
|
}
|
|
sort.SliceStable(out, func(i, j int) bool { return out[i].At.After(out[j].At) })
|
|
return out, nil
|
|
}
|
|
|
|
func (m *Memory) reference() time.Time {
|
|
if m.now != nil {
|
|
return m.now()
|
|
}
|
|
return time.Now().UTC()
|
|
}
|
|
|
|
// identify returns the accessor for a subject kind.
|
|
func identify(kind string) (func(Event) string, bool) {
|
|
switch kind {
|
|
case SubjectUser:
|
|
return func(e Event) string { return e.User }, true
|
|
case SubjectAccount:
|
|
return func(e Event) string { return e.Account }, true
|
|
case SubjectCounterparty:
|
|
return func(e Event) string { return e.Counterparty }, true
|
|
case SubjectDevice:
|
|
return func(e Event) string { return e.Device }, true
|
|
case SubjectAddress:
|
|
return func(e Event) string { return e.Address }, true
|
|
}
|
|
return nil, false
|
|
}
|