Skip to main content

ADR-0166: Control-disabled coverage records — the governing-lane test

Date: 2026-08-01 Status: Accepted Deciders: Adrian (Soft4U BV), Claude Opus 5 (analysis + implementation), Codex (review findings on PR #955 — round 1 §7-§10, which changed what §1 decides about destroying prior evidence and what §5's probe covers; round 2 §11-§13, which changed the evidentiary standard for supersession and corrected §3's readiness claim and §6's distinguishing principle) Amends: ADR-0022 (Neo4j knowledge graph with sequential ETL pipeline). ADR-0022 stays Accepted; §5 below reverses exactly one line of it — the neo4j_enabled=False default and the silent skip that went with it. Its ETL design, ontology and graph-model decisions are untouched. Amended by: issue #961 — owner decision of 2026-08-01, scheduled and not yet implemented: a dimension_unavailable record on the sanctioned_ownership dimension with no prior determination will WITHHOLD CLEAR (verdict insufficient_data), and §6's distinguishing principle is restated on the correct axis. See §13. Everything in this ADR is what shipped in PR #955; §13 records what is already known to change next.

Context

ADR-0067 established the fail-closed contract: a check that did not run must never read as a check that ran and found nothing. Its Decision.1 trigger list names five conditions — unconfigured source, empty list, exception, missing tenant context, absent data. A deliberately disabled control is not on that list, and the gap has been filled inconsistently ever since.

Six conditions collapse onto "no result"

ConditionWhat it meansTyped vocabulary today
Flag deliberately off"we chose not to run this"none exists
Unconfigured (no credential)"we cannot run this here"Outcome.SOURCE_UNAVAILABLE
Unavailable (timeout / error / breaker)"we tried and could not"Outcome.SOURCE_UNAVAILABLE
Mock mode"this is not a real answer"Outcome.INCONCLUSIVE
Not applicable (wrong vertical / country)"this does not apply"Outcome.NOT_APPLICABLE (zero producers)
Genuinely empty"we ran it, there is nothing"NOT_FOUND / VERIFIED_CLEAR

Six distinct facts, three distinguishable states in the type system, and the one this ADR is about — flag-off — has no representation at all.

The live contradiction

The same flag has two opposite answers in the same release:

  • immediate_designation_rescreen_service.py:120-140 publishes ownership_dimension_evaluated=False when sanctioned_ownership_control_enabled is off. Its docstring names the defect: "a 'claim vs check' contradiction… an unrun check must never read as assessed."
  • activities.py:4880 wraps the entire AMLR Art. 20(1)(d) computation — and its own fail-closed gap emitter — inside if settings.sanctioned_ownership_control_enabled:. With the flag off the case record says nothing at all about the ownership dimension.

One of those is wrong today, on any policy. This is a defect, not only a design gap.

Why "classify each flag by kind" cannot work

The obvious resolution — annotate every flag as DARK-LAUNCH / OPTIONAL-SOURCE / DESTRUCTIVE-GUARD / OPS-CONFIG, and let the kind decide the emission — was investigated and rejected on evidence. Of the 87 booleans in config.py:

  • Six are simultaneously dark-launch and optional-source: exa_search_enabled, brightdata_serp_fallback_enabled, brightdata_social_enrichment_enabled, licence_discovery_enabled, deep_research_discovery_enabled, domain_footprint_enabled. A flag's kind would have to be a tuple, and the tuple still would not decide the emission.
  • A fifth kind exists that the taxonomy omits — control-enforcement-phase: rbac_enforcement_enabled, approval_insufficient_data_gate_enabled, approval_verification_gate_enabled, verification_gate_enabled. Flag-off there is neither a no-op nor a gap: it emits would_deny / would_block telemetry plus an immutable audit row, and allows.

Why a blanket gap is worse than the problem

Routing flag-off through build_screening_gap_finding emits category screening_error, which is in _ADVERSE_ERROR_CATEGORIES (compliance_verdict.py:174) → forces adverse-media coverage partialgap_reason (:838-842) → state="insufficient_data" (:874) → HTTP 409 on every approval (case_decisions.py:550-566) the moment ADR-0125 Phase 2 flips.

That is roughly 15 dark-launched features permanently blocking approval in every deployment that never enabled them. The failure mode is not the block — it is that a permanently stuck gate gets override-clicked by reflex until the override stops meaning anything. It would also print "adverse media not assessed" on cases where Tavily fully assessed it: ADR-0067 inverted into a different lie.

Decision

A control a deployment deliberately disabled did not fail — it was never in scope for the run. It emits no Finding and no screening_error, and the lane-level return stays byte-identical, preserving the dark-launch kill-switch guarantee. But it must not be silent either.

1. The discriminator is the governing lane, not the flag's kind

Where a disabled flag gates an entire assessment dimension with no other lane governing it, the run MUST write a typed coverage record. Where a governing primary lane still completed the dimension, it writes nothing.

The codebase already implements exactly this test, ten lines apart:

# adverse_media_agent.py:342-348
if not settings.exa_search_enabled:
return [], False # genuinely disabled — no contribution; Tavily governs
if not settings.exa_api_key:
return [], True # enabled but cannot run → data gap

So exa_search_enabled=False under a healthy Tavily marks nothing, because adverse media was assessed. sanctioned_ownership_control_enabled=False marks Art. 20(1)(d) not-assessed, because nothing else tests it.

2. The record shape

result["<dimension>_coverage"] = {
"status": "not_assessed",
"reason": "control_disabled:<flag_name>",
"degraded": False, # nothing failed — provider health is not the axis
...dimension-specific fields
}

degraded=False alongside status="not_assessed" is deliberate and already the codebase's mature pattern (licence_discovery_phase.py:179-183). degraded is a provider-health axis; status is a coverage axis. They are orthogonal.

3. Disclosed, not blocking

A control-disabled coverage record:

  • maps to Outcome.NOT_APPLICABLE — the weakest member of GAP_PRECEDENCE (claims.py:35-46), ranked below VERIFIED_CLEAR, so it never out-ranks a real determination;
  • stays out of _COVERAGE_STATE_CATEGORIES, so it never becomes a gap_reason, never becomes insufficient_data, and never 409-blocks an approval under ADR-0125;
  • floors the assistant readiness verdict at REVIEW_REQUIRED, never BLOCKED (dashboard_agent._derive_coverage_readiness_blockers_merge_wave1_readiness). This bullet held for one dimension only until §12; read it with §12.

An officer can never read its absence as a clear; a deliberately-staged feature can never block an approval.

4. The consumer obligation is half the decision

A producer-only rule reproduces the defect it fixes. licence_discovery_coverage and deep_research_coverage are written today and read by nobody — a coverage record no surface renders is indistinguishable from no record at all.

Therefore: every coverage record MUST reach the case pack and the officer verdict surface as an explicit "not assessed — control disabled", never as an absent line. A standing test asserts producer and consumer in both directions.

5. neo4j_enabled defaults to True

neo4j_enabled shipped False (config.py:570) and silently disarmed sanctioned- ownership, pattern detection and entity-network — and was absent from both lists in test_activation_flag_state.py, so nothing pinned it. An empty ownership graph and a proven-clean one produced the identical determination.

The default flips to True. This is a scrutiny-increasing flip, consistent with the ADR-0146 guardrail. It lands together with the coverage record, not before: with the flag on and Neo4j unreachable, the dimension must record not_assessed rather than read clean — otherwise the flip trades a known-off control for a silently-broken one.

6. Distinguishing this from country_capability_gap

AMENDED — the axis below is wrong, and was replaced by ADR-0170 §2 on 2026-08-01 (issue #961, owner decision, option B — now implemented, not merely scheduled). The corrected principle is: "does the law require this determination for this customer?", not "can the officer act on it?".

country_capability_gap does not block because it is actionable. It blocks because the field applies to the entity and was not determined — which is what absence_vocabulary.py:86-90 actually says. Under the corrected axis a dimension_unavailable record on the statutory sanctioned_ownership_control dimension (AMLR Art. 20(1)(d)), where no prior determination exists, WITHHOLDS clear. control_disabled records of every dimension, and dimension_unavailable on the three supporting graph dimensions, remain disclosed-not-blocking exactly as this section's conclusion describes — so §3's "~15 dark-launched features would 409 every approval" argument is untouched. See ADR-0170 §1-§3 and §13 below.

The paragraphs below are kept unedited so the record shows what was decided, not a retrofit of what was decided later.

country_capability_gap is also a deployment-capability gap, it is in _COVERAGE_STATE_CATEGORIES, and it therefore blocks — and absence_vocabulary.py:86-90 explicitly refuses to type it NOT_APPLICABLE because "the field DOES apply to the entity."

The distinguishing principle: a country gap is case-actionable, a flag gap is not. An officer can route a case to a supported country; an officer cannot change the deployment's configuration. A control that blocks on something the officer cannot act on is not a control, it is an obstacle.

Consequences

Positive

  • The live activities.py:4880 contradiction is fixed; the case record now says what was and was not evaluated.
  • Flag-off gains a representation in the type system it never had.
  • Two deployments of the same commit no longer produce silently different coverage.
  • neo4j_enabled=True puts the ownership dimension into the default path.

Negative

  • Coverage records are only as good as their consumers; wiring the case-pack and verdict surfaces is real work that the ADR makes obligatory rather than optional.
  • Outcome.NOT_APPLICABLE gains its first producers, so its precedence ranking is now load-bearing where it was previously theoretical.
  • A deployment that disables many controls will show many "not assessed" lines. That is the honest reading, but it is more surface than a silent skip.
  • neo4j_enabled=True means a deployment without Neo4j now does work that fails and records a gap, rather than skipping cheaply. Cost is one failed connection per case.

Neutral

  • Zero of the 49 existing *flag_off* pinning tests break — they assert lane-level returns and key-absence, which stay byte-identical. Three gain a positive assertion.
  • The ro_onrc_service.py untyped "not configured" note is a credential defect, not a flag defect. It is fixed alongside but is not governed by this ADR.

Review remediation (Codex on PR #955)

Nine review findings, all upheld on verification. Four change what this ADR decides, not merely how it was coded, and are recorded here as §7-§10; the other five were engineering defects fixed without amending anything above (a two-value return on one ro_onrc_service branch that the three-value caller could not unpack; the bases the coverage records print missing from the sealed citation appendix; ADR-0166 absent from docs/adr/README.md; _latest_iteration reading the stale investigation_results where six other readers prefer investigation_results_override; mandatory=False on the RO credential gap).

Two of those five needed a judgement rather than a transcription, and the reasoning is recorded so a future reader can disagree with it:

  • RO credential gap severity. The PR set mandatory=False citing person_validation_agent, which really does use MEDIUM for the same provider and the same trigger. That precedent does not transfer: its screen is identity validation of directors already identified elsewhere, whereas BrightData is the ONLY source of RO directors, so an absent token means director identification AND the dependent PEP/sanctions screening did not run. Severity follows the consequence, not the cause — which is also what the branch directly above does (HIGH) and what build_screening_gap_finding documents.
  • _latest_iteration. The finding is correct but understates the defect: the divergence predates this PR and affects every input the verdict reads (findings, PEP coverage, product classification), not only the new coverage collector. Fixed at the root rather than special-cased for the collector, which would have put two different answers inside one function.

Nothing here reopens §2: degraded=False alongside status="not_assessed" stands — provider health and coverage remain orthogonal axes.

7. A not-assessed record must never DESTROY what was already known

The first draft let the substrate-unavailable record flow through _apply_sanctioned_ownership_result, which REPLACES the determination block and PRUNES every SOC-category finding before re-appending the ones the activity returned. An unavailable checkpoint returns an empty finding list — so a Neo4j outage at the post-network checkpoint deleted a CRITICAL sanctioned-ownership determination that the post-OSINT checkpoint had confirmed.

That inverts the ADR's own premise. §1 disclosed a not-assessed dimension; it did not say the disclosure must sit alongside prior evidence rather than on top of it. It does now: a coverage record never replaces a real determination, and the disclosure still reaches the case record through control_coverage. This is ADR-0158's invariant one mechanism over — that one defended a retained signal against a foreign SOURCE LABEL, this one defends it against SUBSTRATE LOSS. Monotonic: the branch can only retain.

8. §5's probe must gate EVERY graph-derived dimension, not one

§5 paired neo4j_enabled=True with a reachability probe so the flip could not trade a known-off control for a silently-broken one. Only the sanctioned-ownership precheck probed. GraphService.enabled is True against a dead server and GraphETL's _run_write/_run_query degrade to [], so the ETL "completed" with empty stats: no patterns found, an empty entity network. Three dimensions still read clean. The probe now gates the ETL itself and records all four graph-derived dimensions as dimension_unavailable — the §5 guarantee was written for the whole substrate and was only implemented for one of its consumers.

9. A record must be superseded by a real determination, not outlive it

merge_control_coverage could only add. Neo4j down at post-OSINT and up at post-network left the stale dimension_unavailable record telling the verdict and the case pack "not assessed" over a dimension that had since been assessed. Activities now return control_coverage_resolved — the dimensions the run actually produced a result for — and only that clears a record. Silence never does; this is supersession by evidence, not suppression.

10. The heading is derived from coverage_kind, never assumed

Both the officer surface and the sealed PDF captioned every record "control disabled in this deployment". For a dimension_unavailable entry that misattributes an operational outage to deployment policy, contradicting the record's own coverage_kind and degraded fields — and the two facts have different owners. Both headings are now computed from the records.

All four are pinned by mutation-tested guards: each was broken, the named test was watched to fail, and restored (16 mutations, 16 caught, 0 inert).

Review remediation — round 2 (Codex on PR #955)

Four findings against the round-1 head. Three of them are about this document rather than the code, and all three are the same failure applied to a decision record: the ADR claimed a property it did not have. That is the claim-vs-check class the codebase already treats as a defect class, so it is treated as one here.

11. Supersession needs EVIDENCE of success, not evidence of a return

§9 made a produced result the only thing that clears a not-assessed record. It then took "the ETL returned a dict" as the produced result. GraphService._run_write degrades every failure to [] without raising, so read-only credentials, a timeout after the §8 probe, or an outage mid-run all produce status="completed", errors=[] and an all-zero stats — and that shape cleared the dimension_unavailable records the same run's earlier checkpoint had written. The false-clean read came back one layer in from where §9 closed it. The two skipped early returns (no_registration_number, neo4j_disabled) are dicts too, and were treated the same way.

Resolution now rests on positive witnesses, never on the absence of a failure:

  • graph_writes_witnessed — at least one Neo4j write came back with a record. Derived only from the two counters that increment under an explicit if result: (investigations, companies); most other counters increment unconditionally after the await and are TRUE against a dead server, so a standing source-level test pins the witness set against widening.
  • pattern_detection_assessed — cross-case detection actually ran. Needed separately because the step is not invoked at all for a case with no registration number, and its None return means both "found nothing" and "never ran" — the same sentinel ambiguity §1 fixed for its coverage record.
  • A second GraphService.probe() after the ETL. The §8 probe proves the substrate answered one read at the START of the run; a mid-run outage is otherwise silent. A failed post-probe earns the same dimension_unavailable records the pre-probe earns — the outage is disclosed, not merely unresolved.

Honest residual, recorded rather than papered over: a substrate that serves writes but fails the pattern QUERIES (which also degrade to []) still resolves the pattern dimension. The witness available at this seam is substrate-level; a per-query witness would have to come from GraphService itself. Second residual: a run that is reachable but witnesses no write resolves nothing and mints nothing — the summary cannot distinguish "read-only credentials" from "no write was attempted for this case", and attributing it to an outage would be the §10 mistake in the other direction.

12. The declared readiness floor now applies to every record, not one

§3 declared that a coverage record "floors the assistant readiness verdict at REVIEW_REQUIRED". That was true of exactly one dimension, and by accident: _sanctioned_ownership_precheck writes its record to investigation["sanctioned_ownership_control"], where summarize_sanctioned_ownership reads it and the ADR-0132 Wave-1 gate floors on the resulting not_assessed. Nothing carried a disabled licence verification, pattern detection, knowledge graph or entity network into _derive_audit_readiness_full at all, so a case with a complete CDD register could read APPROVE_SAFE with four dimensions never assessed.

_derive_coverage_readiness_blockers now turns every record into its own non-downgradable REVIEW_REQUIRED floor, entering the same MAX as the Wave-1 blockers. Sourced by discovery (collect_control_coverage over the latest investigation result, the same read compliance_verdict uses), so a future producer is carried without anyone remembering to wire it.

REVIEW_REQUIRED, never BLOCKED, is unchanged and load-bearing: readiness is the assistant's advisory answer to "can I approve?", not the ADR-0125 approval gate. In a healthy default deployment — every graph/pattern/licence/ownership control on — zero records exist, so this fires only when something genuinely was not assessed.

An ADR that overstates its own control is worse than one that claims less, because the reader stops checking. That is why this was fixed in the code rather than by softening §3.

13. §6's distinguishing principle is already scheduled to be amended

Now implementedADR-0170, 2026-08-01. Everything this section predicted shipped as described; ADR-0170 §2 carries the corrected axis and §4 adds the prior-determination disarm that binds the change to the genuinely-never-assessed case. This section is left as written — the record should show that the change was seen and scheduled before it was made.

§6 separated 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". Issue #961 (owner decision, 2026-08-01) records that this is the wrong axis. country_capability_gap blocks because the field applies to the entity and was not determinedabsence_vocabulary.py:86-90 says exactly that — and Neo4j being down is the same situation for AMLR Art. 20(1)(d). The correct axis is "does the law require this determination for this customer?", not "can the officer act on it?".

Under that reading, #961 will make a dimension_unavailable record on the sanctioned_ownership dimension, where no prior determination exists, withhold clear. The three supporting graph dimensions and all control_disabled records of any dimension stay disclosed-not-blocking, so §3's "~15 dark-launched features would 409 every approval" argument is untouched — dimension_unavailable is minted only when a control is ON and its substrate FAILED, which in a healthy deployment is never.

This was recorded here unimplemented, deliberately. #961 moves a verdict state and is Calibration-Review-gated; folding it into PR #955 would have invalidated the review that PR already had. The register must show a decision that is known to be changing, not a decision that looks settled. It shipped separately as ADR-0170.

Round 2 adds 13 mutation-tested guards (13 broken, 13 named tests watched to fail, 13 restored, 0 inert), two of which mutate test DATA rather than code: the healthy-ETL stub losing its write witness, and the coverage record demoted off the latest investigation result.

Alternatives Considered

Alternative 1: Classify each flag by KIND; dark-launch = no-op, optional-source = gap

  • Annotate every boolean with a kind and let the kind decide the emission.
  • Why rejected: undecidable exactly where it matters. Six flags are simultaneously dark-launch and optional-source, and a fifth kind (control-enforcement-phase) exists that the taxonomy does not contain. It also leaves the live activities.py:4880 defect untouched.

Alternative 2: Blanket — every flag-off emits a typed gap

  • Route all flag-off paths through build_screening_gap_finding.
  • Why rejected: mechanically produces HTTP 409 on every approval for ~15 features under ADR-0125 Phase 2, breaks ~49 pinning tests, and prints "not assessed" on dimensions that were fully assessed by a governing lane — inverting ADR-0067 rather than serving it.

Alternative 3: Do nothing — treat the two tests as a genuine conflict to resolve later

  • Why rejected: the two tests are not in conflict (they pin a provider-health axis and a coverage axis, and neither touches a flag). The premise for deferring is false, and activities.py:4880 is a live defect either way.

Decision context:

  • Latency: no measurable change. A coverage record is a dict write. neo4j_enabled=True adds one connection attempt per case where the deployment has no Neo4j; unmeasured because the target deployment runs Neo4j.
  • Dependency surface: none. No new packages; reuses Outcome, GAP_PRECEDENCE, and the existing *_coverage dict convention.
  • Debuggability: improves. A case record that says "Art. 20(1)(d): not assessed — control_disabled:sanctioned_ownership_control_enabled" is self-explaining; today it says nothing and the reader must inspect deployment config to know why.
  • Reversibility: hours. The coverage records are additive dict keys; neo4j_enabled reverts with a one-line default change.
  • Blast radius: additive at 4 producer sites plus 2 consumer surfaces. No existing determination changes value; no Finding is added or removed.
  • Alternative considered: blanket typed gap on every flag-off — rejected because it 409-blocks approvals for deliberately-staged features under ADR-0125.