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