adapter-outbound-httpclient — 모듈 인벤토리
revision=a24ece9cf797f7ea647e33bf846b115208ed1ba5
generatedAt=2026-08-30T05:19:48+00:00

=== tracked file 총계 ===
$ git ls-files 'adapter/outbound/httpclient' | wc -l
370
exit=0

$ for d in $(git ls-files 'adapter/outbound/httpclient/src' | sed 's|^adapter/outbound/httpclient/src/||' | cut -d/ -f1 | sort -u); do n=$(git ls-files "adapter/outbound/httpclient/src/$d" | wc -l); l=$(git ls-files "adapter/outbound/httpclient/src/$d" | xargs wc -l 2>/dev/null | tail -1 | awk '{print $1}'); echo "$d: $n files, $l LOC"; done
httpClientPerformanceTest: 7 files, 495 LOC
jmh: 2 files, 130 LOC
main: 260 files, 15004 LOC
test: 62 files, 6049 LOC
testkit: 35 files, 2754 LOC
exit=0

=== main 패키지 배치 ===
$ git ls-files 'adapter/outbound/httpclient/src/main' | grep '\.java$' | sed 's|.*/dev/caskeleton/||' | xargs -n1 dirname | sort | uniq -c | sort -rn
     39 adapter/outbound/httpclient/resilience
     29 adapter/outbound/httpclient/profile
     23 adapter/outbound/httpclient/api/error
     21 adapter/outbound/httpclient/security
     19 adapter/outbound/httpclient/restclient
     18 adapter/outbound/httpclient/auth
     17 adapter/outbound/httpclient/webclient
     14 adapter/outbound/httpclient/service
     11 adapter/outbound/httpclient/dynamic
     10 adapter/outbound/httpclient/transport
      9 adapter/outbound/httpclient/api/result
      7 adapter/outbound/httpclient/api/body
      6 adapter/outbound/httpclient/reactor
      6 adapter/outbound/httpclient/observation
      6 adapter/outbound/httpclient/api/operation
      5 adapter/outbound/httpclient/api
      5 adapter/outbound/httpclient/apache
      4 adapter/outbound/httpclient/migration
      4 adapter/outbound/httpclient/jdk
      4 adapter/outbound/httpclient/http3
      3 adapter/outbound/httpclient/spring7
exit=0

=== test 패키지 배치 ===
$ git ls-files 'adapter/outbound/httpclient/src/test' | grep '\.java$' | sed 's|.*/dev/caskeleton/||' | xargs -n1 dirname | sort | uniq -c | sort -rn
      8 adapter/outbound/httpclient/resilience
      7 adapter/outbound/httpclient/contract
      5 adapter/outbound/httpclient/webclient
      5 adapter/outbound/httpclient/service
      5 adapter/outbound/httpclient/security
      5 adapter/outbound/httpclient/restclient
      3 adapter/outbound/httpclient/architecture
      2 adapter/outbound/httpclient/testkit
      2 adapter/outbound/httpclient/profile
      2 adapter/outbound/httpclient/observation
      2 adapter/outbound/httpclient/migration
      2 adapter/outbound/httpclient/jdk
      2 adapter/outbound/httpclient/dynamic
      2 adapter/outbound/httpclient/auth
      2 adapter/outbound/httpclient/api/body
      2 adapter/outbound/httpclient/apache
      1 adapter/outbound/httpclient/transport
      1 adapter/outbound/httpclient/spring7
      1 adapter/outbound/httpclient/http3
      1 adapter/outbound/httpclient/api/operation
      1 adapter/outbound/httpclient/api/error
      1 adapter/outbound/httpclient/api
exit=0

