ADR-0170: A statutory dimension_unavailable withholds clear
Date: 2026-08-01
Status: Accepted
Deciders: Adrian (Soft4U BV) — owner decision of 2026-08-01, option B; Claude Opus 5 (implementation); Codex (round-2 review on PR #955, which identified that ADR-0161 §6's axis was wrong and recorded it as §13 rather than changing a verdict state inside a PR already under review)
Amends: ADR-0161 §3 and §6. ADR-0161 stays Accepted; §3's "disclosed, not blocking" holds for every control_disabled record on every dimension, and this ADR carves out exactly one case from it. §6's distinguishing principle is replaced — see §2.
Implements: issue #961, pre-recorded as ADR-0161 §13.
Related: ADR-0067 (fail-closed "not assessed" contract), ADR-0113 (per-case compliance verdict), ADR-0125 (insufficient-data approval gate), ADR-0127 (the Art. 20(1)(d) test itself), ADR-0070 (four-eyes), ADR-0158 (never-suppress on substrate loss).
Context
ADR-0161 introduced two coverage-record kinds and shipped both as disclosed-not-blocking:
| Kind | Minted when | Owner of the condition |
|---|---|---|
control_disabled | a flag is deliberately off | the deployment (a policy choice) |
dimension_unavailable | the control is ON and its substrate FAILED | operations (an outage) |
The blanket non-blocking rule was argued from a real constraint: roughly 15
dark-launched features would otherwise 409 every approval in a deployment that
never enabled them, permanently, on something the officer cannot act on. A gate that
can never be satisfied gets override-clicked by reflex until the override stops meaning
anything. That argument is sound — and it is an argument about control_disabled.
The gap it left
With neo4j_enabled defaulting to True (ADR-0161 §5), an unreachable Neo4j mints
dimension_unavailable on four dimensions, one of which is
sanctioned_ownership_control — AMLR Art. 20(1)(d), the test of whether a
sanctioned person controls or holds >50% of the customer. Under the blanket rule, a
case whose Art. 20(1)(d) test never ran still resolved to clear.
That is the ADR-0067 false clear, on a named statutory duty. A sanctioned-ownership test that never ran is not a sanctioned-ownership test that passed.
ADR-0161 §6 reasoned on the wrong axis
§6 distinguished a blocking country_capability_gap from a non-blocking
coverage record on the grounds that "a country gap is case-actionable, a flag gap is
not" — an officer can route a case to a supported country, but cannot change the
deployment's configuration.
country_capability_gap does not block because it is actionable. It blocks because
the field applies to the entity and was not determined — absence_vocabulary.py:86-90
says exactly that, and refuses to type it NOT_APPLICABLE for that reason. Neo4j being
down is the same situation: Art. 20(1)(d) applies to this customer, and it was not
determined.
Decision
1. A dimension_unavailable record on a STATUTORY dimension withholds clear
Where no prior determination exists for the case, such a record contributes a
gap_reason, so the ADR-0113 verdict resolves to insufficient_data rather than
clear. No new verdict state is invented; the existing precedence
escalate > insufficient_data > clear carries it.
control_disabled records — of any dimension, including this one — stay disclosed
and non-blocking, unchanged. ADR-0161 §3's ~15-feature argument is untouched, because
dimension_unavailable is minted only when a control is ON and its substrate FAILED:
in a healthy deployment it fires never, and in an outage it fires until the outage
ends. That is an outage signal with a natural end, not permanent noise.
2. The correct axis is "does the law require this determination for this customer?"
Not "can the officer act on it?". §6 of ADR-0161 is replaced by this sentence.
Under this axis, exactly one dimension qualifies today:
| Dimension | What it is | Statutory? |
|---|---|---|
sanctioned_ownership_control | AMLR Art. 20(1)(d) — the EU 50%-rule over the ownership graph | yes |
entity_network | connected-entity risk | no — a way of finding things |
cross_case_patterns | structural motifs | no — a way of finding things |
knowledge_graph | the substrate the other three read | no — not a determination |
licence_verification | AMLR Art. 34 risk factor | no — a factor to consider, not a determination the regulation requires be recorded |
An unreachable Neo4j disarms four dimensions. Only one of them is a named statutory duty; the other three are how the platform finds things. Blocking on all four would be the false-alarm rate that gets the control switched off.
3. The policy is a table, not a special case
control_coverage.STATUTORY_DETERMINATION_DIMENSIONS is a frozenset, and
blocks_clear(dimension, coverage_kind) is the whole policy. Both builders stamp
blocking from it, so the record carries the policy as data and every consumer
reads one answer. A future dimension joins by being named in the set, not by someone
remembering to add a branch in the verdict.
Two properties are asserted in both directions by standing tests, because a policy table that can be emptied without a failure is decorative:
- emptying the set fails
test_the_statutory_set_is_not_empty; blocks_clear(d, control_disabled)isFalsefor everyd, including the statutory one.
The consumer re-derives blocking from the record's own dimension/coverage_kind
rather than trusting the stored boolean. A record persisted before this policy existed,
or hand-edited in a JSONB blob, is judged by the current policy — a stale boolean cannot
buy an exemption from a statutory block.
4. A prior determination disarms the block
PR #955 fixed _apply_sanctioned_ownership_result so a coverage record can never
replace a determination on the investigation result: a transient outage must not
delete an established CRITICAL (ADR-0158's invariant, one mechanism over). The retained
determination therefore sits at investigation_result["sanctioned_ownership_control"]
while the outage record lands, correctly, alongside it in control_coverage.
That is precisely the shape that would otherwise re-open a closed determination. So
has_prior_determination is the verdict-side half of the same invariant: this ADR binds
only the case where the graph was never reachable — genuinely never assessed.
"Real determination" is read structurally (a non-empty dict at the dimension's key that is not itself a coverage record). Fail-closed: anything unrecognised reads as no prior determination, so the block stands. The failure direction is more scrutiny.
5. A loud startup warning, as part of the change
neo4j_enabled defaults to True and the Neo4j driver connects lazily, so a deployment
that simply does not run Neo4j looks healthy at boot and then withholds clear on every
case with no visible cause. That is a config trap.
graph_service.warn_if_graph_unreachable() runs on both boot paths (main.py lifespan
and worker.py) and logs at ERROR under a greppable marker,
graph.unreachable_at_boot, naming the consequence (every case → insufficient_data
on Art. 20(1)(d)) and the fix (start Neo4j, or set NEO4J_ENABLED=false — which is
recorded as control_disabled and does not withhold clear).
It is a diagnostic: it never raises, never fails startup, and deliberately does not auto-disable the flag. Silently reverting to a known-off control is the failure ADR-0161 §5 exists to prevent. A control whose failure mode is unexplained is a control that gets switched off.
6. Not a dead end for the officer
insufficient_data already has an audited escape: ADR-0125's override_insufficient_data,
reason-required and routed through ADR-0070 four-eyes. The outcome is a recorded
decision by two people, not a stop.
Never-suppress and monotonicity
Both hold by construction, not by convention:
- The block is appended to
gap_reasons, never toescalate_reasons.escalatewins the precedence, so a CRITICAL finding on a case with an unreachable graph still escalates — a hit on partial data is still a hit (ADR-0067). Withholding a green can never downgrade a hit. - It can only add scrutiny: the sole reachable transition is
clear→insufficient_data. No path exists fromescalateto anything weaker. - It fabricates nothing. No
Findingis minted, no severity is set, no escalator fires, no open-finding count moves. "Not assessed" is not "found something" — the reason string says so explicitly.
Consequences
Positive
- The Art. 20(1)(d) false clear is closed on the one dimension the regulation names.
- ADR-0161 §6's principle is corrected on the register rather than left as a footnote.
- The blocking policy is one reviewable declaration instead of an implicit convention.
- An unreachable graph is now loud at boot, not discovered case by case.
Negative
- A Neo4j outage now gates approvals for the duration of the outage. This is intended, bounded by the outage, and escapable via the audited four-eyes override — but it is a real operational cost that did not exist before.
- A deployment that runs
neo4j_enabled=Truewithout actually running Neo4j will 409 every approval. §5's warning is the mitigation; the honest fix is to set the flagFalse, which is a recorded decision. blockingon a coverage record stops being a constant and becomes a policy output. Two existing assertions that readblocking is Falseover a mixed record set were narrowed to assert the per-dimension split in both directions.
Neutral
- No migration, no new flag, no schema change. The record already carried
blocking; only its value moved, on one(dimension, kind)pair. - The ADR-0132 assistant readiness gate is deliberately not changed: it already
floors every coverage record at
REVIEW_REQUIRED(ADR-0161 §12), which is scrutiny-adding and consistent. Promoting a statutory blocker toBLOCKEDthere is a tracked follow-up, not a silent omission. licence_verificationis deliberately excluded. AMLR Art. 34 is a risk factor to be taken into account, not a per-customer determination the regulation requires be recorded; including it would widen the block on an argument this ADR cannot make.
Alternatives Considered
Alternative A: block on every dimension_unavailable record
- Why rejected: an unreachable Neo4j disarms four dimensions, three of which are discovery aids. Blocking on all four triples the 409 surface for no statutory gain and is the false-alarm profile that gets a control switched off. The owner's decision names this explicitly as option B over option A.
Alternative B: keep the blanket non-blocking rule and rely on disclosure
- Why rejected: this is the shipped state, and it is the ADR-0067 false clear. A
clearverdict is consumed as a determination by the approval path, the case pack and the assistant; a disclosure line beside it does not stop any of them.
Alternative C: block on control_disabled too, for the statutory dimension
- Why rejected:
neo4j_enabled=Falseis a recorded, deliberate configuration. A deployment that legitimately runs without a graph would 409 every approval forever — ADR-0161 §3's argument, and it is correct. The distinction between a chosen absence and an outage is the point of having two record kinds; conflating them here would waste it.
Alternative D: auto-disable neo4j_enabled when the boot probe fails
- Why rejected: it converts an outage into a silent policy change, downgrading the
case from a blocking
dimension_unavailableto a non-blockingcontrol_disabledwithout anyone deciding that. That is exactly the "trade a known-off control for a silently-broken one" failure ADR-0161 §5 was written to prevent — inverted.
Decision context:
- Latency: none.
blocks_clearis a frozenset membership test; the verdict already collected the records, and this moves that collection earlier in the same function. - Dependency surface: none. No new packages, no new config, no new state.
- Reversibility: removing
DIMENSION_SANCTIONED_OWNERSHIPfromSTATUTORY_DETERMINATION_DIMENSIONSrestores ADR-0161's behaviour exactly, and a standing test fails when it is removed — so the reversal cannot be accidental.