Symptom

Kraken’s nightly architectural scan (routed via Sautner-Studio-LLC/krill-oss#233) flagged that krill-sdk’s NodeObserver interface could not type-enforce its coroutine-cancellation contract for implementations that bypass AbstractNodeObserver. krill-sdk 0.0.66 gates direct NodeObserver implementation behind @SubclassOptInRequired(UnsafeNodeObserverImplementation::class). krill’s own DefaultNodeObserver implemented NodeObserver directly, so once the krill-sdk pin in gradle/libs.versions.toml moved past 0.0.66 this class would have failed to compile.

Root cause

DefaultNodeObserver predated AbstractNodeObserver (added to krill-sdk at 0.0.58) and hand-rolled the exact scope-lifecycle pattern the SDK now standardizes: a child CoroutineScope built from SupervisorJob(parentJob) so individual node failures stay isolated while the scope still dies with its parent, plus a synchronous close() that cancels that scope. Nothing was functionally wrong — close() already cancelled synchronously, so there was no live leak — but the class was never migrated to extend the SDK’s abstract base once it existed, so it kept using the unsafe direct-implementation path the new opt-in gate is designed to catch.

Fix

Prevention

When an SDK adds an abstract base class that formalizes a pattern already hand-rolled downstream, migrate the downstream implementation at the same time rather than leaving it on the direct-interface path “since it still compiles.” A @SubclassOptInRequired gate added later turns that deferred migration into a surprise compile failure on an unrelated version-bump PR. Grep consuming repos for direct implementations of an SDK interface whenever that interface gains an Abstract* base class, not just when a breaking annotation actually lands.