=== main 상위 30 ===
$ git ls-files 'adapter/outbound/httpclient/src/main' | grep '\.java$' | xargs wc -l | sort -rn | head -31
 15004 total
   330 adapter/outbound/httpclient/src/main/java/dev/caskeleton/adapter/outbound/httpclient/webclient/DefaultReactiveHttpGateway.java
   304 adapter/outbound/httpclient/src/main/java/dev/caskeleton/adapter/outbound/httpclient/restclient/DefaultGenericHttpGateway.java
   291 adapter/outbound/httpclient/src/main/java/dev/caskeleton/adapter/outbound/httpclient/profile/ClientProfileValidator.java
   272 adapter/outbound/httpclient/src/main/java/dev/caskeleton/adapter/outbound/httpclient/service/ReactiveKernelHttpExchangeAdapter.java
   269 adapter/outbound/httpclient/src/main/java/dev/caskeleton/adapter/outbound/httpclient/restclient/BlockingAttemptExecutor.java
   228 adapter/outbound/httpclient/src/main/java/dev/caskeleton/adapter/outbound/httpclient/dynamic/DefaultDynamicTargetGateway.java
   215 adapter/outbound/httpclient/src/main/java/dev/caskeleton/adapter/outbound/httpclient/dynamic/IpAddressClassifier.java
   211 adapter/outbound/httpclient/src/main/java/dev/caskeleton/adapter/outbound/httpclient/service/KernelHttpExchangeAdapter.java
   206 adapter/outbound/httpclient/src/main/java/dev/caskeleton/adapter/outbound/httpclient/restclient/RestClientResponseReader.java
   190 adapter/outbound/httpclient/src/main/java/dev/caskeleton/adapter/outbound/httpclient/http3/JettyHttp3TransportProvider.java
   187 adapter/outbound/httpclient/src/main/java/dev/caskeleton/adapter/outbound/httpclient/profile/ClientRuntimeRegistry.java
   184 adapter/outbound/httpclient/src/main/java/dev/caskeleton/adapter/outbound/httpclient/webclient/ReactiveAttemptExecutor.java
   183 adapter/outbound/httpclient/src/main/java/dev/caskeleton/adapter/outbound/httpclient/webclient/DefaultReactiveSseGateway.java
   176 adapter/outbound/httpclient/src/main/java/dev/caskeleton/adapter/outbound/httpclient/security/TrustedTargetPolicy.java
   176 adapter/outbound/httpclient/src/main/java/dev/caskeleton/adapter/outbound/httpclient/security/TlsMaterialProvider.java
   169 adapter/outbound/httpclient/src/main/java/dev/caskeleton/adapter/outbound/httpclient/restclient/BlockingStreamingGateway.java
   165 adapter/outbound/httpclient/src/main/java/dev/caskeleton/adapter/outbound/httpclient/api/error/HttpFailureMetadata.java
   154 adapter/outbound/httpclient/src/main/java/dev/caskeleton/adapter/outbound/httpclient/service/ServiceOperationDescriptorScanner.java
   153 adapter/outbound/httpclient/src/main/java/dev/caskeleton/adapter/outbound/httpclient/apache/ApacheClientFactory.java
   152 adapter/outbound/httpclient/src/main/java/dev/caskeleton/adapter/outbound/httpclient/api/operation/HttpOperation.java
   148 adapter/outbound/httpclient/src/main/java/dev/caskeleton/adapter/outbound/httpclient/auth/SingleFlightTokenLoader.java
   144 adapter/outbound/httpclient/src/main/java/dev/caskeleton/adapter/outbound/httpclient/restclient/RestClientRuntimeFactory.java
   140 adapter/outbound/httpclient/src/main/java/dev/caskeleton/adapter/outbound/httpclient/apache/ApacheFailureClassifier.java
   137 adapter/outbound/httpclient/src/main/java/dev/caskeleton/adapter/outbound/httpclient/reactor/ReactorHttpClientFactory.java
   137 adapter/outbound/httpclient/src/main/java/dev/caskeleton/adapter/outbound/httpclient/reactor/ReactorFailureClassifier.java
   135 adapter/outbound/httpclient/src/main/java/dev/caskeleton/adapter/outbound/httpclient/resilience/AttemptResiliencePipeline.java
   133 adapter/outbound/httpclient/src/main/java/dev/caskeleton/adapter/outbound/httpclient/resilience/DefaultRetryEligibilityEngine.java
   132 adapter/outbound/httpclient/src/main/java/dev/caskeleton/adapter/outbound/httpclient/restclient/BlockingClientRuntime.java
   128 adapter/outbound/httpclient/src/main/java/dev/caskeleton/adapter/outbound/httpclient/auth/OAuth2CredentialProvider.java
   123 adapter/outbound/httpclient/src/main/java/dev/caskeleton/adapter/outbound/httpclient/webclient/WebClientRuntimeFactory.java
exit=0

