ADR-0067: Fail-Closed Compliance Outputs & the "Not Assessed" Contract
Status: Accepted
Date: 2026-06-26
Deciders: Adrian (Soft4U) + Claude Opus 4.8
Milestone: M1 — Data Honesty & Fail-Closed (see docs/plans/2026-06-26-world-class-product-roadmap.md)
Decision-context
A 2026-06-26 officer review + regression audit (docs/research/2026-06-26-regression-audit-findings.md)
found multiple paths that report a benign result when no check actually ran — a live /scan
wired with an empty sanctions list returning "clear"; person_screening unconditionally returning
sanctions_clear=True; screening exceptions swallowed into silence; CDD coverage computed from node
presence. For an MLRO personally liable for the decision, a false "clear" is worse than no
answer: it manufactures unwarranted comfort that survives into the audit file.
Decision
Every compliance-facing output (screening, coverage, score, monitoring check) MUST be fail-closed:
- No benign-by-default. A check that cannot run (unconfigured source, empty list, exception,
missing tenant context, absent data) MUST NOT return
clear/covered/low/no change. It MUST return an explicit indeterminate / gap /not_screened/status="error"signal. - Indeterminate reads as scrutiny, never comfort. The indeterminate state surfaces as a HIGH /
requires_reviewdata-gap finding (or an equivalent non-benign status), is recorded for audit (EU AI Act Art. 12), and never silently advances a case toward approval. - One canonical gap shape. Reuse the existing
sanctions_screening_gapfinding shape and thecheck_ubo_screeningindeterminate pattern; do not invent per-call-site variants. New determinism categories are registered inapp/models/finding_determinism.py. - Presence ≠ evidence (companion rule,
docs/calibration-review-checklist.md#1): a coverage/ "verified" metric asserts substance, not that a flag/node/field exists.
Consequences
- Positive: the officer can trust that "clear/covered/low" means checked-and-clean, not never-checked. Honest gaps are visible and triageable. Directly satisfies the calibration checklist's "no silent suppression" + "honest empty-states".
- Negative / accepted: more HIGH data-gap findings surface (some cases that previously read "clean" now show "not assessed"). This is intended — the noise is honest signal the officer must see; it is mitigated by clear gap categorisation, not by hiding it.
Implementation (M1)
- W1 (this ADR's first delivery): H3
/scanempty-list, M2person_screening, M7/M18 swallowed screening exceptions. Subsequent waves extend the contract to coverage honesty (H1/H2) and honest scores/defaults (M14/M6/H6).