Skip to main content

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:

KindMinted whenOwner of the condition
control_disableda flag is deliberately offthe deployment (a policy choice)
dimension_unavailablethe control is ON and its substrate FAILEDoperations (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_controlAMLR 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 determinedabsence_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:

DimensionWhat it isStatutory?
sanctioned_ownership_controlAMLR Art. 20(1)(d) — the EU 50%-rule over the ownership graphyes
entity_networkconnected-entity riskno — a way of finding things
cross_case_patternsstructural motifsno — a way of finding things
knowledge_graphthe substrate the other three readno — not a determination
licence_verificationAMLR Art. 34 risk factorno — 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) is False for every d, 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 to escalate_reasons. escalate wins 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 clearinsufficient_data. No path exists from escalate to anything weaker.
  • It fabricates nothing. No Finding is 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=True without actually running Neo4j will 409 every approval. §5's warning is the mitigation; the honest fix is to set the flag False, which is a recorded decision.
  • blocking on a coverage record stops being a constant and becomes a policy output. Two existing assertions that read blocking is False over 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 to BLOCKED there is a tracked follow-up, not a silent omission.
  • licence_verification is 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 clear verdict 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=False is 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_unavailable to a non-blocking control_disabled without 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_clear is 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_OWNERSHIP from STATUTORY_DETERMINATION_DIMENSIONS restores ADR-0161's behaviour exactly, and a standing test fails when it is removed — so the reversal cannot be accidental.