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.

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.

BackendLLM_BACKENDRequiresNotes
Ollama (local)ollama (default)ollama serve running locallyNo API key, no data leaves the machine. Set OLLAMA_MODEL to any model you've pulled.
Anthropic (frontier)anthropicANTHROPIC_API_KEYUses the Messages API. Set ANTHROPIC_MODEL to override the default.

See .env.example in the repository root for all variables.

Adding a new backend

  1. Implement LlmProvider in crates/llm/src/providers/<name>.rs.
  2. Add a variant to LlmBackend in crates/llm/src/config.rs and wire it into LlmConfig::from_env.
  3. provider_from_env() in crates/llm/src/lib.rs picks 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:

  1. Persistence — done: crates/db's CaseRepository trait + SqliteCaseRepository, wired into web/server via Leptos context.
  2. NLP extraction pipeline — done: crates/extraction extracts structured domain::Resolution fields from raw filing text via the llm crate, with a schema-constrained prompt and validation, persisted via CaseRepository::upsert.
  3. Crawler — done for SEC and FCA: crates/crawler's FilingSource trait + run_crawl fetch, dedupe, and feed real filings through the same extraction::extract_case entrypoint the manual-paste UI uses. No DoJ connector (bot-blocked, see CLAUDE.md) or OFAC connector yet. Nothing schedules the crawl binary itself — that's on the operator (cron, systemd timer, ...).
  4. Search and filtering UI — done: SearchPanel/CaseList (web/app/src/app.rs) filter by industry, jurisdiction, violation type, and law firm/monitor via CaseRepository::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.
  5. 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/AlertsPanel manage 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.
  6. 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_report feeds it the whole dataset (not the search filter); TrendPanel renders it as simple CSS bar lists. No time-series/trend- over-time view yet — extracted cases don't reliably have a signed_on date, so a "per quarter" breakdown isn't meaningful with today's data.
  7. Routing (leptos_router) — only once there's a second page to justify it.
  8. 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.