7 Commits
Author SHA1 Message Date
zeekayandHanzo Dev 3c041ed331 merge the detection library and the screening rewrite into one engine
Two branches had independently rebuilt the same two packages, which is one
implementation too many. This resolves them into one.

Their side is the base for detection and screening, because it is better:

  - Rules name evidence, and SetRules refuses a set whose evidence this
    deployment cannot supply — so the eleven stub helpers that silently disabled
    thirteen rules cannot exist. A rule that cannot fire is worse than an absent
    one, because it still appears in the catalog a regulator reads.
  - One screening store, filled by the refresh and read by the endpoints. Two of
    them is why sanctions search answered "no match" for every name while
    designations were being loaded somewhere else.
  - Parsers for all four publishers against their real files, recovering what the
    single-parser path could not read at all: OFAC's digital-currency addresses,
    weak aliases held apart from screening candidates, and OFSI's row-per-name
    layout collapsed to subjects.
  - Name matching over runes, not bytes. Two distinct Han names scored 0.867
    byte-wise, above the 0.85 threshold, and 0.700 correctly.
  - Deviation from the median and MAD rather than the mean, because a monitoring
    baseline contains the outliers it is used to hunt.
  - A rule vocabulary with providers behind every term, in place of expressions
    reaching into a struct.

Kept from this side, because their branch did not have it:

  - The tenant comes from an authenticated identity, not from a request header.
  - A rule that could not run carries no weight. Their action() already capped a
    failing rule at review, which is right, but the hit still entered the score
    at full weight — so a broken rule saturated every transaction's score and the
    queue stopped ranking. Faults are now excluded from the aggregate while still
    raising their own alert and still forcing review.
  - Per-source readiness: which list, how many designations, how long ago, and
    the reason if the last attempt failed. Their store answers whether screening
    can be relied on at all; this answers what is behind it, which is what an
    operator can act on.
  - Behavioural scoring, retention, tokenisation, replay and reference data.

Conversion now refuses an unknown currency instead of passing the amount
through. Passing it through understates every currency worth more than a dollar,
so a payment three times over a reporting limit tested as sitting under it.

One ordering table for actions (types.ActionRank), one EvalAll signature, one
screening store, one readiness surface. 15 packages, tests and race clean.

Co-authored-by: Hanzo Dev <[email protected]>
2026-07-29 11:49:08 -07:00
zeekayandHanzo Dev d8812d66f1 engine: a rule that could not run is a fault, not a match
An ordinary $100 payment came back with six alerts and a saturated score, so the
effective false-positive rate was 100% and the score ranked nothing.

The mechanism was one line of policy. A rule whose evaluation errored was
recorded as a hit with Match=true, on the reasoning that a broken rule should not
silently let a transaction through. The safety instinct is right; the encoding
conflated two different findings. "This transaction exhibits this typology" is
evidence about the transaction. "This rule did not run" is a fault in the system,
which says nothing about the transaction — and because a rule that fails once
fails on every transaction, recording it as a match alerts on all of them.

Evaluate now separates them. A matching rule produces a typology alert that
carries its score. Rules that could not be evaluated produce one alert naming
them, with no score, because a rule that did not run is not evidence of risk. The
transaction is still not cleared: the fault forces review, since a transaction
assessed by only part of the rule set has not been assessed.

Tests cover the reported case directly — six unevaluable rules against a benign
payment, expecting one fault alert, zero score, and a refusal to allow — plus a
benign transaction under a working rule set alerting not at all, and a fault
alongside a real match where only the match scores. Five mutants, including
restoring the original Match=true, all killed.

Co-authored-by: Hanzo Dev <[email protected]>
2026-07-29 11:08:24 -07:00
zeekayandHanzo Dev c1372cb0cc aml: anomaly scoring, retention, tokenisation, replay, reference data
Five gaps that a monitoring product cannot be sold without.

anomaly scores whether a transaction is unusual for the entity that made it,
which is the statutory question — consistency with the customer's own profile —
and one no fixed threshold can express. It complements the rule library rather
than replacing it: rules encode typologies someone has already named, and are
blind to anything nobody has written down. The design is deliberately
attributable: a score can name the features that drove it, which is what a
supervisor asks for and what a neural scorer cannot answer.

retention keeps records for their statutory period and destroys them when it
runs out, including the retained assessment whether or not it produced a report.
token protects direct identifiers at rest while keeping the engine able to
correlate: one key schedule, a deterministic org-scoped pseudonym to index on
and a sealed record for reads that are entitled to it, so a record can be found
by party without holding the party's name in the clear.

replay runs a rule over real history and reports what it would have done before
anyone activates it — the same question as "how many alerts would this raise",
which is what makes a rule change reviewable rather than a live experiment.

