ADR-0173: Some monitoring checks are statutory inputs and cannot be disabled
Date: 2026-08-03 Status: Proposed Deciders: Adrian (project owner), Claude Opus 5, Codex (review, PR #1014) Relates to: ADR-0083 (event-trigger taxonomy), ADR-0085 (Art. 21 lifecycle), ADR-0096 (durable schedule provisioning), ADR-0128 (immediate re-screen)
Context
monitoring_schedule_service already declares MANDATORY_EVENT_TRIGGERS — the
event triggers AMLR Art. 26 requires a tenant to respond to. What it did not
declare is that a trigger is only as real as the check that detects it.
Four checks are the sole producers of four mandatory triggers:
| check | trigger it is the only producer of |
|---|---|
ubo_screening | sanctions_list_update, pep_status_change |
ownership_change | ownership_change_above_25pct |
jurisdiction_change | jurisdiction_change |
adverse_media | adverse_media_critical |
A tenant could keep every mandatory trigger declared and switch the producing check off. The config validated, the schedule provisioned, and the trigger then never fired — while every surface reported the trigger configured. That is the claim-vs-check class applied to a statutory obligation: the register said the control was there, and nothing detected that its input had been removed.
Two facts about the surrounding machinery shape the decision, both from Codex on PR #1014 against an implementation that guarded only the per-check flag:
- The schedule has a top-level switch. A config with
enabled=falseand all four checks enabled passed the per-check guard, and applying it through/schedule/startpauses the Temporal schedule. None of the four checks then run. The per-check guard is bypassed without disabling any individual check — a guard that governs the parts and not the whole. - The guard runs on write only. A tenant that stored a non-compliant config before the guard existed keeps it indefinitely: the reconciler deserializes stored JSON and treats an existing schedule as an ensure-no-op, so nothing revisits it. Prevention without repair leaves the population it was built for untouched.
Decision
A check that is the sole producer of a mandatory trigger cannot be disabled, and "disabled" includes every mechanism that stops it running.
-
Declare the mapping, don't infer it.
MANDATORY_TRIGGER_INPUTSmaps each mandatory trigger to the check(s) that produce it. It is a table, reviewable as data, and it is the single place the policy lives. Inferring the relationship from names would make a rename silently disarm the rule. -
Refuse the config, with the breaches named. Storing a config that disables such a check returns 409 with the specific breaches — never a silent normalisation to a compliant value, which would leave the tenant believing their edit took effect.
-
The top-level switch is covered by the same policy. Pausing the schedule stops the mandatory checks exactly as surely as disabling them individually, so
enabled=falseon a schedule carrying mandatory checks is refused by the same rule. The alternative — a separate schedule the switch cannot reach — is recorded below and rejected. -
Validation semantics follow the write path, not the reader. An omitted
enabledkey means enabled, becauseMonitoringCheckConfigdefaults it toTrueand the PUT path parses through that model. The advisoryPOST /config/validateendpoint must agree with what a write would actually do; an endpoint that reports a check disabled where the writer would enable it is worse than no endpoint, because it is consulted precisely when someone is unsure. -
Already-stored configs are repaired at read, not left to a future write. The reconciler evaluates the stored config against this policy and surfaces a breach rather than treating an existing schedule as satisfied. A rule that only binds on the next PUT does not bind at all for a tenant who has stopped editing.
Amendment 2026-08-03 — the fifth producer, and per-lane protection
The Context above describes four checks, each the sole producer of its mandatory
trigger. Codex found (PR #1014) that the map was incomplete and that the "sole
producer" model is wrong, so MANDATORY_TRIGGER_INPUTS now holds five:
material_change routes its sanctions, pep and adverse_media signals to
three mandatory triggers via trigger_router_service._MATERIAL_CHANGE_SIGNAL_TRIGGER,
and it is the only lane carrying the subject entity's own status, which arrives
through fiscal_rep_data. check_ubo_screening takes directors, so it covers
directors and UBOs and nothing else.
Two checks feeding one trigger is not redundancy — it is two lanes into one obligation, and disabling either silences the half it carries. That is why the map is keyed on the CHECK rather than on the trigger, and why an entry may name a trigger another entry also names.
Recorded here rather than left to the code because the ADR is the specification: a
maintainer reading "four sole producers" would reasonably remove the
material_change row as scope creep. The reconciliation test added in the same PR
derives the producer set from the router's own tables, so this list cannot silently
fall behind the code again — the earlier test parametrised over the map itself and
could only ever confirm what was already declared.
Consequences
Positive
- A mandatory trigger cannot be declared while its only producer is switched off, by any of the three routes that previously allowed it (per-check flag, top-level pause, never-revisited stored config).
- The refusal names what breached, so the tenant can act on it without reading the source.
- The mapping is data. Adding a mandatory trigger or a second producer is a reviewable edit, not a code change buried in a validator.
Negative
- A tenant genuinely cannot pause monitoring wholesale any more. That is the point, but it removes an operational escape hatch some deployments may have been using for maintenance windows, and they will hit it as a 409 with no alternative. A maintenance-window mechanism that does not stop mandatory checks is future work, not covered here.
- Existing non-compliant configs start surfacing breaches on read. Those tenants were not monitoring what they believed they were monitoring; the surfacing is correct and will still look like a regression to them.
- The policy binds four checks by name. A fifth producer added without a row in the table is unguarded — which is why the table is the decision and a test pins it in both directions.
Neutral
- No schema change; the config shape is unchanged.
- ADR-0083's trigger taxonomy is unchanged. This ADR constrains which checks may be disabled, not which triggers exist.
Alternatives Considered
Alternative 1: Guard the per-check flag only (what PR #1014 first did)
Refuse a disabled mandatory check; leave the top-level switch alone.
Why rejected: measured — a config with enabled=false and all four checks
enabled passes, and pausing the schedule stops all four. The guard governs the
parts while the whole remains switchable, so the rule can be satisfied and
defeated in one config.
Alternative 2: Run mandatory checks on a separate, unpausable schedule
Split the four out so the tenant's switch cannot reach them.
Why rejected for now: it is the structurally stronger answer and remains open as future work. It is rejected here because it changes the provisioning model (ADR-0096) for every tenant, needs its own reconciler behaviour and its own failure semantics, and would land as a large change to the one subsystem whose silent failure this ADR exists to prevent. Refusing the pause is the smaller move that closes the same hole today; if maintenance windows become a real requirement, this alternative is how to grant them safely.
Alternative 3: Warn instead of refusing
Accept the config, emit a warning, surface a banner.
Why rejected: the defect being fixed is that a non-monitoring state reported itself as monitoring. A warning on a stored config is read once, at write time, by the person who chose the setting — and never again by the supervisor who later asks whether the control was in place.
Decision context:
- Latency: none — a dictionary lookup during config validation.
- Dependency surface: no new packages.
- Debuggability: a 409 naming the breached checks, versus a trigger that silently never fires.
- Reversibility: hours. One table plus validator branches; no migration.
- Blast radius: the monitoring config write path, the advisory validate endpoint, and the reconciler's read of stored configs. Tenants with non-compliant stored configs change behaviour.
- Alternative considered: a separate unpausable schedule — stronger, deferred, and recorded above with the condition that would justify it.