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"
| Condition | What it means | Typed 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-140publishesownership_dimension_evaluated=Falsewhensanctioned_ownership_control_enabledis off. Its docstring names the defect: "a 'claim vs check' contradiction… an unrun check must never read as assessed."activities.py:4880wraps the entire AMLR Art. 20(1)(d) computation — and its own fail-closed gap emitter — insideif 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 emitswould_deny/would_blocktelemetry 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 partial → gap_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 ofGAP_PRECEDENCE(claims.py:35-46), ranked belowVERIFIED_CLEAR, so it never out-ranks a real determination; - stays out of
_COVERAGE_STATE_CATEGORIES, so it never becomes agap_reason, never becomesinsufficient_data, and never 409-blocks an approval under ADR-0125; - floors the assistant readiness verdict at
REVIEW_REQUIRED, neverBLOCKED(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_gapdoes not block because it is actionable. It blocks because the field applies to the entity and was not determined — which is whatabsence_vocabulary.py:86-90actually says. Under the corrected axis adimension_unavailablerecord on the statutorysanctioned_ownership_controldimension (AMLR Art. 20(1)(d)), where no prior determination exists, WITHHOLDSclear.control_disabledrecords of every dimension, anddimension_unavailableon 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:4880contradiction 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=Trueputs 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_APPLICABLEgains 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=Truemeans 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.pyuntyped "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=Falsecitingperson_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 whatbuild_screening_gap_findingdocuments. _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 explicitif result:(investigations,companies); most other counters increment unconditionally after theawaitand 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 itsNonereturn 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 samedimension_unavailablerecords 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 implemented — ADR-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 determined — absence_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:4880defect 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:4880is a live defect either way.
Decision context:
- Latency: no measurable change. A coverage record is a dict write.
neo4j_enabled=Trueadds 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*_coveragedict 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_enabledreverts with a one-line default change. - Blast radius: additive at 4 producer sites plus 2 consumer surfaces. No existing
determination changes value; no
Findingis 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.