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<Duration> retryAfter(Optional<String> 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

