Architecture

  1. Hexagonal core
  2. The flow
  3. One shared driver manager, many axes
  4. Capability-aware routing
  5. Advisory AI
  6. Quality bar

Hexagonal core

The core package depends only on PSR interfaces:

Concern Interface Notes
HTTP PSR-18 ClientInterface + PSR-17 factories injected; no hard Guzzle
Cache PSR-16 CacheInterface used by the AI strategy
Logging PSR-3 LoggerInterface optional; defaults to NullLogger
Clock PSR-20 ClockInterface SystemClock in prod, FrozenClock in tests

There is no framework and no vendor SDK in the core, so the same domain logic runs unchanged on Laravel, Symfony, or plain PHP. The framework bridges only wire PSR implementations and add ergonomics (facade, config, validation rules, DI tags).

The flow

Counterparty ─► Verifier ─► [ Check, Check, ... ] ─► VerificationReport (hard facts)
                                                          │
                                                          ▼
                                              RiskStrategy ─► RiskAssessment (advisory)
                                                          │
                                                          ▼
                                                   VerificationOutcome
  • A Check produces one deterministic CheckResult (pass / warning / fail / inconclusive).
  • The VerificationReport is the body of hard facts.
  • A RiskStrategy turns that report into an advisory RiskAssessment.
  • The VerificationOutcome bundles counterparty + report + assessment.

See Verification flow for the step-by-step.

One shared driver manager, many axes

A single generic AbstractDriverManager<T> powers every pluggable axis, with two registration paths:

  • extend(string $name, callable $factory) - Laravel-style closure DX for app code.
  • register(DriverFactory $factory) - DI-friendly, taggable in Symfony.

Resolution is lazy, memoised and case-insensitive. The same manager is reused for separate driver interfaces: RegistryDriver, SanctionsProvider, and (in the AI package) AiResearchProvider and ResearchTool. The mechanism is abstracted; the driver shapes are not - there is deliberately no single “universal driver” interface.

Capability-aware routing

A RegistryDriver declares which RegistryCapability values it can answer and for which countries. The RegistryManager routes by (country + capability) and returns an honest inconclusive (with a reason) when nothing covers a request - never a guess. This is what makes “adding a country” a local change. See Registries.

Advisory AI

Sanctions hits and VAT status are deterministic CheckResults. The AI consumes the finished report as ground truth and only contextualises qualitative risk, grounded only in tool outputs. Every claim becomes Evidence with a source URL; no source means inconclusive. RiskAssessment::requiresHumanReview() returns true below a confidence threshold or on any adverse finding. See AI.

Quality bar

  • PHPStan level max across all packages (with larastan / phpstan-symfony for the bridges).
  • Psalm error level 1 on the framework-agnostic packages and the Symfony bundle.
  • Tests mock external APIs - no live network in CI. Matrix: PHP 8.2 / 8.3 / 8.4.
  • The RegistryDriverContractTestCase is shipped so third-party drivers are held to the same bar.

Counterparty Verification - a due-diligence aid, not a compliance product. MIT licensed.

This site uses Just the Docs, a documentation theme for Jekyll.