Files
DongHyeonkaandClaude Opus 5 b2963105a8 docs(keycloak-session-store): import the session-storage lab as a new project
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>
2026-09-04 22:51:59 +09:00

466 lines
19 KiB
Plaintext

adapter-inbound-web module inventory / denominator
revision=a24ece9cf797f7ea647e33bf846b115208ed1ba5
generatedAt=2026-08-30T07:06:20+00:00
=== TRACKED FILE COUNTS ===
$ git ls-files adapter/inbound/web | wc -l
638
exit=0
$ git ls-files adapter/inbound/web/src/main | wc -l
400
exit=0
$ git ls-files adapter/inbound/web/src/test | wc -l
150
exit=0
$ git ls-files adapter/inbound/web | grep -v '^adapter/inbound/web/src/'
adapter/inbound/web/CLAUDE.md
adapter/inbound/web/README.md
adapter/inbound/web/build.gradle
adapter/inbound/web/gradle.lockfile
exit=0
=== MAIN JAVA LOC ===
$ git ls-files 'adapter/inbound/web/src/main/**/*.java' | xargs wc -l | tail -1
27473 total
exit=0
$ git ls-files 'adapter/inbound/web/src/test/**/*.java' | xargs wc -l | tail -1
18319 total
exit=0
$ git ls-files adapter/inbound/web/src/main | grep -v '\.java$'
adapter/inbound/web/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
adapter/inbound/web/src/main/resources/META-INF/web/problem-catalog.yaml
adapter/inbound/web/src/main/resources/META-INF/web/wire-type-manifest.yaml
exit=0
$ git ls-files adapter/inbound/web/src/test | grep -v '\.java$'
adapter/inbound/web/src/test/java/dev/caskeleton/adapter/inbound/web/contract/.gitkeep
adapter/inbound/web/src/test/java/dev/caskeleton/adapter/inbound/web/mvc/.gitkeep
adapter/inbound/web/src/test/resources/application-web-contract.yaml
exit=0
=== MAIN PACKAGE TREE (file counts) ===
$ git ls-files 'adapter/inbound/web/src/main/**/*.java' | sed 's|.*/web/||;s|/[^/]*$||' | sort | uniq -c | sort -rn
19 pagination
18 ratelimit
16 error
13 advanced/stream
13 advanced/patch
12 observability
12 http
11 security
11 operation
11 auth
10 fileserver/reactive
10 conditional
9 operationasync
9 idempotency
9 core
7 versioning
7 openapi
7 notification/platform/submission
7 budget
7 advanced/webflux
7 advanced/stream/encoding
6 evidence
6 advanced/codec
5 notification/platform/callback
5 fileserver/tus
5 fileserver/problem
5 fileserver/dto
5 fileserver/draft12
5 authz
5 advanced/stream/replay
5 advanced/openapi
5 advanced/mvc
5 admission
4 validation
4 proxy
4 notification/platform/callback/reactive
4 mvc/budget
4 json
4 fileserver/nginx
4 fileserver/config
4 contract
4 cache
4 advanced/functional
4 admin/route
3 settings
3 moduleboundary
3 fileserver/mapper
3 fileserver/http
3 fileserver/admin
3 config
3 advanced/virtualthread
3 advanced/ratelimit
3 advanced/blockingbridge
2 webflux/guard
2 webflux/context
2 webflux/budget
2 webflux/autoconfigure
2 mvc/idempotency
2 mvc/filter
2 mvc/context
2 mvc/autoconfigure
2 filter
2 fileserver/controller
2 cursor
2 advanced/release
2 advanced
2 admin/platform
1 webflux/throttle
1 webflux/operation
1 webflux/idempotency
1 webflux/error
1 mvc/throttle
1 mvc/operation
1 mvc/error
1 fileserver/security
1 fileserver/lifecycle
1 envelope
1 controller
exit=0
=== TEST PACKAGE TREE ===
$ git ls-files 'adapter/inbound/web/src/test/**/*.java' | sed 's|.*/web/||;s|/[^/]*$||' | sort | uniq -c | sort -rn
11 auth
8 fileserver/testkit
7 ratelimit
7 error
5 pagination
5 observability
4 idempotency
4 authz
3 settings
3 moduleboundary
3 http
3 fileserver/reactive
3 conditional
3 advanced/release
3 advanced/codec
2 versioning
2 testkit/contract
2 security
2 operationasync
2 openapi
2 notification/platform/callback
2 mvc/filter
2 fixtures/badcontroller
2 filter
2 fileserver/nginx
2 fileserver/controller
2 envelope
2 core
2 advanced/stream/replay
2 advanced/stream
2 advanced/patch
2 advanced/openapi
2 advanced/mvc
1 webflux/idempotency
1 webflux/guard
1 webflux/context
1 webflux/autoconfigure
1 validation
1 testkit/throttle
1 testkit/performance
1 testkit/order
1 testkit/operation
1 testkit/mvc
1 testkit/fault
1 testkit/budget
1 testkit/arch
1 proxy
1 operation
1 mvc/idempotency
1 mvc/autoconfigure
1 json
1 fileserver/tus
1 fileserver/security
1 fileserver/problem
1 fileserver/http
1 fileserver/draft12
1 fileserver/config
1 fileserver/admin
1 evidence
1 cursor
1 contract
1 config
1 cache
1 budget
1 advanced/webflux
1 advanced/virtualthread
1 advanced/stream/encoding
1 advanced/ratelimit
1 advanced/functional
1 advanced/blockingbridge
1 admin/route
1 admin/platform
exit=0
=== BUILD ===
$ cat adapter/inbound/web/build.gradle
// The inbound HTTP API execution platform design models itself as 23 Stable Gradle modules under
// modules/web. This repository's fail-closed module registry outranks that layout, so those modules
// are packages here and WebModuleBoundaryTest enforces the design's module dependency table. The
// full mapping, and the three other places the design's assumptions were adapted, are in
// docs/web/repository-adaptation.md.
// HTTP / web adapters. Depends on application and shared operational contracts.
dependencies {
implementation project(':application-core')
implementation project(':shared-contract')
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.springframework.boot:spring-boot-starter-oauth2-resource-server'
implementation 'org.springframework.boot:spring-boot-starter-validation'
implementation 'org.springframework.session:spring-session-core'
annotationProcessor 'org.springframework.boot:spring-boot-configuration-processor'
implementation('org.openapitools:jackson-databind-nullable:0.2.6') {
exclude group: 'com.fasterxml.jackson.core', module: 'jackson-databind'
}
// feature-api-contract-baseline D10: OpenAPI producer. springdoc exposes the
// running app's machine-readable contract at /v3/api-docs (OAS 3.1, generated —
// never a hand-maintained stale schema). The release-blocking drift gate is
// owned by feature-contract-verification-test-suite (planned).
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-api:3.0.0'
// The two Advanced representations, compile-only on purpose. They were `implementation` first,
// on the reasoning that a missing backend would surface as a NoClassDefFoundError at the first
// request that negotiated one. That reasoning was wrong about what the jars do: Spring Boot's
// Jackson auto-configuration registers an `xmlMapper` and a `cborMapper` the moment each
// backend is on the runtime classpath, and Spring registers a message converter for XML with
// it. So every deployment got three ObjectMapper beans — which broke `@Autowired ObjectMapper`
// in the composition root with an ambiguity — and, worse, silently began parsing
// `application/xml` request bodies. An Advanced capability that is off by default had turned
// XML deserialization on for everybody, which is the opposite of what the flag promises and an
// XXE surface nobody chose.
//
// Compile-only keeps the factories compiling and their tests running while leaving the runtime
// classpath to the deployment that enables the capability. `WebRepresentation.available()` is
// what turns the absent backend into a sentence instead of a NoClassDefFoundError.
compileOnly 'tools.jackson.dataformat:jackson-dataformat-cbor'
compileOnly 'tools.jackson.dataformat:jackson-dataformat-xml'
testImplementation 'tools.jackson.dataformat:jackson-dataformat-cbor'
testImplementation 'tools.jackson.dataformat:jackson-dataformat-xml'
// Fileserver reactive transport. Only the WebFlux framework and Reactor core are declared —
// deliberately not spring-boot-starter-webflux, which would put a second embedded server
// (reactor-netty) on the runtime classpath. DispatcherServlet stays present, so Spring Boot's
// WebApplicationType deduction keeps resolving SERVLET; the reactive handlers are wired only
// when the fileserver reactive profile is selected.
implementation 'org.springframework:spring-webflux'
implementation 'io.projectreactor:reactor-core'
testImplementation 'org.springframework.security:spring-security-test'
testImplementation 'io.projectreactor:reactor-test'
}
tasks.named('test') {
useJUnitPlatform {
excludeTags 'security-boundary'
// The parity gate compares recordings written by three lanes. In `test` alone only one of
// them exists, and a gate that fails because the others have not run yet is a gate people
// learn to ignore. It runs from `webCrossStackParityTest`, which depends on all three.
excludeTags 'web-parity'
}
}
// The web platform's reusable ArchUnit rules ship in their own source set, consumed by this leaf's
// tests and by the composition root. A rule pack that only its own fixture tests import is verified
// as library code and applied to nothing — the shape the JPA testkit had to be corrected out of.
strictTestLanes {
sourceSet('testkit') { compilesAgainst 'main' }
// The Jetty compatibility lane is its own source set because it needs a different embedded
// server on the classpath. Two servers in one source set means Spring Boot picks one and the
// "Jetty" lane silently runs on Tomcat — a compatibility matrix that certifies the same
// container twice.
sourceSet('jettyCompatTest') {
compilesAgainst 'main', 'testkit'
inherits 'implementation'
}
// Reactor Netty is the Stable WebFlux server baseline and it cannot share a source set with
// Tomcat: Spring Boot deduces one application type from the classpath, so with both servers
// present the reactive gate would start a servlet container and certify nothing reactive.
// Nothing is inherited: the leaf's own `implementation` carries spring-boot-starter-web, and
// inheriting it would put Tomcat back on this lane's classpath. Boot then deduces a servlet
// application, starts a servlet container, and the reactive gate certifies the servlet stack.
// The Nginx lane runs the platform behind a real reverse proxy in a container. Its own source
// set because it is the only lane that needs Docker: folding it into `test` would make every
// developer's `check` depend on a container runtime, and the usual outcome of that is an
// @Disabled that nobody notices has been there for months.
sourceSet('nginxProxyTest') {
compilesAgainst 'main', 'testkit'
inherits 'implementation'
}
sourceSet('webfluxContractTest') {
compilesAgainst 'main', 'testkit'
// Inherits nothing. The default is to extend `testImplementation`, which extends the leaf's
// own `implementation` and therefore carries spring-boot-starter-web — and with Tomcat on
// the classpath Boot deduces a servlet application, starts a servlet container, and the
// reactive gate certifies the servlet stack while reporting itself green.
inherits()
}
}
testkitPublisher {
consumedBy 'test'
publishAs 'webTestkit'
}
// ArchUnit is declared after the testkit source set exists, because `testkitImplementation` is
// created by that declaration.
dependencies {
testkitImplementation 'com.tngtech.archunit:archunit-junit5:1.3.0'
testImplementation 'com.tngtech.archunit:archunit-junit5:1.3.0'
// Jetty replaces Tomcat for this lane only. The exclusion is what makes the lane mean
// something: with both on the classpath Boot starts Tomcat and the lane certifies nothing.
jettyCompatTestImplementation('org.springframework.boot:spring-boot-starter-jetty')
jettyCompatTestImplementation('org.springframework.boot:spring-boot-starter-test') {
exclude group: 'org.springframework.boot', module: 'spring-boot-starter-tomcat'
}
jettyCompatTestImplementation('org.springframework.boot:spring-boot-starter-web') {
exclude group: 'org.springframework.boot', module: 'spring-boot-starter-tomcat'
}
jettyCompatTestImplementation 'org.springframework.boot:spring-boot-starter-security'
jettyCompatTestImplementation 'org.springframework.boot:spring-boot-starter-validation'
// The root build adds the launcher to `test` only; a custom lane has to say so itself, or the
// executor starts and finds no JUnit Platform.
jettyCompatTestRuntimeOnly 'org.junit.platform:junit-platform-launcher'
// The reactive lane: reactor-netty in, Tomcat out. The exclusion is what makes the lane mean
// something — Boot picks the servlet stack when both are present.
webfluxContractTestImplementation('org.springframework.boot:spring-boot-starter-webflux') {
exclude group: 'org.springframework.boot', module: 'spring-boot-starter-tomcat'
}
webfluxContractTestImplementation('org.springframework.boot:spring-boot-starter-test') {
exclude group: 'org.springframework.boot', module: 'spring-boot-starter-tomcat'
}
webfluxContractTestImplementation 'org.springframework.boot:spring-boot-starter-validation'
webfluxContractTestImplementation 'io.projectreactor:reactor-test'
// Explicit because this lane inherits nothing: the platform's auto-configuration references
// ObjectMapper, and without Jackson the condition evaluation fails before a server starts.
webfluxContractTestImplementation 'org.springframework.boot:spring-boot-starter-jackson'
webfluxContractTestImplementation 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310'
webfluxContractTestImplementation project(':application-core')
webfluxContractTestImplementation project(':shared-contract')
webfluxContractTestRuntimeOnly 'org.junit.platform:junit-platform-launcher'
// The Nginx lane. Testcontainers starts the proxy; the application runs in this JVM on a
// random port and the container reaches it through the host gateway.
nginxProxyTestImplementation 'org.springframework.boot:spring-boot-starter-test'
nginxProxyTestImplementation 'org.springframework.boot:spring-boot-starter-web'
nginxProxyTestImplementation 'org.springframework.boot:spring-boot-starter-security'
nginxProxyTestImplementation 'org.springframework.boot:spring-boot-starter-validation'
nginxProxyTestImplementation 'org.testcontainers:testcontainers'
nginxProxyTestImplementation 'org.testcontainers:testcontainers-junit-jupiter'
nginxProxyTestRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}
// The lane task. A release compatibility gate that is not wired to a task is a document.
tasks.register('webFluxContractTest', Test) {
group = 'verification'
description = 'Runs the Stable HTTP contract against a real Reactor Netty.'
testClassesDirs = sourceSets.webfluxContractTest.output.classesDirs
classpath = sourceSets.webfluxContractTest.runtimeClasspath
useJUnitPlatform()
failOnNoDiscoveredTests = true
outputs.upToDateWhen { false }
jvmArgs '-Duser.timezone=UTC'
}
// Docker-gated, and it says so rather than skipping. A lane that quietly passes when the container
// runtime is missing is a lane that has been certifying nothing since whenever Docker last broke.
tasks.register('webNginxProxyTest', Test) {
group = 'verification'
description = 'Runs the proxy, prefix and spoofing contract behind a real Nginx.'
testClassesDirs = sourceSets.nginxProxyTest.output.classesDirs
classpath = sourceSets.nginxProxyTest.runtimeClasspath
useJUnitPlatform()
failOnNoDiscoveredTests = true
outputs.upToDateWhen { false }
jvmArgs '-Duser.timezone=UTC'
}
// The cross-stack gate. It depends on every recording lane rather than tolerating a missing one:
// a parity check that compares whatever happens to be present would report agreement across a
// matrix with a hole in it.
tasks.register('webCrossStackParityTest', Test) {
group = 'verification'
description = 'Compares the wire contract recorded by Tomcat, Jetty and Reactor Netty.'
testClassesDirs = sourceSets.test.output.classesDirs
classpath = sourceSets.test.runtimeClasspath
useJUnitPlatform {
includeTags 'web-parity'
}
failOnNoDiscoveredTests = true
outputs.upToDateWhen { false }
jvmArgs '-Duser.timezone=UTC'
dependsOn 'test', 'webJettyCompatTest', 'webFluxContractTest'
}
// The Advanced lane. Every capability is off unless a deployment names it, so none of them is
// exercised by anything a production deployment runs — which makes a lane that runs them all the
// only place a break is noticed before whoever enables it notices.
//
// They also run inside `test`, deliberately. They are ordinary unit tests, and excluding them from
// the PR gate to make this lane look meaningful would mean the PR gate stopped covering a fifth of
// the leaf.
tasks.register('webAdvancedTest', Test) {
group = 'verification'
description = 'Runs every web Advanced capability contract.'
testClassesDirs = sourceSets.test.output.classesDirs
classpath = sourceSets.test.runtimeClasspath
useJUnitPlatform {
includeTags 'web-advanced'
}
failOnNoDiscoveredTests = true
outputs.upToDateWhen { false }
jvmArgs '-Duser.timezone=UTC'
}
tasks.register('webJettyCompatTest', Test) {
group = 'verification'
description = 'Runs the Stable HTTP contract against a real Jetty instead of Tomcat.'
testClassesDirs = sourceSets.jettyCompatTest.output.classesDirs
classpath = sourceSets.jettyCompatTest.runtimeClasspath
useJUnitPlatform()
failOnNoDiscoveredTests = true
outputs.upToDateWhen { false }
jvmArgs '-Duser.timezone=UTC'
}
strictTestLanes {
lane('jpaPersistenceRedactionContractTest') {
description = 'Runs the exact persistence error log/trace redaction contract used by JPA evidence.'
// Named rather than tagged. This lane is JPA evidence's proof that a database failure never
// reaches a log line or a span, and it must stay exactly these two contracts — a tag would
// let a later test opt itself in and change what the evidence covers.
requires(
'dev.caskeleton.adapter.inbound.web.error.GlobalExceptionHandlerTest.persistenceFailureObservabilityDoesNotCarryRawDatabaseDetails',
'dev.caskeleton.adapter.inbound.web.error.SpanErrorRecorderHookTest.persistenceFailureHandlerRecordsSanitizedExceptionWithClassifiedCode')
}
lane('webSecurityBoundaryTest') {
tag = 'security-boundary'
description = 'Runs hermetic JWT/JWKS and CORS filter-boundary contracts with no skips.'
customize = { test ->
test.shouldRunAfter test.project.tasks.named('test')
test.jvmArgs '-Duser.timezone=UTC'
test.afterSuite { descriptor, result ->
if (descriptor.parent == null && result.skippedTestCount > 0) {
throw new GradleException(
"webSecurityBoundaryTest forbids skipped tests: ${result.skippedTestCount}")
}
}
}
}
}
tasks.named('check') {
dependsOn tasks.named('webSecurityBoundaryTest')
}
exit=0