incomplianceRadar
AI-powered research and monitoring for global compliance monitorships, Deferred Prosecution Agreements (DPAs), and Non-Prosecution Agreements (NPAs).
Compliance counsel, corporate-governance officers, and risk officers need fast access to comparable enforcement cases — who was appointed as monitor, what obligations were imposed, how long the term ran — but that information is scattered across DoJ/SEC/FCA press releases, court filings, and international registers. incomplianceRadar aggregates, structures, and analyzes that data in one place.
This site documents the project for engineers (human and AI) working on the codebase. For the full product concept, see Product Concept. For the current implementation state and how to run it locally, jump to Getting Started.
- Source code: github.com/asmuelle/incompliance-radar
- Status: early-stage full-stack Rust scaffold — see Roadmap
Product Concept
(English summary of spec.md, which remains the canonical source in German.)
The problem
Finding current and historical monitorships, DPAs, or NPAs is time-consuming. Information is scattered across regulatory press releases, court filings, and international registers (DoJ, SEC, OFAC, etc.). Law firms preparing a mandate or doing competitive analysis struggle to systematically compare patterns, appointed monitors, or specific conditions across cases.
Core capabilities (target state)
- Automated data crawler — continuously scans global regulators (DoJ, SEC, FCA, etc.) and court databases for new settlements, deferred prosecutions, and monitor appointments.
- NLP extraction — parses unstructured filings and multi-hundred-page agreements to extract: company name and industry, violation type (FCPA/ bribery, money laundering, sanctions violations, ...), DPA/NPA term and status, appointed monitor and firm, specific obligations and financial sanctions.
- Advanced search and filtering — by industry, jurisdiction, violation type, or law firm, to find precedents for advisory work.
- Real-time radar and alerts — notify users when a new proceeding is opened, a monitor is appointed, or a DPA concludes, scoped to industries or competitors they track.
- Trend and benchmark analysis — statistics on which industries face the most monitor appointments and which compliance requirements are becoming standard.
Value for compliance-focused law firms
- Time savings in mandate preparation — fast access to comparable cases and conditions.
- Strategic insight — which monitors are active in which industries, and how strict comparable conditions have been.
- Business development — identify companies in active remediation phases that may need help building new compliance structures.
Architecture
Full-stack Rust using Leptos (server-side rendering +
WASM hydration) on Axum, built with
cargo-leptos.
crates/
domain/ Wasm-safe core types (Company, ComplianceCase, Resolution,
Monitor, Sanction, ViolationType, Regulator).
llm/ Pluggable LLM provider abstraction (Ollama + Anthropic),
server-only.
db/ Persistence: CaseRepository + AlertRepository traits,
SqliteCaseRepository/SqliteAlertRepository (share one pool),
server-only.
extraction/ LLM-based structured extraction of a ComplianceCase from raw
filing text, server-only.
crawler/ Scheduled fetch jobs (FilingSource trait + SEC/FCA connectors)
feeding extraction. Standalone `crawl` binary, server-only.
web/
app/ Shared Leptos UI + server functions + fictional seed data.
frontend/ Wasm hydration entry point.
server/ Axum server binary.
style/ Plain CSS.
Why split app / frontend / server
cargo-leptos needs one crate compiled for wasm32-unknown-unknown (the
client) and one compiled natively (the server), sharing UI code. Server-only
dependencies (axum, tokio, sqlx, llm, db, extraction) must never leak
into a crate built for wasm32, or the client build breaks.
crates/domain has no async runtime or HTTP client dependency, so it
compiles for both targets and is shared everywhere. web/app's llm, db,
and extraction dependencies are optional and gated behind the ssr Cargo
feature; the #[server] functions that use them reference them via
fully-qualified paths rather than a top-level use, since the macro only
compiles the function body under ssr — see CLAUDE.md in the repository
root for the full rationale and the pattern to follow when adding new
server-only dependencies.
Persistence
crates/db stores each ComplianceCase as a JSON blob in SQLite, alongside
indexed industry/jurisdiction/company_name columns used for filtering.
web/server connects and runs migrations at startup, seeding the fictional
demo cases only if the database is empty, then makes the repository
available to server functions through Leptos context
(leptos_routes_with_context + provide_context).
CaseRepository::search combines a SQL WHERE on the indexed
industry/jurisdiction columns with an in-memory filter (over that
already-narrowed result set) for violation type and monitor firm, which live
inside the JSON blob rather than their own columns. Still server-side search
from the client's perspective — see Search and filtering
below.
NLP extraction
crates/extraction sends raw filing text to the configured llm::LlmProvider
with a schema-constrained system prompt, then parses and validates the
model's JSON response before converting it into a domain::ComplianceCase
(unrecognized enum values fall back to Other(_); malformed dates, negative
sanction amounts, or an unrecognized status are rejected rather than
guessed). The "Extract a case from filing text" panel in the UI calls this
end-to-end and persists the result via CaseRepository::upsert.
Crawler
crates/crawler fetches real filings so extraction doesn't rely solely on
manual paste. FilingSource is the per-regulator trait; run_crawl fetches,
dedupes by URL against sources already recorded on existing cases, and feeds
new filings through extraction::extract_case + CaseRepository::upsert.
Two connectors exist, both verified against the live sites: SEC (RSS feed +
Drupal body selector, rate-limit aware) and FCA (general news RSS + article
selector). There's deliberately no DoJ connector — justice.gov blocks
automated clients with a bot-management challenge, and defeating that isn't
something this project does. The crawl binary runs one pass and exits; an
external scheduler (cron, a systemd timer) invokes it periodically.
Search and filtering
The UI's SearchPanel filters by industry, jurisdiction, violation type, and
law firm/monitor, calling list_cases with a CaseFilterQuery (an all-None
filter matches everything). CaseList's data resource refetches whenever the
filter changes or an extraction completes.
Alerts
Global watch rules (industry and/or company-name-substring criteria), not
per-user — the app has no auth/user system. db::evaluate_case checks a
newly-persisted case against every rule and records a domain::Alert for
each match; both extract_case and the crawler's ingest_filing call it
right after CaseRepository::upsert, so a case triggers alerts the same way
regardless of how it arrived. The UI's WatchRulesPanel manages rules;
AlertsPanel shows and acknowledges triggered alerts. There's no actual
notification delivery (email/push) — alerts only show up in-app.
Trend analysis
domain::compute_trend_report is a pure function over &[ComplianceCase] —
no database access, fully unit-testable with hand-built fixtures. It
aggregates case counts by industry, resolution counts by regulator/violation
type/kind/status, monitorship rate by industry, and total sanctions summed
per currency (not converted to one currency). The get_trend_report server
function calls CaseRepository::list() (the whole dataset, not the current
search filter) and feeds it straight to compute_trend_report. TrendPanel
renders each section as a simple CSS bar list rather than pulling in a JS
charting library.
Data flow (current state)
Two ways a filing reaches extraction::extract_case:
Browser (WASM) --hydrate--> App component --#[server] fns--> Axum server
SEC/FCA RSS + pages --crawler::run_crawl-------------------> Axum process
Both converge on the same pipeline:
raw filing text --> extraction::extract_case (schema prompt + validate)
| |
llm::provider_from_env() db::CaseRepository::upsert
| |
Ollama (local) or SQLite
Anthropic (frontier)
See Roadmap for what's next.
Getting Started
Prerequisites
rustup target add wasm32-unknown-unknown
cargo install cargo-leptos
cargo-leptos shells out to wasm-bindgen, and the installed
wasm-bindgen-cli version must exactly match the wasm-bindgen crate
version resolved in Cargo.lock:
cargo install wasm-bindgen-cli --version <version>
If cargo leptos build fails with a schema-version mismatch, that's the fix
— reinstall the CLI at the version it names in the error.
Run locally (local model via Ollama)
ollama pull llama3.1 # or use any model you already have pulled
cp .env.example .env # defaults to LLM_BACKEND=ollama
cargo leptos watch
Open http://127.0.0.1:3000.
Run locally (frontier model via Anthropic)
cp .env.example .env
Then in .env:
LLM_BACKEND=anthropic
ANTHROPIC_API_KEY=sk-...
Common commands
cargo leptos watch # dev server with hot reload
cargo leptos build --release # production build
cargo test --workspace --exclude frontend
cargo fmt --all
cargo clippy --workspace --exclude frontend -- -D warnings
LLM Backends
incomplianceRadar's NLP extraction and Q&A features run against a single
LlmProvider trait (crates/llm/src/lib.rs), selected at runtime via the
LLM_BACKEND environment variable — no code changes needed to switch.
| Backend | LLM_BACKEND | Requires | Notes |
|---|---|---|---|
| Ollama (local) | ollama (default) | ollama serve running locally | No API key, no data leaves the machine. Set OLLAMA_MODEL to any model you've pulled. |
| Anthropic (frontier) | anthropic | ANTHROPIC_API_KEY | Uses the Messages API. Set ANTHROPIC_MODEL to override the default. |
See .env.example in the repository root for all variables.
Adding a new backend
- Implement
LlmProviderincrates/llm/src/providers/<name>.rs. - Add a variant to
LlmBackendincrates/llm/src/config.rsand wire it intoLlmConfig::from_env. provider_from_env()incrates/llm/src/lib.rspicks it up automatically.
This is the same Repository/Strategy pattern used elsewhere in the codebase — callers depend on the trait, never on a concrete provider.
Roadmap
Current state is a working full-stack scaffold: SSR + WASM hydration,
SQLite-backed persistence (crates/db, seeded with fictional demo data), a
live query panel against the configured LLM backend, an LLM-based extraction
pipeline (crates/extraction), a crawler (crates/crawler) that pulls real
press releases from the SEC and FCA and feeds them through extraction
automatically, search/filtering by industry, jurisdiction, violation type,
and law firm/monitor, global watch-rule alerts, and a trend/benchmark
dashboard. Every core feature in Product Concept has a
working (if scoped-down) implementation now.
Rough next steps, roughly in dependency order:
Persistence— done:crates/db'sCaseRepositorytrait +SqliteCaseRepository, wired intoweb/servervia Leptos context.NLP extraction pipeline— done:crates/extractionextracts structureddomain::Resolutionfields from raw filing text via thellmcrate, with a schema-constrained prompt and validation, persisted viaCaseRepository::upsert.Crawler— done for SEC and FCA:crates/crawler'sFilingSourcetrait +run_crawlfetch, dedupe, and feed real filings through the sameextraction::extract_caseentrypoint the manual-paste UI uses. No DoJ connector (bot-blocked, see CLAUDE.md) or OFAC connector yet. Nothing schedules thecrawlbinary itself — that's on the operator (cron, systemd timer, ...).Search and filtering UI— done:SearchPanel/CaseList(web/app/src/app.rs) filter by industry, jurisdiction, violation type, and law firm/monitor viaCaseRepository::search, server-side (not client-side filtering of a fully-fetched list). No free-text search, date-range filtering, or pagination yet — revisit once there's a real backlog of cases to justify it.Alerts— done as global watch rules, not user-scoped (this app has no auth/user system — see CLAUDE.md for that tradeoff).domain::WatchRule(industry and/or company-name-substring criteria) +db::evaluate_case, checked after every case persisted via manual extraction or the crawler.WatchRulesPanel/AlertsPanelmanage rules and show/acknowledge triggered alerts. No actual notification delivery (email/push) — in-app only. Revisit user-scoping if/when real multi-user need emerges.Trend/benchmark analysis— done:domain::compute_trend_report(pure function, no DB access) aggregates case/resolution counts, monitorship rate by industry, and total sanctions by currency;get_trend_reportfeeds it the whole dataset (not the search filter);TrendPanelrenders it as simple CSS bar lists. No time-series/trend- over-time view yet — extracted cases don't reliably have asigned_ondate, so a "per quarter" breakdown isn't meaningful with today's data.- Routing (
leptos_router) — only once there's a second page to justify it. - OFAC connector, and a real DoJ data source — e.g. an official API/data-sharing arrangement rather than scraping around their bot protection.
Contributions and issues: github.com/asmuelle/incompliance-radar.