The supplement that breaks the chain

The supplement that breaks the chain

GDPR Art.22 says if you make automated decisions that significantly affect people, you document what the algorithm did and why. In AML, that’s the triage decision — the moment the system decides whether someone gets investigated further or cleared.

The platform already has ComplianceSupplement — a metadata attachment on ledger entries that records algorithm reference, confidence score, rationale, contestation URI, and whether human override is available. The clinical-trial harness uses it for EU AI Act Art.12 compliance. AML doesn’t use it at all.

The implementation looked straightforward: factory class for the supplement, attach it to the triage entry, add an Art.22 section to the compliance evidence report. The clinical pattern is clean — static factory methods, entry.attach(supplement) before save, done.

Two things weren’t straightforward.

The sanitiser that shouldn’t be global. The epic mentioned a DecisionContextSanitiser SPI for PII redaction. We designed one — then the decision review pointed out that ContentSanitiser already exists in casehub-ledger-runtime with the exact same signature. Parameter even named decisionContextJson. The entire foundation SPI work evaporated.

But the plan review caught a subtler problem: making AML’s IBAN-redacting sanitiser a CDI bean that displaces the platform’s PassThroughContentSanitiser would affect every domain sharing the ledger. A future clinical-trial supplement with a legitimate IBAN in its context would get silently redacted. The fix: plain class, instantiated locally, no CDI. Domain-specific PII rules stay domain-scoped.

The observer that corrupts the chain. The spec review said Art.22 logic shouldn’t be coupled to the CBR observer — different concerns, different error boundaries, different packages. Fair point. So we designed a separate CaseOutcomeObserver that queries the profile entry back, attaches the supplement, and re-saves.

The plan review killed it. LedgerEntry.canonicalBytes() includes supplementJson. The digest computed at the first save doesn’t match the content after supplement attachment. The Merkle chain quietly corrupts — no error at write time, only discovered when verify() runs and reports chain inconsistency.

The fix is obvious once you see it: attach the supplement inside the existing observer, before the single repository.save() call. One write, one digest, chain intact. The factory and sanitiser remain separate classes — the concern separation is in the components, not in the observer boundary.

The spec’s own global constraint said it: “attach() must be called before save().” We wrote the constraint, then designed around it, and the review caught us.

That’s why reviews exist.