Sub-scope 05 (notification) provider + core + platform/{provider,observation,reactor} — manifest and probes revision=a24ece9cf797f7ea647e33bf846b115208ed1ba5 generatedAt=2026-08-30T06:52:39+00:00 === OWNED FILES (main) === 60b282fdaf571b03713ec3322598cc026599956e main/java/dev/caskeleton/adapter/outbound/notification/core/FailOpenNotificationProvider.java 45 fae64a058a039a7ecc240272827fbac9959d6fdc main/java/dev/caskeleton/adapter/outbound/notification/core/NotificationProvider.java 31 1fc3ed6bfcd98beccd79980aa8b35121a09d233d main/java/dev/caskeleton/adapter/outbound/notification/core/RoutingNotifier.java 143 10a762d66040f26d0f0827ab3bddf691cb44709b main/java/dev/caskeleton/adapter/outbound/notification/platform/observation/LoggingNotificationAudit.java 60 6760f65f4adbb5b0567b0f3694568aebb1533cc6 main/java/dev/caskeleton/adapter/outbound/notification/platform/observation/LoggingNotificationMetrics.java 59 c710af99e34cf4eab650d0aa7a084fd2bacb16c9 main/java/dev/caskeleton/adapter/outbound/notification/platform/observation/NotificationHealthReporter.java 109 a41a5704d0da558c81a19daca41e41dd75c2d905 main/java/dev/caskeleton/adapter/outbound/notification/platform/observation/NotificationHealthSnapshot.java 31 861144530bb50837f313e785d0f8cd87504d1cc3 main/java/dev/caskeleton/adapter/outbound/notification/platform/observation/NotificationServingThresholds.java 77 8eabe4b61fa791a15280b5a2740d41c62cc801db main/java/dev/caskeleton/adapter/outbound/notification/platform/provider/EmailAttachments.java 74 3536d27bf4bb68c4b77de1cdf520d4c128ea781b main/java/dev/caskeleton/adapter/outbound/notification/platform/provider/ProviderResults.java 100 579bf96a1b0635342ac56f1550c73256214754ef main/java/dev/caskeleton/adapter/outbound/notification/platform/provider/UnconfiguredAttachmentResolver.java 29 bc130299645984991cbc7e73499a55161a34f214 main/java/dev/caskeleton/adapter/outbound/notification/platform/reactor/ReactiveNotificationOrchestrator.java 27 4be68bc232781fed6174ddc72dc10741ae8449c7 main/java/dev/caskeleton/adapter/outbound/notification/platform/reactor/ReactorContextBridge.java 41 45e34dc34bfe4a170a3f87c3b2f7c8cf9352e072 main/java/dev/caskeleton/adapter/outbound/notification/platform/reactor/ReactorNotificationOrchestrator.java 74 3a1929a0d2efc4c07a381711ed48365021cf25a2 main/java/dev/caskeleton/adapter/outbound/notification/provider/AttemptCorrelationId.java 34 8426e14502c0df2e96d7db106d11f379d7f17a64 main/java/dev/caskeleton/adapter/outbound/notification/provider/InlineNotificationAttemptAdapter.java 54 df65ef07819220cb95aa5f2928015ed6868d3561 main/java/dev/caskeleton/adapter/outbound/notification/provider/NotificationAdmissionReadinessAdapter.java 163 28f03e15069e1d3ae7431c10c38e9783a962e217 main/java/dev/caskeleton/adapter/outbound/notification/provider/NotificationAttemptContext.java 54 83b67ce2cc2cb1737a7b0565e7fbb8084bacf1ca main/java/dev/caskeleton/adapter/outbound/notification/provider/NotificationProviderAttemptAdapter.java 363 0f67f36f30ebd67abf8366471e9ca81efe8e5407 main/java/dev/caskeleton/adapter/outbound/notification/provider/NotificationProviderAttemptClient.java 104 49fbf9f8d45dde999dc6d49e4081a073c984bfd9 main/java/dev/caskeleton/adapter/outbound/notification/provider/NotificationProviderRateAdmission.java 35 90f5abc482ada17ed5dfde249b621d487c866baa main/java/dev/caskeleton/adapter/outbound/notification/provider/NotificationProviderReadinessProbe.java 12 3c4eee63e00d175b104bd210e120e12cb25c4b01 main/java/dev/caskeleton/adapter/outbound/notification/provider/NotificationProviderReadinessSnapshot.java 42 effdc813379835d984da6aff60d04d0cde3cf7ce main/java/dev/caskeleton/adapter/outbound/notification/provider/NotificationProviderSecretMaterialProvider.java 10 a869b5935248b5813d2ea5b04e07936632ddb653 main/java/dev/caskeleton/adapter/outbound/notification/provider/NotificationReconciliationAdapter.java 151 3b4d8e838567ebb873b994f17bf48ff8c02edce2 main/java/dev/caskeleton/adapter/outbound/notification/provider/NotificationSecretMaterialHandle.java 93 157d1b26ff0f939fd34e25a324f9be9407d81e03 main/java/dev/caskeleton/adapter/outbound/notification/provider/PreparedNotificationAttempt.java 31 a164147837b12da9e75f4d46a4ba49b9730a43ef main/java/dev/caskeleton/adapter/outbound/notification/provider/ProviderMessageReference.java 15 fdd03f4d009e5c08444b5d0ebf4a43bb9b4e3ac2 main/java/dev/caskeleton/adapter/outbound/notification/provider/ReconciliationLookupMode.java 9 main_count=29 === OWNED FILES (test) === 450f4afda7e0ae1af60aab81b48bfed733b91416 test/java/dev/caskeleton/adapter/outbound/notification/core/NotificationAdapterTest.java 174 8f120fc7d63f4022d8d7098da4435d3b3082d79d test/java/dev/caskeleton/adapter/outbound/notification/core/RoutingNotifierTest.java 359 3357d050c0bd755ad75f726cf38cf43bbe913a8a test/java/dev/caskeleton/adapter/outbound/notification/platform/provider/ProviderSandboxSmokeTest.java 37 ef82853f024be998396b1dec61826b51875a57ad test/java/dev/caskeleton/adapter/outbound/notification/platform/provider/SubmissionCompatibilityTest.java 73 a2bff37f2be9a3f9ee338391d7bd5955492b2d32 test/java/dev/caskeleton/adapter/outbound/notification/provider/InlineNotificationAttemptAdapterTest.java 55 6e46da9d5aeaf1416b9775292555ceea5791cb62 test/java/dev/caskeleton/adapter/outbound/notification/provider/NotificationAdmissionReadinessAdapterTest.java 195 a2558bf4bba9b894b767093c046fb2da39da9352 test/java/dev/caskeleton/adapter/outbound/notification/provider/NotificationProviderAttemptContractTest.java 402 0fdad560acb5aaa1b0c499fe005339957cd97afd test/java/dev/caskeleton/adapter/outbound/notification/provider/NotificationReconciliationAdapterTest.java 194 d2e8ba7b4ac70ae3303ab965c5f33d9ec4e433f6 test/java/dev/caskeleton/adapter/outbound/notification/provider/NotificationSecretMaterialHandleTest.java 38 test_count=9 === 8.1 Retry-After 힌트의 최종 소비처 === $ grep -rn 'ProviderFailure::retryAfter' --include=*.java application-core/src/main application-core/src/main/java/dev/caskeleton/application/notification/platform/dispatch/NotificationDispatchService.java:381: result.failure().flatMap(ProviderFailure::retryAfter), exit=0 $ grep -n 'chosen\|max' application-core/src/main/java/dev/caskeleton/application/notification/platform/policy/RetryBackoff.java 11: * the configured maximum: a provider asking for an hour must not silently extend a delivery 14:public record RetryBackoff(Duration base, Duration max, double multiplier) { 21: Objects.requireNonNull(max, "max"); 25: if (max.compareTo(base) < 0) { 26: throw new IllegalArgumentException("max must not be smaller than base"); 41: long ceilingMillis = (long) Math.min(exponential, (double) max.toMillis()); 43: Duration computed = Duration.ofMillis(Math.max(jittered, base.toMillis())); 44: Duration chosen = retryAfter.filter(hint -> hint.compareTo(computed) > 0).orElse(computed); 45: return chosen.compareTo(max) > 0 ? max : chosen; exit=0 === 8.2 음수 Retry-After: 파서와 생성자의 계약 비교 === $ sed -n '89,99p' adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/provider/ProviderResults.java /** Parse a {@code Retry-After} header expressed in seconds. */ public static Optional retryAfter(Optional headerValue) { return headerValue.flatMap( value -> { try { return Optional.of(Duration.ofSeconds(Long.parseLong(value.trim()))); } catch (NumberFormatException notSeconds) { return Optional.empty(); } }); } exit=0 $ sed -n '28,35p' application-core/src/main/java/dev/caskeleton/application/notification/platform/provider/ProviderFailure.java } retryAfter.ifPresent( delay -> { if (delay.isNegative()) { throw new IllegalArgumentException("retryAfter"); } }); } exit=0 $ grep -rn 'ProviderResults.retryAfter' --include=*.java adapter/outbound/notification/src/main adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/provider/ses/SesFailureClassifier.java:37: ProviderResults.retryAfter(response.header("retry-after")), adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/provider/ses/SesFailureClassifier.java:49: response.statusCode(), ProviderResults.retryAfter(response.header("retry-after"))); adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/provider/webpush/WebPushFailureClassifier.java:39: status, ProviderResults.retryAfter(response.header("retry-after"))); adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/provider/twilio/TwilioFailureClassifier.java:35: ProviderResults.retryAfter(response.header("retry-after")), adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/provider/twilio/TwilioFailureClassifier.java:39: response.statusCode(), ProviderResults.retryAfter(response.header("retry-after"))); exit=0 === 8.3 첨부 무결성: resolve 안에 verify 가 있는가 === $ grep -n 'expectedSize\|expectedDigest\|isEqual' application-core/src/main/java/dev/caskeleton/application/notification/platform/dispatch/AttachmentIntegrityGuard.java 35: if (attachment.size() != reference.expectedSize()) { 39: if (!constantTimeEquals(attachment.digest(), reference.expectedDigest())) { 64: return java.security.MessageDigest.isEqual(left, right); exit=0 === 8.4 헬스: 어떤 런타임 상태가 unhealthy 인가 === $ grep -n 'healthy = false' -B 3 adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/observation/NotificationHealthReporter.java 55- for (ProviderProfileId profileId : monitoredProfiles) { 56- var runtime = runtimes.find(profileId); 57- if (runtime.isEmpty()) { 58: healthy = false; -- 63- ProviderRuntimeState state = runtime.get().state(); 64- if (state == ProviderRuntimeState.AUTHENTICATION_FAILED 65- || state == ProviderRuntimeState.DISABLED) { 66: healthy = false; -- 76- // A platform with providers but no route accepts every request and delivers none. It was 77- // reported healthy because every runtime was healthy — which was true and beside the point. 78- if (!monitoredProfiles.isEmpty() && routedChannels.isEmpty()) { 79: healthy = false; -- 81- 82- NotificationServingState serving = servingState.currentState(); 83- if (thresholds.exceededBy(serving)) { 84: healthy = false; exit=0