reference holds the published data a rule needs but cannot derive: currency
conversion and jurisdiction risk listings, each carrying the date it was current
at, each refusing to answer rather than guess. Passing an unknown currency
through unchanged moves every threshold by the exchange rate — the structuring
rule compared raw notional, so 9,500 units of a minor currency read as a
near-threshold cash amount. A jurisdiction list that answers "not listed"
because it was never loaded is indistinguishable from a clean world.

Co-authored-by: Hanzo Dev <[email protected]>
2026-07-29 11:04:33 -07:00
zeekayandHanzo Dev 04accf64e0 engine, history: one definition of a subject that names nobody
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]>
2026-07-29 11:01:22 -07:00
zeekayandHanzo Dev 039fa7e3d6 engine: a rule is admitted only if its evidence exists
The previous evaluator registered a stub for every term the language offered.
Absent a real implementation, sanctions screening answered false, geographic
matching answered true, the local hour answered noon, travel-rule completeness
answered complete, and chain risk answered zero. Six of the terms were later
replaced by store-backed versions; eleven never were, which left thirteen of the
twenty-three shipped rules structurally unable to fire. Measured on the shipped
code: a 50,000 dollar crypto payment at 3 a.m. from a politically exposed
customer in a listed jurisdiction, to a fresh address, naming a designated
person, raised four alerts out of twenty-three.

There are now no stubs. Terms are methods on the evaluation scope, each term is
mapped to the provider that answers it, and a rule naming a term whose provider
is absent is refused when the rule set is installed. The refusal names the term.
A rule set containing one such rule installs nothing, because a partially
installed set is a control surface nobody can describe.

Rules must yield a boolean. The previous evaluator accepted a number and read it
as truthy, so a velocity rule written as a bare sum fired on any activity at all.

Evidence failures at evaluation time — an unknown currency, an absent subject, an
unreachable store — are recorded on the alert and routed to review rather than
cleared, so an outage cannot become an approval.

Windows are fetched once per subject and lookback for the whole rule set rather
than once per rule.

Aggregation is over the transaction's own timestamp, not the row's insertion
time, and history queries are parameterised and scoped to the tenant. The
previous store interpolated the customer identifier into the filter string and
filtered by neither organisation nor transaction time.

Co-authored-by: Hanzo Dev <[email protected]>
2026-07-29 10:55:48 -07:00
zeekayandHanzo Dev e6f58397ab sanctions: parse all four published list families
Each parser was written against the published file, not against a description
of it. Verified counts: OFAC 19,175 (matching the Record_Count the file
declares), UN 1,011, EU 6,017, OFSI 5,135.

What the previous single parser lost, and why each matters:

OFAC publishes 963 digital currency addresses across 19 networks in idList.
Dropping idList meant a payment to a designated blockchain address could not be
detected by any means.

OFAC marks 4,383 of its 24,546 aliases weak and the UN and OFSI mark theirs
Low; flattening them to plain strings matched fragments at full confidence.

OFAC flags which of several dates of birth is the main one, and for 37 subjects
it is not the first — taking the first dropped the tie-break for exactly the
subjects that need one.

UN, EU and OFSI were parsed with the OFAC parser on the stated grounds that the
formats are similar. They are not: the roots are CONSOLIDATED_LIST, export and
a CSV. All three failed every night, logged a line, and continued.

OFSI publishes one row per name and joins them by group; 19,761 rows are 5,135
subjects. It also puts the header on line two behind a preamble, orders the
name columns Name 6 first, and writes partial dates as 00/00/1994.

A parse that yields fewer records than the publisher declares is refused rather
than loaded, because a partial sanctions list clears designated subjects.

Co-authored-by: Hanzo Dev <[email protected]>
2026-07-29 10:38:47 -07:00
zeekayandHanzo Dev 07e69e3ee2 luxfi/aml — real-time AML/CFT transaction monitoring
A Go AML engine: a configurable rules DSL over expr-lang with weight-of-evidence
scoring, sanctions screening against OFAC/UN/EU/HMT, case management, and scored
alerting. Single binary, embedded SQLite per org, embedded UI.

Surface (all /v1, org-scoped):
  /v1/aml/transactions        ingest and score
  /v1/aml/rules  /rules/test  the rules DSL and its dry-run
  /v1/aml/sanctions/search    list screening
  /v1/aml/cases               investigation workflow
  /v1/aml/health

Build: make build produces one static binary with the UI embedded (embedui tag);
bare Go tooling needs no Node. Third-party license and notice texts are generated
from the module graph the linker actually walks and ship beside every artifact.

Co-authored-by: Hanzo Dev <[email protected]>
2026-07-28 22:51:25 -07:00