The keycloak project ended with four open questions that design could not
settle. A two-VM lab was built to answer them by measurement, and this is
that material: 26 experiments, 125 raw command outputs, 22 browser captures.
Follows the import procedure in README.md.
source/ the originating repository verbatim — 78 documents, 28 SVGs,
8 manifests, plus .source-revision recording the commit
final/ the SSOT
document.md 729 lines written from the 29 experiment documents, not
concatenated: what was predicted, what was measured, and
where the measurement itself was wrong
evidence/raw 125 outputs, flattened to <experiment>__<file> because
the originals collided (01-baseline.txt appeared three
times) and the audit only globs the top level
evidence/meta one per raw file; command and exitCode are null and the
README says why rather than inventing them
evidence/browser 22 captures
assets/ three diagrams through techviz
.techviz/ their VizSpecs
A separate project rather than an addition to keycloak: the B-layer answers
that project's four questions, but the A, C and D layers are about cluster
failure, SSO and operations, and one document.md should hold one subject.
The four question records there can point here through 관계.
Recorded rather than papered over: only three of the 28 diagrams were
remade. The repository forbids hand-drawn SVG and forbids titles inside the
canvas; all 28 originals carry both, so converting them is redrawing, not
reformatting. They stay in source/ and the gap is written into the document.
verify-pipeline.py passes. audit-records.py reports no issues.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
168 lines
15 KiB
Plaintext
168 lines
15 KiB
Plaintext
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<String> 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<String> 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<Locale, LoadedResourceSet> 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
|
|
|