Sub-scope 02·03 (notification) catalog + template + platform/dispatch — manifest and probes revision=a24ece9cf797f7ea647e33bf846b115208ed1ba5 generatedAt=2026-08-30T06:40:03+00:00 === OWNED FILES (sub-scope 02) === 589a6a1e880e9f6e3a277bb8873c4dc166caee90 main/java/dev/caskeleton/adapter/outbound/notification/catalog/CompiledNotificationBinding.java 151 6a3825e6e2a64b0a7adb54ec9753c7278b677f21 main/java/dev/caskeleton/adapter/outbound/notification/catalog/NotificationBindingCompiler.java 516 066b7a1d0ae6cfeb428edc3330bdb898830dfc27 main/java/dev/caskeleton/adapter/outbound/notification/catalog/NotificationCanonicalRouteCatalog.java 124 e7e5ef12bf86a5b829f41ccf0ebc5e4a23ada484 main/java/dev/caskeleton/adapter/outbound/notification/catalog/NotificationCatalogException.java 70 2fd0408dfc2faaf79535f2ab7214e8885aa0e5d0 main/java/dev/caskeleton/adapter/outbound/notification/catalog/NotificationCutoverRouteCatalog.java 151 ddcab4056acd253a408b642fb14272733dbfce4f main/java/dev/caskeleton/adapter/outbound/notification/catalog/NotificationPlanAdapter.java 86 4eefb8246e58aeb032496078beafc06d584e6d64 main/java/dev/caskeleton/adapter/outbound/notification/catalog/NotificationProviderCapabilityCard.java 99 a5f6322e2013f6aa647307d1a485bb973b6cd949 main/java/dev/caskeleton/adapter/outbound/notification/catalog/NotificationProviderCapabilityDescriptorSource.java 43 9ebffdeb3505ced57d8607a8764b0cf32b67ee56 main/java/dev/caskeleton/adapter/outbound/notification/catalog/NotificationProviderDescriptor.java 41 da9f2cd56fe0d9498671b4663c2b63037b325d8b main/java/dev/caskeleton/adapter/outbound/notification/catalog/NotificationProviderRuntimeProfile.java 45 e473bfa621c8a655884378b9faadfe0343a91168 main/java/dev/caskeleton/adapter/outbound/notification/catalog/NotificationRouteDescriptor.java 93 b8d3e1709002af836d344aabee0f694e637a68ad main/java/dev/caskeleton/adapter/outbound/notification/catalog/NotificationTemplateDescriptor.java 81 4652840326390e8ae5ef66a9c13688a153f12980 main/java/dev/caskeleton/adapter/outbound/notification/template/LocalEmailRenderer.java 473 17f3acb4ca227f6a17ebd7221f8cdfcc4bff56a1 main/java/dev/caskeleton/adapter/outbound/notification/template/NotificationTemplateCatalog.java 253 0549f405ed971fd795f9dc9d9d96a9e426e1fb60 main/java/dev/caskeleton/adapter/outbound/notification/template/NotificationTemplateManifest.java 175 79524df5cc59327e111645de9010197b4cb526a0 main/java/dev/caskeleton/adapter/outbound/notification/template/NotificationTemplateRenderer.java 10 de1aa1eebb973d60204fc214e8f7379433ee9d69 main/java/dev/caskeleton/adapter/outbound/notification/template/RenderedNotification.java 112 3e61ef2951ac929a0be7e22978156c57b1db0d9b main/java/dev/caskeleton/adapter/outbound/notification/template/SlackBlockKitRenderer.java 124 7627cf28e4ce66e34cc0e7d1466ae4a852efda7f main/java/dev/caskeleton/adapter/outbound/notification/template/TemplateRenderingException.java 15 4e9c525557909ddc38910e77322714a60e5bf2c9 test/java/dev/caskeleton/adapter/outbound/notification/catalog/NotificationBindingCompilerTest.java 489 d0f1992101e31137cf7e50f1480f4c66be10ff3f test/java/dev/caskeleton/adapter/outbound/notification/catalog/NotificationCanonicalRouteCatalogTest.java 54 e162f1a8c256cc78986e97e2b1efcbd0f497fc79 test/java/dev/caskeleton/adapter/outbound/notification/catalog/NotificationCutoverRouteCatalogTest.java 123 979fe69fa706843c2a748a859b981b7b6bb3e88e test/java/dev/caskeleton/adapter/outbound/notification/template/NotificationTemplateRendererTest.java 390 === OWNED FILES (sub-scope 03) === d97258758e960640cc23e46b30ddd29b32f2d046 main/java/dev/caskeleton/adapter/outbound/notification/platform/dispatch/AttemptPermit.java 17 e565164aed62148e5d5c97aae536c7f30f850014 main/java/dev/caskeleton/adapter/outbound/notification/platform/dispatch/CapabilityReconciliationGateway.java 52 009388b82da767c2f96d36721f9f96ba492ccf24 main/java/dev/caskeleton/adapter/outbound/notification/platform/dispatch/ConfiguredProfileCatalog.java 31 b41729af6a8adf105d2cb005a5336ab404a01a2e main/java/dev/caskeleton/adapter/outbound/notification/platform/dispatch/CredentialProbe.java 9 20c658fd89c69e530a8f2fd6accd24f67f94a281 main/java/dev/caskeleton/adapter/outbound/notification/platform/dispatch/CredentialValidationException.java 19 6693cd0cbe52d2f4b425f7c4f221cbd85c1ccaf0 main/java/dev/caskeleton/adapter/outbound/notification/platform/dispatch/JacksonRoutingPlanCodec.java 61 ea00a346eeebafb0d177141e459c0998a9f2af07 main/java/dev/caskeleton/adapter/outbound/notification/platform/dispatch/LeaseRecoveryService.java 144 24e24f3cc32ed115ba61461f76e57b6beaba2d06 main/java/dev/caskeleton/adapter/outbound/notification/platform/dispatch/LoggingInboxSignalPublisher.java 29 b2e3d8dbd976dab548210b801f255a7dfce5b9bb main/java/dev/caskeleton/adapter/outbound/notification/platform/dispatch/MapTemplateRendererRegistry.java 31 3187117e3e1e0479a2a8cc4a1cdea4c9e3b0d5d7 main/java/dev/caskeleton/adapter/outbound/notification/platform/dispatch/NotificationBackgroundWorkers.java 141 63ff2c75298ae8aeb05b4996feacc85481db5802 main/java/dev/caskeleton/adapter/outbound/notification/platform/dispatch/NotificationDispatchProperties.java 55 d761c92316165a52e85e1014777063b5ebf122d9 main/java/dev/caskeleton/adapter/outbound/notification/platform/dispatch/NotificationSchedulerWorker.java 197 2d3e20668e901868f84112567b4a0b6719f69407 main/java/dev/caskeleton/adapter/outbound/notification/platform/dispatch/ProviderAttemptLimiter.java 102 050f8ac3c0272ddeb96d5c44bf8134d8e0c2e659 main/java/dev/caskeleton/adapter/outbound/notification/platform/dispatch/ProviderEventReplayWorker.java 77 61e84aa2f97184dbe1cfb04ecbdcebd0f1e820bd main/java/dev/caskeleton/adapter/outbound/notification/platform/dispatch/ProviderRuntime.java 276 93b652bcf9806dd8fe8a660c8bb999f91b9deff0 main/java/dev/caskeleton/adapter/outbound/notification/platform/dispatch/ProviderRuntimeRegistry.java 140 c9cd5ec389c5bb3ece5af8c05a747cdd280bf30f main/java/dev/caskeleton/adapter/outbound/notification/platform/dispatch/ProviderRuntimeRotator.java 69 807b6f1df09c4dcae4b8e9cd2f04de3aa55221cc main/java/dev/caskeleton/adapter/outbound/notification/platform/dispatch/ReconciliationJobWorker.java 132 9249058d4eabaf9e6c68404350328fcea1463eb8 main/java/dev/caskeleton/adapter/outbound/notification/platform/dispatch/RegistryProviderDispatchGateway.java 76 b5ad665c4752288a260609acc3af2fdadf672461 main/java/dev/caskeleton/adapter/outbound/notification/platform/dispatch/RegistryProviderRuntimeControl.java 50 8db03d0736babf4eafa5043caabaad0de9d08045 main/java/dev/caskeleton/adapter/outbound/notification/platform/dispatch/RuntimeDrainCoordinator.java 46 4b0fae795e4a49db9cb29836cf4bcfbb10b1d64f main/java/dev/caskeleton/adapter/outbound/notification/platform/dispatch/SingleTenantContext.java 27 938430f0e355959ec427b43212ee8cb1176c1070 main/java/dev/caskeleton/adapter/outbound/notification/platform/dispatch/UuidV7Generator.java 54 0fd3c92679909482c3df8e44e1aa6d818d6f3080 test/java/dev/caskeleton/adapter/outbound/notification/platform/dispatch/LeaseRecoveryServiceTest.java 324 96670aa796637412e44af63443abda5096f9ec7e test/java/dev/caskeleton/adapter/outbound/notification/platform/dispatch/ProviderAttemptLimiterConcurrencyTest.java 193 30391872da4734092c3183df872dd002c22b6efe test/java/dev/caskeleton/adapter/outbound/notification/platform/dispatch/ProviderRuntimeRegistryConcurrencyTest.java 194 46a8ac0fafc9355c5c1d33c735730367ba6ce8be test/java/dev/caskeleton/adapter/outbound/notification/platform/dispatch/ProviderRuntimeRegistryTest.java 192 6fe877abe56e01992ebdabace964dce28ea0b8d1 test/java/dev/caskeleton/adapter/outbound/notification/platform/dispatch/ProviderRuntimeRotationTest.java 96 44f27b336d0675c619f11054eea30877706efc7e test/java/dev/caskeleton/adapter/outbound/notification/platform/dispatch/ProviderRuntimeStateTest.java 241 01101163c77558ebc864f75705f3b6e2b673ad81 test/java/dev/caskeleton/adapter/outbound/notification/platform/dispatch/ReconciliationJobWorkerTest.java 180 === 8.1 렌더링 컨텍스트 사슬 === $ grep -n 'case "' adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/template/LocalEmailRenderer.java | head -20 182: case "html_text" -> { 188: case "html_attr" -> { 194: case "url_component" -> { 224: case "header" -> { 231: case "text" -> raw; 232: case "html_text" -> html(raw); 233: case "html_attr" -> htmlAttribute(template, matcher, raw); 234: case "url_component" -> { exit=0 $ grep -n 'APPROVED_HTML_TAGS =' -A 6 adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/template/LocalEmailRenderer.java 20: private static final java.util.Set APPROVED_HTML_TAGS = 21- java.util.Set.of( 22- "a", "b", "body", "br", "div", "em", "h1", "h2", "h3", "h4", "h5", "h6", "head", "html", 23- "i", "img", "li", "ol", "p", "span", "strong", "table", "tbody", "td", "th", "thead", 24- "tr", "ul"); 25- private static final java.util.Set APPROVED_INERT_ATTRIBUTES = 26- java.util.Set.of("alt", "aria-label", "title"); exit=0 $ grep -n 'quoteReplacement\|unresolved parameter\|line break' adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/template/LocalEmailRenderer.java 62: throw new TemplateRenderingException("rendered email header contains a line break"); 227: "rendered email header parameter contains a line break"); 243: matcher.appendReplacement(output, Matcher.quoteReplacement(replacement)); 247: throw new TemplateRenderingException("template has an unresolved parameter"); exit=0 === 8.2 템플릿 자산이 체크섬으로 고정되는가 === $ grep -n 'checksum' adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/template/NotificationTemplateCatalog.java | head -10 20: * Loads only manifest-named classpath assets, validates exact checksums, and retains immutable 52: String actual = checksumLoaded(sources); 53: if (!actual.equals(manifest.checksum())) { 54: throw new TemplateRenderingException("template manifest checksum mismatch"); 92: public static String checksum( 96: return checksumLoaded( 125: private static String checksumLoaded(Map sources) { exit=0 $ grep -n 'templateChecksum\|rendererRevision' adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/template/LocalEmailRenderer.java 93: if (!plan.binding().templateChecksum().equals(manifest.checksum())) { 96: if (!plan.binding().rendererRevision().equals(manifest.rendererRevision())) { exit=0 === 8.3 배경 작업자가 실제로 스케줄되는가 (과거 미배선 수정) === $ git grep -rn 'NotificationBackgroundWorkers\|LeaseRecoveryService\|ProviderEventReplayWorker\|ReconciliationJobWorker' -- 'adapter/outbound/notification/src/main' 'app-bootstrap/src/main' | grep -v 'platform/dispatch/' | head app-bootstrap/src/main/java/dev/caskeleton/bootstrap/notification/NotificationPlatformWorkerConfig.java:3:import dev.caskeleton.adapter.outbound.notification.platform.dispatch.LeaseRecoveryService; app-bootstrap/src/main/java/dev/caskeleton/bootstrap/notification/NotificationPlatformWorkerConfig.java:4:import dev.caskeleton.adapter.outbound.notification.platform.dispatch.NotificationBackgroundWorkers; app-bootstrap/src/main/java/dev/caskeleton/bootstrap/notification/NotificationPlatformWorkerConfig.java:7:import dev.caskeleton.adapter.outbound.notification.platform.dispatch.ProviderEventReplayWorker; app-bootstrap/src/main/java/dev/caskeleton/bootstrap/notification/NotificationPlatformWorkerConfig.java:8:import dev.caskeleton.adapter.outbound.notification.platform.dispatch.ReconciliationJobWorker; app-bootstrap/src/main/java/dev/caskeleton/bootstrap/notification/NotificationPlatformWorkerConfig.java:57: public ReconciliationJobWorker reconciliationJobWorker( app-bootstrap/src/main/java/dev/caskeleton/bootstrap/notification/NotificationPlatformWorkerConfig.java:63: return new ReconciliationJobWorker( app-bootstrap/src/main/java/dev/caskeleton/bootstrap/notification/NotificationPlatformWorkerConfig.java:90: public LeaseRecoveryService notificationLeaseRecoveryService( app-bootstrap/src/main/java/dev/caskeleton/bootstrap/notification/NotificationPlatformWorkerConfig.java:99: return new LeaseRecoveryService( app-bootstrap/src/main/java/dev/caskeleton/bootstrap/notification/NotificationPlatformWorkerConfig.java:119: public ProviderEventReplayWorker notificationProviderEventReplayWorker( app-bootstrap/src/main/java/dev/caskeleton/bootstrap/notification/NotificationPlatformWorkerConfig.java:124: return new ProviderEventReplayWorker(ledger, projection, properties.claimBatchSize()); exit=0 === 8.4 dispatch test === $ git ls-files 'adapter/outbound/notification/src/test/java/dev/caskeleton/adapter/outbound/notification/platform/dispatch' 'adapter/outbound/notification/src/test/java/dev/caskeleton/adapter/outbound/notification/catalog' 'adapter/outbound/notification/src/test/java/dev/caskeleton/adapter/outbound/notification/template' | xargs -n1 basename NotificationBindingCompilerTest.java NotificationCanonicalRouteCatalogTest.java NotificationCutoverRouteCatalogTest.java LeaseRecoveryServiceTest.java ProviderAttemptLimiterConcurrencyTest.java ProviderRuntimeRegistryConcurrencyTest.java ProviderRuntimeRegistryTest.java ProviderRuntimeRotationTest.java ProviderRuntimeStateTest.java ReconciliationJobWorkerTest.java NotificationTemplateRendererTest.java exit=0 $ git grep -h ' void [a-z]' -- 'adapter/outbound/notification/src/test/java/dev/caskeleton/adapter/outbound/notification/platform/dispatch' | sed 's/^ *//' | head -30 void aDeliveryWithNoAttemptIsRequeued() { void aDeliveryWithAnIncompleteAttemptIsReconciled() { void anAttemptProvenNeverStartedIsRequeued() { void anAttemptThatMayHaveStartedIsReconciled() { void theDecisionDependsOnCertaintyNotValueAlone() { void recoveryAlsoRetiresExpiredDeliveries() { void advanceOneSecond() { void oneSecondAdmitsExactlyTheRate() throws InterruptedException { void theWindowAfterARolloverAdmitsTheRateOnce() throws InterruptedException { void aRefusedSlotIsReturnedToItsOwnWindow() { void aRefundDoesNotCreditTheNextWindow() { void registeringTwiceIsReported() { void anOlderGenerationIsRefused() { void concurrentRotationsLeaveTheHighestGenerationCurrent() throws InterruptedException { void aDrainingGenerationWithWorkInFlightSurvivesASweep() throws InterruptedException { void authenticationFailureRejectsNewAttemptsWithoutConsumingPermit() { void replacementKeepsOldRuntimeDrainingUntilItsAttemptsFinish() { void newAttemptUsesNewGenerationWhileOldAttemptDrains() { void failedNewCredentialKeepsOldRuntimeActive() { void concurrencyLimitFailsFastRatherThanQueueing() { void rotationAuditRecordsGenerationButNoCredentialMaterial() { void activationRecordsTheGenerationAndItsKeyIdButNeverTheMaterial() { void aGenerationThatDoesNotSupersedeTheActiveOneIsRefused() { void anUnresolvableKeyIdFailsTheRotationRatherThanTheFirstNotification() { void aKeyIssuedForAnotherPurposeCannotBecomeAProviderCredential() { void contactEncryptionAndVapidRotationAreRefusedWithTheMigrationTheyActuallyNeed() { void materialIsResolvedPerCallSoARotationIsNotOutlivedByACachedCredential() { void closingAPermitTwiceDoesNotRaiseTheCeiling() { void concurrentDoubleClosesReleaseOneSlotEach() throws InterruptedException { void aSuccessClearsThrottlingAndDegradation() { exit=0