Skip to main content

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_readiness adds one build_amlr_wave1_summary call (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 (with source), so the assistant's answer is grounded and the exact input is inspectable (EU AI Act Art. 12/13). A read failure is a LOUD logger.warning plus a fail-closed not_assessed (never a silent clear). The merged readiness carries wave1_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_readiness and 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):

  1. Sanctioned ownership/control — AMLR Art. 20(1)(d) (ADR-0127)
  2. Register-discrepancy report obligation — AMLR Art. 24 (ADR-0129)
  3. Senior-managing-official fallback / exhausted-means abstention — AMLR Art. 22(2)/63 (ADR-0126)
  4. 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):

ToolReadsArticle / ADR
get_sanctioned_ownership_determinationsummarize_sanctioned_ownership (pure)Art. 20(1)(d) / ADR-0127
get_register_discrepancy_status_summarize_register_discrepancyArt. 24 / ADR-0129
get_smo_fallback_record_summarize_smo_abstentionArt. 22(2)/63 / ADR-0126
get_screening_currency_summarize_immediate_rescreenArt. 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_readiness and 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_verdict and 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.