Architecture
- Hexagonal core
- The flow
- One shared driver manager, many axes
- Capability-aware routing
- Advisory AI
- 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
RegistryDriverContractTestCaseis shipped so third-party drivers are held to the same bar.