ADR-0132: Dashboard-assistant AMLR Wave-1 determination tools + non-downgradable audit-readiness gate
Date: 2026-07-24 Status: Accepted Deciders: Adrian (Soft4U BV), Claude Opus 4.8 (implementation + safety analysis)
Decision context:
- Latency: negligible. The four determination tools each do one already-existing indexed read reused verbatim from
amlr_wave1_summary(#572);assess_audit_readinessadds onebuild_amlr_wave1_summarycall (the same four small tenant-scoped reads) onto a path that runs only on interactive chat demand, never the request-critical path. Not measured because it rides the existing read helpers and fires interactively, not per request. - Dependency surface: zero new packages. Reuses
app.services.amlr_wave1_summary(#572 read plumbing),register_discrepancy_service,immediate_designation_rescreen_service(coverage note),audit_service(via the summary), and the existing pure_derive_audit_readiness. - Debuggability: every tool returns a structured dict carrying
determination+regulatory_basis+adr+provenance(withsource), so the assistant's answer is grounded and the exact input is inspectable (EU AI Act Art. 12/13). A read failure is a LOUDlogger.warningplus a fail-closednot_assessed(never a silent clear). The merged readiness carrieswave1_summary+wave1_blockers+non_downgradable_blockers, so the precise reason a verdict is BLOCKED is queryable from the tool output. - Reversibility: pure additive application code — no schema, no migration, no feature flag. Undo = delete the four tools + the three helper functions and revert the two call sites (
assess_audit_readiness,draft_decision_memorandum); the prompt change is a single version bump. - Blast radius: additive. Exactly two call sites change, both in
dashboard_agent.py. The base_derive_audit_readinessand the separate decision-memorandum API path (app/api/decision_memorandum.py) are byte-unchanged. The Wave-1 merge only ever RAISES the verdict (a MAX over an ordered scale), never lowers it. - Alternative considered: teach the prompt to "always surface Wave-1 blockers" and leave the verdict logic unchanged (rejected — a prompt instruction can be ignored, truncated, or drift, and the never-suppress rule is exactly the property that must not depend on the model choosing to obey; it must be structurally enforced in the tool's return).
Context
The CopilotKit officer-dashboard assistant (app/agents/dashboard_agent.py) has an
assess_audit_readiness tool that answers "can I approve this case today?". It derived
its verdict (APPROVE_SAFE | REVIEW_REQUIRED | BLOCKED) from the 9-item CDD matrix plus
critical findings/discrepancies (_derive_audit_readiness, pure).
Four AMLR controls shipped to master in Mini-Wave 0 (epic #528) with full backend logic
and, via #572, a fail-closed officer read surface (build_amlr_wave1_summary):
- Sanctioned ownership/control — AMLR Art. 20(1)(d) (ADR-0127)
- Register-discrepancy report obligation — AMLR Art. 24 (ADR-0129)
- Senior-managing-official fallback / exhausted-means abstention — AMLR Art. 22(2)/63 (ADR-0126)
- Immediate-on-designation sanctions re-screen — AMLR Art. 26(4) (ADR-0128)
Two gaps followed. First, the assistant had no tools to query any of these four
determinations, so it answered questions about them from general knowledge or not at all —
an ungrounded answer on a compliance surface (EU AI Act Art. 11-15 traceability). Second,
and more dangerous: assess_audit_readiness computed its verdict with no knowledge of
the four controls at all, so it could return APPROVE_SAFE for a case that carries a
confirmed sanctioned owner (Art. 20(1)(d)) or an open register-discrepancy reporting
obligation (Art. 24). That is a never-suppress violation (ADR-0067, ADR-0116, EU AI Act
Art. 14 human oversight): an artifact that reads "ready" over a hard compliance blocker.
A prompt instruction alone cannot fix this — the property required is that the assistant
cannot wash out a blocker, which must not depend on the model choosing to comply.
Decision
Add four read-only, fail-closed Wave-1 determination tools to the dashboard agent, and
make assess_audit_readiness structurally non-downgradable over Wave-1 hard blockers.
1. Four determination tools — each reuses the authoritative fail-closed reads in
amlr_wave1_summary (#572) and returns a structured, traceable result carrying the
determination state + the regulatory article + the ADR + provenance (with source):
| Tool | Reads | Article / ADR |
|---|---|---|
get_sanctioned_ownership_determination | summarize_sanctioned_ownership (pure) | Art. 20(1)(d) / ADR-0127 |
get_register_discrepancy_status | _summarize_register_discrepancy | Art. 24 / ADR-0129 |
get_smo_fallback_record | _summarize_smo_abstention | Art. 22(2)/63 / ADR-0126 |
get_screening_currency | _summarize_immediate_rescreen | Art. 26(4) / ADR-0128 |
A control that never ran, or a read that fails, returns an explicit not_assessed
(assessed=False) — never fabricated as clear.
2. Non-downgradable readiness gate. A pure _derive_wave1_readiness_blockers maps each
Wave-1 control state to a readiness blocker carrying an explicit verdict_floor:
confirmed sanctioned ownership and an open_obligation register discrepancy are HARD
blockers (BLOCKED); an un-run/indeterminate required determination (not_assessed, a
coverage gap, a name-only candidate) is NOT-ready (REVIEW_REQUIRED). A pure
_merge_wave1_readiness folds these into the base assessment and derives the final verdict
as the MAX of the base verdict and every Wave-1 floor over the ordered scale
APPROVE_SAFE < REVIEW_REQUIRED < BLOCKED. Because the verdict is computed from the
blockers by a monotonic max, a favourable base can never wash out a Wave-1 blocker, and the
tool cannot return APPROVE_SAFE while a Wave-1 hard blocker is on file. Each blocker is
tagged non_downgradable=True and surfaced under non_downgradable_blockers; the full
fail-closed wave1_summary is carried for traceability. assess_audit_readiness and
draft_decision_memorandum both route through the merged async deriver
(_derive_audit_readiness_full); if the whole summary read fails, all four controls fail
closed to not_assessed rather than certify over an unread gate.
3. Prompt (v1→v2). The dashboard template teaches the four controls, their articles,
when to call each tool, and the load-bearing rule: the model MUST surface every
non_downgradable_blocker verbatim and may add scrutiny but never omit, soften, or
downgrade one. The prompt is a belt to the structural braces — not the enforcement itself.
Consequences
Positive
- The assistant can explain all four shipped Wave-1 controls with a grounded determination + article + provenance, instead of guessing.
assess_audit_readiness(and the decision-memo preview the agent drafts) can no longer read audit-ready over a confirmed sanctioned owner or an open Art. 24 report obligation — the never-suppress invariant is enforced in code, not merely requested of the model.- Reuses the #572 read plumbing unchanged, so the tools and the readiness gate see exactly the same fail-closed states the officer read surface does (no second, drift-prone path).
Negative
- A case whose Wave-1 determinations are
not_assessed(e.g. an older case predating the determination, or a dark-launched control) now floors at REVIEW_REQUIRED rather than APPROVE_SAFE. This is the intended honest-absence behaviour, but it means the assistant will decline to certify some cases it previously called safe. If the sanctioned-ownership determination is not yet universally produced, this can raise the REVIEW rate — a calibration input to watch, not a defect. - The readiness tool is now async and does four tenant-scoped reads per call (previously pure/in-memory). Negligible on the interactive chat path, but no longer zero-I/O.
Neutral
- The separate decision-memorandum API path (
app/api/decision_memorandum.py) still uses the pure base_derive_audit_readinessand does NOT fold in the Wave-1 gate. Its verdict can therefore differ from the assistant's for a case with a Wave-1 blocker; unifying that path is deliberately out of scope here (tracked follow-up) to bound blast radius.
Alternatives Considered
Alternative 1: Prompt-only instruction
- Teach the prompt to always call the four tools and never omit a Wave-1 blocker, leaving the verdict computation unchanged.
- Why rejected: never-suppress is the property that must not depend on the model obeying an instruction. A prompt can be truncated, ignored, or drift across model upgrades; the tool would still be able to return APPROVE_SAFE over a hard blocker. The invariant has to live in the tool's return value.
Alternative 2: Re-use the deterministic per-case compliance verdict (ADR-0113) directly
- Have the assistant read the ADR-0113
compliance_verdictand surface it instead of a distinct readiness gate. - Why rejected: the ADR-0113 verdict answers "is the collected evidence escalate / clear / insufficient_data", a related but different question from "is this case audit-defensible to approve today" (which folds in the 9-item CDD matrix and the Wave-1 controls). The two should stay consistent but are not the same object; conflating them would blur the officer-facing readiness surface. The Wave-1 gate reuses the same fail-closed doctrine (ADR-0067) and monotonic-ratchet idea (ADR-0089) without collapsing the two verdicts.