ADR-019: Integration Adapter Registry
# ADR-019 — Integration Adapter Registry
**Status:** Accepted
**Date:** 2026-05-11
**Author:** Petter Graff (CodeCraft — Architecture Lead)
**Decision-maker:** CEO Alem Bašić
**Mehanik clearance:** /tmp/mehanik-cleared-100362
**MC Task:** #100362 (Phase 0' ADR Consolidation)
**Cross-references:**
- ADR-015 (CountryPlugin — plugin selects adapters for its market; plugin version compatibility)
- ADR-016 (EInvoiceAdapter — one of the 7 adapter categories; lifecycle states formalised here)
- ADR-023 (routing — market resolved at edge before adapter dispatch)
- `apps/api/src/main/kotlin/no/alai/bilko/country/hr/StorecoveHrFiskEInvoiceAdapter.kt` (reference impl)
- `apps/api/src/main/kotlin/no/alai/bilko/einvoice/EInvoiceTypes.kt` (AdapterLifecycleState on disk)
- Plan v3 §6 Phase 0' Task 0'4 — `~/system/specs/bilko-multi-market-architecture-plan-v3-2026-05-11.md`
---
## 1. Context
### 1.1 Problem: Seven Integration Surfaces, No Governance
Bilko integrates with external systems across seven functional domains. As of 2026-05-11,
only one adapter exists (`StorecoveHrFiskEInvoiceAdapter`). Without a registry and governance
model, adding the second adapter (SEF for RS) and every subsequent adapter will produce:
1. Inconsistent error handling — platform-native exceptions leaking across boundaries
2. No feature-flag mechanism — a broken SEF adapter takes down all RS users
3. Secret sprawl — `STORECOVE_API_KEY` as an env var pattern, but no taxonomy when
there are 7 adapters × 4 markets × 3 environments = up to 84 secrets
4. No observability standard — each adapter invents its own logging and metrics
5. No lifecycle discipline — adapters deployed to production without sandbox verification
### 1.2 Reference Implementation Patterns
`StorecoveHrFiskEInvoiceAdapter.kt` already demonstrates all the patterns this ADR
formalises. This ADR makes those patterns enforceable for all future adapters:
| Pattern | StorecoveHrFiskEInvoiceAdapter | ADR-019 makes it |
| ------------------------------------------------ | --------------------------------------- | ---------------- |
| PII redaction before logging | Lines 24–59 (`sanitizeForLog`) | Mandatory |
| `AdapterException` only (no platform exceptions) | Lines 469–516 (`StorecoveErrorMapper`) | Mandatory |
| Per-adapter Prometheus metrics | Lines 537–540 (`StorecoveMetrics`) | Mandatory |
| Lifecycle state field | Lines 547–548 (`lifecycleState = STUB`) | Mandatory |
| Idempotency key on submit | Lines 591–600 (D5 comment) | Mandatory |
| Credentials NOT required for serialize() | Lines 567–571 | Mandatory |
| Startup credential validation flag | Lines 83–138 | Recommended |
---
## 2. Decision
### 2.1 Seven Adapter Categories
Every external integration belongs to exactly one of the following categories.
Each category is a Kotlin interface in `apps/api/src/main/kotlin/no/alai/bilko/adapter/`.
| Category | Interface | Purpose | Markets |
| -------- | ------------------------ | -------------------------------------------------------------------------- | ------------------------------------------------------------ |
| 1 | `EInvoiceAdapter` | E-invoice serialization + fiscal platform submission | HR (Storecove), RS (SEF), BA-FED (CPF), BA-RS (UINO) |
| 2 | `CompanyRegistryAdapter` | Company data lookup (name, address, tax status) from government registries | HR (FINA), RS (APR), BA (stub) |
| 3 | `BankStatementAdapter` | Bank statement import (MT940, CAMT.053, PSD2 AISP) | All markets — via Tok Open Banking platform |
| 4 | `ExchangeRateAdapter` | FX rate feed (daily/live) | All markets (ECB primary, HNB for HR, NBS for RS) |
| 5 | `TaxFilingAdapter` | Electronic VAT/CIT return submission to tax authority | HR (ePorezna), RS (ePorezi), BA (TBD) |
| 6 | `FiscalDeviceAdapter` | Fiscal receipt device or cloud fiscal service | HR (Fiskalizacija cloud cert), RS (LPFR chip card), BA (TBD) |
| 7 | `QESSigningAdapter` | Qualified Electronic Signature for invoice signing | HR (FINA QES), RS (stub), BA (stub) |
**Current implementation status:**
- `EInvoiceAdapter`: `StorecoveHrFiskEInvoiceAdapter` (HR, STUB lifecycle)
- All other categories: NOT YET IMPLEMENTED
### 2.2 Common Interface Contract
Every adapter interface extends a common `BilkoAdapter` base:
```kotlin
package no.alai.bilko.adapter
import no.alai.bilko.country.TaxJurisdiction
import no.alai.bilko.einvoice.AdapterLifecycleState
/**
* Base contract for all Bilko integration adapters.
*
* Every adapter implementation MUST:
* 1. Expose [jurisdiction] and [lifecycleState] as first-class properties.
* 2. Throw only [AdapterException] — NEVER platform-native exceptions.
* 3. Pass all log writes through [sanitizeForLog] (defined per-adapter for PII fields).
* 4. Record Prometheus metrics on every external call (see §2.6).
* 5. Not require credentials for read-only / serialization operations.
*/
interface BilkoAdapter {
val jurisdiction: TaxJurisdiction
val lifecycleState: AdapterLifecycleState
val adapterVersion: String // Semantic version string, e.g. "1.0.0"
}
```
### 2.3 AdapterException — Canonical Error Contract
All adapters throw `AdapterException` and nothing else. This exception type is the
single crossing point from adapter space to core service space.
```kotlin
package no.alai.bilko.adapter
import no.alai.bilko.country.TaxJurisdiction
/**
* Canonical adapter error. The ONLY exception type that crosses the adapter boundary.
*
* INVARIANT: Core services catch AdapterException only. They MUST NOT catch
* platform-native exceptions (Ktor ResponseException, HttpRequestTimeoutException,
* java.net.SocketTimeoutException, etc.). Map those to AdapterException in the adapter.
*
* [retryable]: if true, caller may retry with exponential backoff.
* [rawPayload]: sanitized (PII-redacted) raw response body for audit. NEVER raw.
*/
data class AdapterException(
val code: AdapterErrorCode,
val market: TaxJurisdiction,
val retryable: Boolean,
val rawPayload: String,
override val message: String = code.name,
override val cause: Throwable? = null,
) : RuntimeException(message, cause)
/**
* Canonical error codes — adapter-independent.
*
* Adapters map platform-specific HTTP status codes and error bodies to these codes.
* See StorecoveErrorMapper (lines 469–516) for the HR reference mapping.
*/
enum class AdapterErrorCode {
// Validation errors — not retryable
VALIDATION_SCHEMA_ERROR, // Invalid document structure (HTTP 400/422)
VALIDATION_BUSINESS_RULE, // Business rule violation (e.g., invalid OIB, non-EUR currency)
VALIDATION_DUPLICATE_DOCUMENT, // Idempotency conflict (HTTP 409)
// Authentication/authorisation — not retryable
AUTH_INVALID_CREDENTIALS, // API key invalid / token expired / certificate rejected
// Platform errors — retryable
PLATFORM_RATE_LIMITED, // HTTP 429 — back off and retry
PLATFORM_MAINTENANCE, // HTTP 503 — platform in scheduled maintenance
PLATFORM_INTERNAL_ERROR, // HTTP 5xx — transient platform error
// Network errors — retryable
NETWORK_TIMEOUT, // Connection or read timeout
NETWORK_UNREACHABLE, // DNS resolution failure or TCP refused
// Implementation status — not retryable
NOT_IMPLEMENTED, // Adapter is in STUB lifecycle state
UNKNOWN, // Unmapped error; always log rawPayload for triage
}
```
**Mapping rule for new adapters:** Every HTTP status code the platform can return MUST
have a mapping to an `AdapterErrorCode`. Use `UNKNOWN` only as a catch-all, never as
the primary mapping for a known status code. See `StorecoveErrorMapper` (lines 469–516
in `StorecoveHrFiskEInvoiceAdapter.kt`) as the reference pattern.
### 2.4 AdapterConfig — DB-Level Feature Flag
Every adapter is gated by an `AdapterConfig` row. An adapter MUST NOT execute any
network call if its `AdapterConfig.enabled = false`. This allows disabling a broken
adapter without redeployment.
```sql
-- V20__adapter_config.sql (Phase 1H — during Phase 2A window)
CREATE TABLE adapter_config (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
market VARCHAR(8) NOT NULL,
-- TaxJurisdiction enum value: 'HR', 'RS', 'BA_FED', 'BA_RS'
adapter_type VARCHAR(32) NOT NULL,
-- Matches the 7 categories: 'EINVOICE', 'COMPANY_REGISTRY',
-- 'BANK_STATEMENT', 'EXCHANGE_RATE', 'TAX_FILING',
-- 'FISCAL_DEVICE', 'QES_SIGNING'
enabled BOOLEAN NOT NULL DEFAULT FALSE,
reason TEXT,
-- Why disabled, e.g. "MC #8675 pending — Storecove account not activated"
-- Required when enabled=false.
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_by TEXT NOT NULL DEFAULT 'system', -- MC task ID or admin user
CONSTRAINT pk_adapter_config UNIQUE (market, adapter_type)
);
-- Seed: all adapters start disabled
INSERT INTO adapter_config (market, adapter_type, enabled, reason, updated_by)
VALUES
('HR', 'EINVOICE', false, 'MC #8675 — Storecove account pending', 'MC-100362'),
('HR', 'COMPANY_REGISTRY', false, 'Not implemented — Phase 1S', 'MC-100362'),
('HR', 'BANK_STATEMENT', false, 'Tok AISP integration pending', 'MC-100362'),
('HR', 'EXCHANGE_RATE', false, 'ECB feed not configured', 'MC-100362'),
('HR', 'TAX_FILING', false, 'ePorezna integration Phase 2 scope', 'MC-100362'),
('HR', 'FISCAL_DEVICE', false, 'Fiskalizacija cert not configured', 'MC-100362'),
('HR', 'QES_SIGNING', false, 'FINA QES Phase 2 scope', 'MC-100362'),
('RS', 'EINVOICE', false, 'SEF adapter Phase 1S scope', 'MC-100362'),
('BA_FED', 'EINVOICE', false, 'CPF platform TBD ~2027', 'MC-100362'),
('BA_RS', 'EINVOICE', false, 'UINO platform TBD', 'MC-100362');
-- (Remaining BA/RS adapter rows follow same pattern)
-- Admin can enable without redeploy:
-- UPDATE adapter_config SET enabled = true, reason = NULL, updated_by = 'MC-8675-DONE'
-- WHERE market = 'HR' AND adapter_type = 'EINVOICE';
```
**Kotlin enforcement pattern:**
```kotlin
// In the adapter registry (apps/api/src/main/kotlin/no/alai/bilko/adapter/AdapterRegistry.kt)
fun requireEnabled(market: TaxJurisdiction, adapterType: String) {
val config = adapterConfigRepository.find(market, adapterType)
?: throw AdapterException(
code = AdapterErrorCode.NOT_IMPLEMENTED,
market = market,
retryable = false,
rawPayload = "",
message = "No AdapterConfig row for ($market, $adapterType) — run Flyway V20"
)
if (!config.enabled) {
throw AdapterException(
code = AdapterErrorCode.NOT_IMPLEMENTED,
market = market,
retryable = false,
rawPayload = "",
message = "Adapter ($market, $adapterType) is disabled: ${config.reason}"
)
}
}
```
### 2.5 Lifecycle States and Transition Criteria
Formalised from `AdapterLifecycleState` enum in `EInvoiceTypes.kt` lines 22–26,
and from ADR-016 §2.3. Applies to ALL adapter categories.
```
STUB ──────────────────► SANDBOX_VERIFIED ──────────────────► PRODUCTION
```
**STUB** (initial state for all adapters):
- Compiles and registers successfully
- All network methods throw `AdapterException(code=NOT_IMPLEMENTED)`
- `serialize()` / read-only operations may work (e.g., HR serialize works in STUB)
- `AdapterConfig.enabled` is `false`
**SANDBOX_VERIFIED** transition criteria (all must be true):
- Minimum 5 distinct happy-path test cases pass against the real sandbox platform
(not mocked — real submission IDs, real response payloads)
- All test case submission IDs are archived in BookStack as evidence
- Error mapping verified: at least HTTP 400, 401, 409, 429, 503, 5xx all produce
correct `AdapterErrorCode` values (not `UNKNOWN`)
- Proveo sign-off with evidence file path in MC task
- `AdapterConfig.enabled` can be set to `true` after this point
**PRODUCTION** transition criteria (all must be true):
- `SANDBOX_VERIFIED` already achieved
- Securion review of adapter error handling, PII sanitization, and idempotency key
implementation — no critical findings
- 30 consecutive days on stage Cloud Run with:
- Zero `PLATFORM_INTERNAL_ERROR` alerts
- Zero cross-market routing errors
- `bilko_integration_request_total{status="error"}` < 1% of total requests
- CEO approval for production activation
- `AdapterConfig.enabled = true` in production DB (separate row from stage DB)
### 2.6 Secret Taxonomy
Runtime secrets follow the pattern `Bilko/{env}/{market}/{secret-name}`.
**Env first, not market first.** This ensures that all production secrets are under
`Bilko/production/` and can be granted/revoked as a unit for environment promotion.
```
Bilko/
production/
HR/
STORECOVE_API_KEY
STORECOVE_LEGAL_ENTITY_ID
FINA_QES_CERTIFICATE (Phase 2)
EPOREZNA_CLIENT_SECRET (Phase 2)
RS/
SEF_API_KEY (Phase 1S)
LPFR_DEVICE_CERT (Phase 2)
BA_FED/
CPF_API_KEY (Phase 1B — pending platform launch)
BA_RS/
UINO_API_KEY (Phase 1B — pending platform launch)
stage/
HR/
STORECOVE_API_KEY
STORECOVE_LEGAL_ENTITY_ID
RS/
SEF_API_KEY
...
local/
HR/
STORECOVE_API_KEY (developer sandbox credentials only)
...
```
**Secret resolution hierarchy:**
1. Runtime: GCP Secret Manager (current) — accessed via `SecretResolver` interface
2. Break-glass: Vaultwarden (`vault.basicconsulting.no`) — human access only, NOT runtime source
3. Local dev: `.env.local` file (`.gitignore`'d) — NEVER committed
```kotlin
// apps/api/src/main/kotlin/no/alai/bilko/adapter/SecretResolver.kt
/**
* Abstracts secret retrieval behind a testable interface.
*
* Production implementation: GCP Secret Manager.
* Test implementation: environment variables / in-memory map.
*
* Secret path convention: Bilko/{env}/{market}/{secret-name}
*/
interface SecretResolver {
/**
* Resolves a secret value by its canonical path.
*
* @param path e.g. "Bilko/production/HR/STORECOVE_API_KEY"
* @return Secret value, or null if not found.
* @throws AdapterException(AUTH_INVALID_CREDENTIALS) if path exists but value is empty/blank.
*/
fun resolve(path: String): String?
/**
* Convenience method: builds canonical path and resolves.
* @param env "production" | "stage" | "local"
* @param market TaxJurisdiction enum value as string
* @param secretName The specific secret name
*/
fun resolve(env: String, market: String, secretName: String): String? =
resolve("Bilko/$env/$market/$secretName")
}
```
**Vaultwarden is NOT the runtime secret source.** Vaultwarden is the human break-glass
vault for emergency access. Do not write Kotlin code that reads from Vaultwarden at
runtime. GCP Secret Manager is the runtime source.
### 2.7 Observability Mandate
Every adapter MUST emit the following for every network call:
**Structured log line (one per call):**
```
level=INFO market=HR integration=EINVOICE env=production org_id=<uuid>
action=submit status=SUCCESS duration_ms=234 submission_id=<guid>
```
Required fields: `market`, `integration`, `env`, `org_id`. Optional but recommended:
`duration_ms`, `submission_id`, `attempt` (for retries).
**NEVER log:**
- OIB, PIB, JIB (tax IDs)
- IBAN
- `document_data` (invoice XML body)
- `api_key`, `api_secret`
Use `sanitizeForLog()` (pattern from `StorecoveHrFiskEInvoiceAdapter.kt` lines 24–59)
before any log write that touches a response body.
**Prometheus metrics (one counter per adapter):**
```kotlin
// apps/api/src/main/kotlin/no/alai/bilko/adapter/AdapterMetrics.kt
/**
* Prometheus counter for all adapter network calls.
*
* Labels: market, integration, status (SUCCESS | ERROR | NOT_IMPLEMENTED | TIMEOUT)
*
* Example PromQL for HR e-invoice error rate:
* rate(bilko_integration_request_total{market="HR",integration="EINVOICE",status="ERROR"}[5m])
* /
* rate(bilko_integration_request_total{market="HR",integration="EINVOICE"}[5m])
*/
// bilko_integration_request_total{market, integration, status}
// bilko_integration_request_duration_seconds{market, integration, status}
```
Per-(market, integration) alert rule:
- Error rate > 10% over 5 minutes: PAGE (PagerDuty or Slack alert)
- Error rate > 25% over 1 minute: CRITICAL (adapter auto-disabled via `AdapterConfig`)
### 2.8 Adapter Versioning
Each adapter declares `val adapterVersion: String` (semantic version, e.g., `"1.0.0"`).
The corresponding `CountryPlugin` implementation declares the minimum adapter version
it requires:
```kotlin
// In PluginHR:
companion object {
const val MIN_EINVOICE_ADAPTER_VERSION = "1.0.0"
}
// Startup check in DI.kt:
val adapter = StorecoveHrFiskEInvoiceAdapter()
require(semVer(adapter.adapterVersion) >= semVer(PluginHR.MIN_EINVOICE_ADAPTER_VERSION)) {
"PluginHR requires EInvoiceAdapter >= ${PluginHR.MIN_EINVOICE_ADAPTER_VERSION}, got ${adapter.adapterVersion}"
}
```
Adapters are versioned independently of the `CountryPlugin`. Breaking changes to
an adapter interface (e.g., new required parameter in `submit()`) require a major
version bump and a coordinated plugin + adapter update.
### 2.9 Idempotency Requirements
**All submit-type methods in all adapters MUST include an idempotency key.**
The idempotency key format is adapter-specific, but the value MUST be derived
deterministically from the invoice or entity content — never a random UUID.
| Adapter | Method | Idempotency key derivation |
| -------------------- | ---------- | -------------------------------------------------------------------- |
| EInvoiceAdapter (HR) | `submit()` | `SHA-256(invoice.id + invoice.invoiceNumber)` — matches Storecove D5 |
| EInvoiceAdapter (RS) | `submit()` | `SHA-256(invoice.id + invoice.invoiceNumber)` (same pattern) |
| TaxFilingAdapter | `submit()` | `SHA-256(filing.periodStart + filing.periodEnd + org.taxId)` |
| QESSigningAdapter | `sign()` | `SHA-256(document.contentHash + signer.taxId)` |
**Rationale:** A network timeout after the platform receives the request but before
the response arrives will cause the client to retry. Without idempotency, this creates
a duplicate document. Storecove returns HTTP 409 on duplicate `document_id` (D2 in
`StorecovePayloadBuilder.wrap()` lines 420–436) — the pattern must be replicated.
---
## 3. Implementation Path
| Phase | Task | Deliverable | Status |
| -------- | ------------------------------------------------------------ | --------------------------------------------------------------- | ------------------- |
| Phase 0' | This ADR written to disk | `ADR-019-INTEGRATION-ADAPTER-REGISTRY.md` | DONE |
| Phase 1H | `AdapterException` + `AdapterErrorCode` formalized | `adapter/AdapterException.kt` (already exists — verify package) | Verify existing |
| Phase 1H | `AdapterConfig` Flyway migration (V20) | `V20__adapter_config.sql` | BLOCKED BY Phase 2A |
| Phase 1H | `SecretResolver` interface + GCP impl | `adapter/SecretResolver.kt` + `GcpSecretResolver.kt` | Phase 1H.4+ |
| Phase 1H | `AdapterRegistry` + `requireEnabled()` check | `adapter/AdapterRegistry.kt` | Phase 1H.4+ |
| Phase 1H | Prometheus metrics wired in `StorecoveHrFiskEInvoiceAdapter` | `StorecoveMetrics.kt` (skeleton exists) | Phase 1H.2+ |
| Phase 1S | SEF RS EInvoiceAdapter | `country/rs/SefRsEInvoiceAdapter.kt` | Post-HR GA |
| Phase 1B | CPF BA-FED + UINO BA-RS stubs | `country/ba/Cpf*`, `country/ba/Uino*` | Post-RS GA |
---
## 4. Consequences
### 4.1 Positive
- **Zero platform exception leakage.** `AdapterException` as the only crossing type means
core services have one error handler for all 7 × 4 = 28 potential adapter instances.
- **Hot disable without redeploy.** `AdapterConfig.enabled = false` disables a broken
adapter in < 1 minute (DB write). No restart required. Incident response time drops
from minutes (redeploy) to seconds (DB update).
- **Observability from day one.** Every adapter emits standardized metrics. Error rate
alerts fire before users report issues.
- **Secret hygiene.** Env-first taxonomy (`Bilko/{env}/{market}/{secret}`) makes
environment promotion (stage → production) a structured operation, not an ad-hoc
copy. Break-glass access is separated from runtime access.
### 4.2 Negative
- **Adapter scaffolding cost.** Each new adapter requires implementing the full
interface contract, `AdapterConfig` rows, `SecretResolver` wiring, and Prometheus
metrics. Estimate: 1–2 days for a new adapter in STUB state.
- **AdapterConfig is a deployment dependency.** The application fails at startup if
`adapter_config` rows do not exist. Flyway V20 must run before the application
version that adds `requireEnabled()` checks.
### 4.3 Risks
- **`AdapterConfig` DB unavailable.** If the database is unreachable, `requireEnabled()`
fails, blocking all adapters. Mitigation: cache `AdapterConfig` in-memory at startup
with a TTL of 5 minutes. Use cached state if DB is unreachable.
- **Metrics cardinality.** High `org_id` cardinality in metrics labels would cause
Prometheus memory issues. The observability mandate specifies `org_id` in log lines,
NOT in Prometheus labels. Labels are `market`, `integration`, `status` only — bounded
cardinality.
---
## 5. References
| Reference | Path | Lines |
| ------------------------------------------------ | ------------------------------------------------------------------------------------- | ------- |
| `AdapterLifecycleState` enum (on disk) | `apps/api/src/main/kotlin/no/alai/bilko/einvoice/EInvoiceTypes.kt` | 22–26 |
| `StorecoveErrorMapper` (error mapping reference) | `apps/api/src/main/kotlin/no/alai/bilko/country/hr/StorecoveHrFiskEInvoiceAdapter.kt` | 469–516 |
| `StorecoveMetrics` (metrics reference) | `apps/api/src/main/kotlin/no/alai/bilko/country/hr/StorecoveHrFiskEInvoiceAdapter.kt` | 537–540 |
| PII sanitization reference | `apps/api/src/main/kotlin/no/alai/bilko/country/hr/StorecoveHrFiskEInvoiceAdapter.kt` | 24–59 |
| Idempotency key (D5) reference | `apps/api/src/main/kotlin/no/alai/bilko/country/hr/StorecoveHrFiskEInvoiceAdapter.kt` | 94–98 |
| `StorecovePayloadBuilder` (D2 document_id) | `apps/api/src/main/kotlin/no/alai/bilko/country/hr/StorecoveHrFiskEInvoiceAdapter.kt` | 420–436 |
| ADR-016 §2.3 (lifecycle states — EInvoice) | `docs/architecture/ADR-016-EINVOICE-ADAPTER.md` | §2.3 |
| ADR-015 §2.4 (DI registration pattern) | `docs/architecture/ADR-015-FOUR-JURISDICTION-PLUGIN.md` | §2.4 |
| Plan v3 §6 Task 0'4 acceptance criteria | `~/system/specs/bilko-multi-market-architecture-plan-v3-2026-05-11.md` | 279–290 |
---
## 6. Approval
**Status:** Accepted
**Unblocks:**
- Phase 1H: `AdapterConfig` Flyway V20 migration
- Phase 1H: `SecretResolver` GCP implementation
- Phase 1H: Prometheus metrics wiring in `StorecoveHrFiskEInvoiceAdapter`
- Phase 1S: SEF RS adapter scaffolding (knows the contract to implement against)
- Phase 1B: CPF/UINO BA adapter stubs
| Role | Sign | Date |
| -------------------------------- | ------------------------------------- | ---------- |
| Architecture Lead (Petter Graff) | Signed | 2026-05-11 |
| CEO (Alem Bašić) | Not required for registry pattern ADR | — |
---
## 7. Document History
| Date | Author | Change |
| ---------- | ------------ | ------------------------------------------------- |
| 2026-05-11 | Petter Graff | Initial — Phase 0' ADR consolidation (MC #100362) |