=== 비 Java 파일 ===
$ git ls-files 'adapter/outbound/httpclient' | grep -v '\.java$'
adapter/outbound/httpclient/CLAUDE.md
adapter/outbound/httpclient/README.md
adapter/outbound/httpclient/build.gradle
adapter/outbound/httpclient/gradle.lockfile
exit=0

=== 레지스트리 엔트리 ===
$ python3 -c "
import json;d=json.load(open('config/architecture/modules.json'))
for m in d['modules']:
    if m['id']=='adapter-outbound-httpclient': print(json.dumps(m,ensure_ascii=False,indent=2))
"
{
  "id": "adapter-outbound-httpclient",
  "gradle_path": ":adapter:outbound:httpclient",
  "source_path": "src/adapter/outbound/httpclient",
  "allowed_dependencies": [
    "domain-core",
    "application-core",
    "shared-contract",
    "adapter-outbound-support"
  ],
  "runtime_memberships": [
    "app-bootstrap"
  ]
}
exit=0

=== build.gradle ===
$ cat -n 'adapter/outbound/httpclient/build.gradle'
     1	// Outbound HTTP Client Platform leaf — see
     2	// docs/superpowers/specs/2026-08-08-httpclient-platform-design.md (design package) and
     3	// docs/httpclient/repository-adaptation.md (how the design's 19 library modules map here).
     4	//
     5	// The design models the platform as 19 separate Gradle modules. This repository's fail-closed
     6	// module registry (src/config/architecture/modules.json) outranks that layout, so the module
     7	// boundaries are packages under dev.caskeleton.adapter.outbound.httpclient and
     8	// HttpClientModuleBoundaryTest enforces the design's module dependency table.
     9	description = 'Outbound adapter: HTTP client platform (typed clients, profiles, evidence-based retry)'
    10	
    11	dependencies {
    12	    implementation project(':application-core')
    13	    implementation project(':shared-contract')
    14	    implementation project(':adapter:outbound:support')
    15	
    16	    // Spring client layer. `httpclient-core-api` must not reach these; ArchUnit enforces it.
    17	    implementation 'org.springframework.boot:spring-boot-autoconfigure'
    18	    implementation 'org.springframework:spring-web'
    19	    implementation 'org.springframework:spring-webflux'
    20	    implementation 'io.projectreactor:reactor-core'
    21	
    22	    // Transport providers. Apache HC5 is the blocking default, JDK HttpClient is the lightweight
    23	    // alternative (JDK built-in), Reactor Netty is the reactive default, Jetty carries the
    24	    // Experimental HTTP/3 transport that the Stable starter never auto-configures.
    25	    implementation 'org.apache.httpcomponents.client5:httpclient5'
    26	    implementation 'io.projectreactor.netty:reactor-netty-http'
    27	    implementation 'org.eclipse.jetty:jetty-client'
    28	    // HTTP/3 is Experimental and off by default, so its transport is compileOnly plus a test
    29	    // dependency rather than a runtime one. It used to be `implementation`, which put the whole
    30	    // QUIC/HTTP-3/QPACK stack on every deployment's runtime classpath — megabytes and an attack
    31	    // surface — to serve a feature the Stable starter never auto-configures. A deployment that
    32	    // opts into HTTP/3 adds `org.eclipse.jetty.http3:jetty-http3-client-transport` itself, and
    33	    // Http3CapabilityReport already refuses the transport when those classes are absent, so the
    34	    // failure mode is a startup error rather than a NoClassDefFoundError mid-call.
    35	    compileOnly 'org.eclipse.jetty.http3:jetty-http3-client-transport'
    36	    testImplementation 'org.eclipse.jetty.http3:jetty-http3-client-transport'
    37	
    38	    // Resilience4j supplies the execution primitives only. HTTP retry *eligibility* is owned by
    39	    // this module (design D-09) and never delegated to a generic retry library.
    40	    implementation 'io.github.resilience4j:resilience4j-retry:2.2.0'
    41	    implementation 'io.github.resilience4j:resilience4j-circuitbreaker:2.2.0'
    42	    implementation 'io.github.resilience4j:resilience4j-ratelimiter:2.2.0'
    43	    implementation 'io.github.resilience4j:resilience4j-bulkhead:2.2.0'
    44	    implementation 'io.github.resilience4j:resilience4j-micrometer:2.2.0'
    45	
    46	    implementation 'org.springframework.security:spring-security-oauth2-client'
    47	    implementation 'com.fasterxml.jackson.core:jackson-databind'
    48	    // Jackson 3, and not optional. `RestClientResponseReader.defaultConverters()` constructs a
    49	    // `JacksonJsonHttpMessageConverter`, which is Spring 7's converter and is built on
    50	    // `tools.jackson.databind.json.JsonMapper`. javac never noticed: the constructor's signature
    51	    // does not mention the type, so compiling the call needs nothing. Loading the class does, and
    52	    // Jackson 3 reached only this leaf's test and jmh classpaths — so the reader worked in every
    53	    // test and would have thrown NoClassDefFoundError in any deployment that did not happen to
    54	    // have Jackson 3 from somewhere else. The Eclipse compiler the editor runs reported it as an
    55	    // error, which is what it is. Jackson 2 stays: two files still read `JsonNode` with it.
    56	    implementation 'tools.jackson.core:jackson-databind'
    57	    implementation 'io.micrometer:micrometer-core'
    58	    implementation 'org.slf4j:slf4j-api'
    59	
    60	
    61	    // Testkit dependencies (design §28.1 test topology). They are test-scoped so no production
    62	    // module can depend on the testkit; the testkit source set inherits them by extending
    63	    // testImplementation, and the test lanes use them directly.
    64	    testImplementation 'com.squareup.okhttp3:mockwebserver:4.12.0'
    65	    testImplementation 'com.squareup.okhttp3:okhttp-tls:4.12.0'
    66	    testImplementation 'org.testcontainers:testcontainers'
    67	    testImplementation 'org.testcontainers:testcontainers-junit-jupiter'
    68	    testImplementation 'org.testcontainers:testcontainers-toxiproxy'
    69	    testImplementation 'io.projectreactor:reactor-test'
    70	    testImplementation 'com.tngtech.archunit:archunit-junit5:1.3.0'
    71	    testImplementation 'io.projectreactor.tools:blockhound:1.0.17.RELEASE'
    72	}
    73	
    74	// The testkit is its own source set, not part of `test`, because three lanes consume it and only
    75	// one of them is a test lane. Reaching into `sourceSets.test.output` from `jmh` compiled under
    76	// Gradle and could not be modelled by the IDE at all: a source set is test source there only when
    77	// a Test task runs its output, `jmh` is driven by JavaExec, and IDE main source may not read IDE
    78	// test source — so every testkit reference in the benchmarks was an unresolved type in the editor
    79	// while the build was green. A source set nobody runs tests from is main source for all three
    80	// consumers, which is what it always was.
    81	//
    82	// Performance certification and JMH benchmarks are separate source sets for their own reason: they
    83	// are slow, they assert on resource bounds rather than behaviour, and they must never be part of
    84	// the default unit lane.
    85	strictTestLanes {
    86	    // The testkit compiles against exactly what a test does: `implementation` inheritance runs
    87	    // through testImplementation, so this is the module's own dependencies plus the test libraries.
    88	    sourceSet('testkit') { compilesAgainst 'main' }
    89	    sourceSet('httpClientPerformanceTest') { compilesAgainst 'main', 'testkit' }
    90	    sourceSet('jmh') { compilesAgainst 'main', 'testkit' }
    91	}
    92	
    93	// Every test lane compiles and runs against the testkit.
    94	sourceSets.test {
    95	    compileClasspath += sourceSets.testkit.output
    96	    runtimeClasspath += sourceSets.testkit.output
    97	}
    98	
    99	dependencies {
   100	    // Testkit dependencies (design §28.1 test topology). They stay off the production
   101	    // configurations, so no production module can depend on the testkit.
   102	    testkitImplementation 'com.squareup.okhttp3:mockwebserver:4.12.0'
   103	    testkitImplementation 'com.squareup.okhttp3:okhttp-tls:4.12.0'
   104	    testkitImplementation 'org.testcontainers:testcontainers'
   105	    testkitImplementation 'org.testcontainers:testcontainers-junit-jupiter'
   106	    testkitImplementation 'org.testcontainers:testcontainers-toxiproxy'
   107	    testkitImplementation 'io.projectreactor:reactor-test'
   108	    testkitImplementation 'com.tngtech.archunit:archunit-junit5:1.3.0'
   109	    testkitImplementation 'io.projectreactor.tools:blockhound:1.0.17.RELEASE'
   110	
   111	    jmhImplementation 'org.openjdk.jmh:jmh-core:1.37'
   112	    jmhAnnotationProcessor 'org.openjdk.jmh:jmh-generator-annprocess:1.37'
   113	}
   114	
   115	// UTF-8 is pinned for every JavaCompile task in the root build; this leaf no longer repeats it.
   116	
   117	// JMH generates its harness classes at compile time. They are not our source, so the
   118	// compile-time checker and -Werror are switched off for that source set only; applying them
   119	// would fail the build on generated code we cannot edit.
   120	tasks.named('compileJmhJava', JavaCompile) {
   121	    options.errorprone.enabled = false
   122	    options.compilerArgs.removeIf { it == '-Werror' }
   123	}
   124	
   125	// The bytecode analyser is disabled for the same generated harness, for the same reason.
   126	tasks.named('spotbugsJmh') {
   127	    enabled = false
   128	}
   129	
   130	// Takes a Test task. The parameter is left untyped because the IDE's Gradle parser has no Gradle
   131	// API on its classpath and reports the annotation as an unresolved type; Groovy dispatches the
   132	// calls below dynamically either way.
   133	Closure<Void> applyContractSelection = { task ->
   134	    // Cross-transport contract lane. The same semantic contract runs against every Stable transport;
   135	    // the transport under test is selected explicitly so a missing transport is an error, not a skip.
   136	    task.systemProperty 'httpclient.contract.transports',
   137	            (project.findProperty('httpclient.contract.transports') ?: 'apache,jdk,reactor').toString()
   138	    // Netty's strictest leak detector is on for every lane. It is only meaningful if it is actually
   139	    // live, so NettyLeakDetectionExtension asserts the level rather than trusting the flag reached
   140	    // the forked JVM.
   141	    task.systemProperty 'io.netty.leakDetection.level', 'paranoid'
   142	    // HTTP/3 is Experimental: it is never part of the default lane and never silently skipped.
   143	    task.systemProperty 'httpclient.http3.tests.enabled',
   144	            (project.findProperty('http3.tests.enabled') ?: 'false').toString()
   145	}
   146	
   147	tasks.named('test', Test) {
   148	    applyContractSelection(it)
   149	    // Two lanes are excluded from the default run for opposite reasons: the fault lane needs Docker
   150	    // and fails closed without it, and the BlockHound lane rewrites core JDK bytecode, which must
   151	    // not be imposed on every unit run.
   152	    useJUnitPlatform {
   153	        excludeTags 'quarantine', 'httpclient-fault', 'httpclient-blockhound'
   154	    }
   155	}
   156	
   157	// Four tag-selected lanes, declared rather than assembled.
   158	//
   159	// Two of them — the stable contract suite and the security suite — did not carry
   160	// failOnNoDiscoveredTests at all. Five lanes were written by copying the block above, and the
   161	// property that makes a lane mean anything was lost on two of the copies, so the cross-transport
   162	// contract suite and the SSRF/credential-leak suite would each have reported success on discovering
   163	// nothing. Declaring the lanes removes the opportunity: the convention has no opt-out.
   164	strictTestLanes {
   165	    lane('httpClientBlockHoundTest') {
   166	        tag = 'httpclient-blockhound'
   167	        description = 'Proves no platform code blocks a Reactor event loop (design §18.2, §28.6).'
   168	        customize = { test ->
   169	            applyContractSelection(test)
   170	            // BlockHound instruments already-loaded JDK classes; Java 13+ needs this to redefine them.
   171	            test.jvmArgs '-XX:+AllowRedefinitionToAddDeleteMethods'
   172	        }
   173	    }
   174	    lane('httpClientStableContractTest') {
   175	        tag = 'httpclient-contract'
   176	        description = 'Runs the cross-transport stable contract suite (design §28.2, §33).'
   177	        customize = { test -> applyContractSelection(test) }
   178	    }
   179	    lane('httpClientSecurityTest') {
   180	        tag = 'httpclient-security'
   181	        description = 'Runs the SSRF, credential-leak, and cardinality suite (design §28.5, §28.7).'
   182	        customize = { test -> applyContractSelection(test) }
   183	    }
   184	    // Its own source set rather than a tag, so the source set is the selection.
   185	    lane('httpClientPerformanceTest') {
   186	        sourceSet = 'httpClientPerformanceTest'
   187	        description = 'Certifies pool, streaming, retry, and rotation resource bounds (design §28.8).'
   188	        customize = { test ->
   189	            applyContractSelection(test)
   190	            test.systemProperty 'performance.assertions.enabled',
   191	                    (project.findProperty('performance.assertions.enabled') ?: 'false').toString()
   192	        }
   193	    }
   194	    lane('httpClientFailureInjectionTest') {
   195	        tag = 'httpclient-fault'
   196	        description = 'Runs the Toxiproxy fault-injection suite; fails closed without Docker ' +
   197	                '(design §28.3).'
   198	        customize = { test ->
   199	            applyContractSelection(test)
   200	            // The upstream image is mutable by default. Passing a digest here is what makes a red
   201	            // fault run attributable to this repository rather than to someone else's image push.
   202	            test.systemProperty 'httpclient.fault.httpbin.image',
   203	                    (project.findProperty('httpclient.fault.httpbin.image')
   204	                            ?: 'kennethreitz/httpbin:latest').toString()
   205	        }
   206	    }
   207	}
   208	
   209	
   210	tasks.register('jmh', JavaExec) {
   211	    group = 'verification'
   212	    description = 'Runs the JMH benchmarks for the blocking and reactive clients (design §28.8).'
   213	    mainClass = 'org.openjdk.jmh.Main'
   214	    classpath = sourceSets.jmh.runtimeClasspath
   215	    args '-rf', 'json', '-rff', layout.buildDirectory.file('reports/jmh/result.json').get().asFile.absolutePath
   216	}
   217	
   218	// Spring 6.2 / 7.0 compatibility lanes. This repository's Spring Boot 4.0 baseline pins Spring
   219	// Framework 7, so the 6.2 lane verifies the *API surface* the common packages compile against
   220	// rather than executing on a 6.2 distribution; the limitation is recorded in
   221	// docs/httpclient/support-matrix.md instead of being hidden behind a green check.
   222	tasks.register('spring62ApiSurfaceScan', Test) {
   223	    group = 'verification'
   224	    description = 'Scans the common packages for Spring 6.2 API-surface confinement. NOT a 6.2 runtime.'
   225	    testClassesDirs = sourceSets.test.output.classesDirs
   226	    classpath = sourceSets.test.runtimeClasspath
   227	    useJUnitPlatform { includeTags 'httpclient-spring62-surface' }
   228	    failOnNoDiscoveredTests = true
   229	    outputs.upToDateWhen { false }
   230	}
   231	
   232	// Which lanes gate an ordinary build, and which do not.
   233	//
   234	// The specialised lanes existed but hung off nothing: `check` ran only `test`, so the SSRF suite,
   235	// the BlockHound lane, the cross-transport contract and the Spring 6.2 surface scan were green in
   236	// CI only because a workflow happened to name them, and green locally because nobody ran them.
   237	// The four below are hermetic and fast — no Docker, no network, no machine-dependent thresholds —
   238	// so they belong in `check`.
   239	//
   240	// httpClientFailureInjectionTest (needs Docker), httpClientPerformanceTest (machine-dependent
   241	// bounds) and jmh (minutes) stay out deliberately. Attaching them would make `check` fail on a
   242	// laptop without Docker, which teaches people to skip `check`.
   243	tasks.named('check') {
   244	    dependsOn 'httpClientStableContractTest',
   245	            'httpClientSecurityTest',
   246	            'httpClientBlockHoundTest',
   247	            'spring62ApiSurfaceScan'
   248	}
   249	
   250	tasks.register('spring70CompatibilityTest', Test) {
   251	    group = 'verification'
   252	    description = 'Runs the contract suite on the repository Spring 7 baseline (design §29).'
   253	    testClassesDirs = sourceSets.test.output.classesDirs
   254	    classpath = sourceSets.test.runtimeClasspath
   255	    useJUnitPlatform { includeTags 'httpclient-contract' }
   256	    applyContractSelection(it)
   257	    outputs.upToDateWhen { false }
   258	}
exit=0

=== leaf 밖 소비자 ===
$ git grep -l 'dev.caskeleton.adapter.outbound.httpclient' -- . | grep -v '^adapter/outbound/httpclient/' | sed 's|/src/.*||' | sort | uniq -c
     17 app-bootstrap
exit=0

