# Module and Gradle Hygiene Refactoring Design - **Date:** 2026-07-25 - **Status:** Approved - **Scope:** all 19 Gradle leaf modules, their project/external dependencies, test conventions, architecture-analysis classpath, runtime composition, and dependency locks - **Source:** repository audit performed on 2026-07-25 against commit `821fe00` ## 1. Prerequisite CI recovery is a hard prerequisite, not part of this refactoring. The implementation may start only after the repository again contains the harness registry and CI contract assets and these commands reach task execution: ```bash cd src ./gradlew projects --console=plain ./gradlew :app-bootstrap:test --tests \ 'dev.caskeleton.bootstrap.contract.DeveloperExperienceContractTest' --console=plain ./gradlew :app-bootstrap:test --tests \ 'dev.caskeleton.bootstrap.contract.SampleRemovalSmokeContractTest' --console=plain ./gradlew verifyTrivyignore --console=plain ``` At audit time `settings.gradle` fails before project configuration because `.harness/project/modules.yaml` is absent. `.tool-versions`, `.trivyignore.yaml`, and the workflow files read by the two contract tests are absent as well. Dependency removal must not be mixed with that recovery because a red baseline cannot distinguish a pre-existing CI failure from a refactoring regression. ## 2. Problem Statement The module direction is broadly clean, but the declared Gradle graph is wider than the source graph: many leaves declare every allowed core dependency even when they use only one contract. Pure-core tests inherit Spring MVC from a global convention. `application-core` imports SLF4J for one outbox use case and therefore carries the complete Spring Boot starter at compile and runtime. Several leaves retain unused Groovy, Spock, generated-stub, UUID, or configuration-processor dependencies. The existing central ArchUnit suite analyzes whatever happens to be on the `app-bootstrap` test runtime classpath. Optional leaves are therefore not guaranteed to be analyzed. The sample-isolation contract also carries a hard-coded subset of modules instead of reading the 19-leaf registry. Locking is strict, but the lock verifier is not a release-gate dependency and non-BOM version ownership is scattered. This design reduces the graph only after characterization, makes topology and architecture coverage registry-driven, restores pure-core test isolation, and separates application logging intent from the logging framework. ## 3. Evidence Classification ### 3.1 Observed facts The following findings are deterministic observations and do not need dependency-removal debate: 1. `src/settings.gradle` cannot configure without `.harness/project/modules.yaml`. 2. There are exactly 19 leaf `build.gradle` files and 19 leaf `gradle.lockfile` files. 3. No production configuration depends on `:sample-portfolio`; `app-bootstrap` has one test-only `sampleFixture` edge. 4. The only adapter-to-adapter project edges are: `messaging`, `cache-redis`, `notification`, and `httpclient` to `adapter:outbound:support`. 5. `domain-core` and `shared-contract` main source contain no Spring, JPA, Jackson, or SLF4J imports. 6. `application-core` main source contains no Spring import. Its only framework imports are SLF4J in `PublishPendingOutboxEventsUseCase`. 7. The root build adds Spring Boot test and Spring MVC test starters to every leaf. 8. `cache-redis`, `messaging`, and `notification` have no Groovy tests although their builds apply Groovy and add Spock. 9. `adapter:outbound:identifier` does not use `uuid-creator`. 10. `src/sample-portfolio/.jqwik-database` is a tracked Java-serialization runtime artifact. 11. `persistence-mongo` owns adapter-local `Example*` domain/document/repository/mapper types and its repository adapter implements no application/domain port. ### 3.2 Static candidates The following are source-reference candidates, not approved removals. Each must first pass a leaf-specific compile/test characterization: | Leaf | Candidate project edges | | --- | --- | | `adapter:inbound:graphql` | `application-core`, `domain-core` | | `adapter:inbound:grpc` | `application-core`, `domain-core` | | `adapter:inbound:web` | `domain-core` | | `adapter:inbound:websocket` | `application-core`, `shared-contract` | | `adapter:outbound:cache-redis` | `domain-core`, `application-core` | | `adapter:outbound:httpclient` | `domain-core`, `application-core` | | `adapter:outbound:identifier` | `domain-core` | | `adapter:outbound:messaging` | `domain-core` | | `adapter:outbound:notification` | `domain-core` | | `adapter:outbound:persistence-jpa` | `domain-core` | | `adapter:outbound:persistence-mongo` | `application-core`, `shared-contract` | | `adapter:outbound:support` | `domain-core`, `application-core`, `shared-contract` | The same characterization rule applies to these external candidates: - GraphQL configuration processor and JSR-310 module. - gRPC protobuf/stub/annotations dependencies in the no-generated-stub skeleton. - broad `spring-boot-starter` usage in gRPC, fileserver, and objectstorage. - explicit Flyway core where the starter already supplies the required API. - duplicate starter/test declarations in app-bootstrap and sample-portfolio. An allowed registry edge is permission, not a requirement to declare that edge. ## 4. Goals 1. Keep all module paths and allowed edges in `.harness/project/modules.yaml` only. 2. Make the actual project DAG the smallest graph required by source, tests, and runtime composition. 3. Preserve the approved outbox failure-reporting refactor's removal of Spring and logging frameworks from `application-core` compile/runtime classpaths. 4. Give `domain-core`, `application-core`, and `shared-contract` framework-free test conventions. 5. Enforce external dependency purity for core modules from resolved compile/runtime graphs. 6. Analyze every registered production leaf with the architecture suite regardless of runtime composition. 7. Apply the Spring configuration processor exactly where main source declares `@ConfigurationProperties`. 8. Verify strict locks in the release gate and assign one owner to every non-BOM version. 9. State which optional adapters are in the default app runtime and which are opt-in. 10. Remove generated jqwik state from source control. 11. Remove the Mongo adapter-local example domain from production without creating a duplicate sample implementation. ## 5. Non-Goals - No feature behavior, endpoint, persistence schema, or public contract change. - No conversion to convention plugins, `buildSrc`, an included build, or a version catalog in this change. Build-logic migration starts only from a green post-refactoring baseline. - No automatic inclusion of every optional adapter in the production runtime. - No new Mongo business port or second Mongo sample in `sample-portfolio`. - No relocation of the shared ThreadLocal implementation in this change. - No LLM Wiki write as part of this documentation-only design task. ## 6. Target Topology and Registry Policy The registry remains the only topology authority. Every leaf entry must continue to own: - stable id - source path - Gradle path - role/family - allowed project dependencies - focused command - nearest module guidance Each runtime-capable leaf also receives one explicit runtime membership: - `core`: contract/core leaf consumed by registered adapters or bootstrap. - `app-default`: present on the default `app-bootstrap` runtime classpath. - `opt-in`: built and architecture-analyzed but absent from the default application runtime. - `sample-only`: used only by the sample fixture/runtime. - `composition-root`: `app-bootstrap` or `sample-portfolio` itself. The registry validator rejects missing membership, unknown dependency ids, duplicate Gradle paths, production edges to `sample-portfolio`, adapter peer edges not explicitly allowed by the source module's `allowed_dependencies`, and cycles. Gradle settings, the project-dependency verifier, sample-isolation checks, and architecture-analysis classpath all consume this data. No Java test keeps a copied module list. The current default runtime membership is preserved during graph cleanup. Optional adapters do not become runtime dependencies merely because architecture analysis needs their classes. ## 7. Approved Application Logging Boundary The logging-boundary implementation is owned by `docs/superpowers/specs/2026-07-25-application-outbox-failure-reporting-design.md` and its matching implementation plan. That design is a prerequisite for dependency pruning in this plan and is not redefined here. The selected contract is: - `application-core` owns `OutboxRelayFailureReportPort`; - the port has one `report(OutboxRelayFailureReport)` method; - the safe immutable report carries the approved FAILED/DEAD operational fields and never carries payload or idempotency data; - `adapter:outbound:messaging` owns the structured SLF4J reporter implementation; - `app-bootstrap` injects the port into the manually assembled relay use case; - application source and dependency guardrails prevent Spring and logging frameworks from returning to `application-core`. Module hygiene begins only after that focused plan is green. This design then verifies the resulting application dependency purity and removes unrelated static-candidate edges; it does not introduce a second reporting port or relocate reporter ownership. ## 8. Test Dependency Conventions Test dependencies are role-specific: | Role | Baseline | | --- | --- | | `domain-core` | JUnit Jupiter API/engine and AssertJ only when tests exist | | `application-core` | JUnit Jupiter, AssertJ; hand-written fakes; no Spring context | | `shared-contract` | JUnit Jupiter and AssertJ; no Spring context | | inbound web/GraphQL/WebSocket/gRPC | transport test modules required by that protocol only | | persistence adapters | mapping/unit baseline plus datastore Testcontainers only where vendor behavior is tested | | other outbound adapters | JUnit/Spock selected by actual test language; fake external systems | | `app-bootstrap` | Spring Boot context/slice support, ArchUnit, and integration-test dependencies | | `sample-portfolio` | feature, property, slice, and integration-test dependencies owned by the sample | The root build may supply JUnit platform launch/runtime configuration, but it must not supply Spring MVC or Spring context libraries to every leaf. A test dependency belongs in the leaf that uses it. ## 9. External Dependency Purity Gate The logging-boundary plan first introduces `verifyApplicationCoreDependencyPurity`. This refactoring then replaces that task with the registry-wide `verifyExternalDependencyPurity`; the two tasks do not remain as overlapping gates. The replacement preserves the application main/test classpath rules and `:application-core:check` wiring, then resolves each registered leaf's production `compileClasspath` and `runtimeClasspath` and applies the broader role rules: - `domain-core` and `shared-contract`: no external production module at all. - `application-core`: no Spring, SLF4J/logging backend, JPA/Hibernate, servlet, transport, database, cloud, or adapter implementation dependency. - inbound/outbound adapters: no logging implementation dependency; SLF4J API is allowed. - all production leaves: no test framework on production configurations. The task reports `module → configuration → forbidden coordinate → rule`. It checks resolved coordinates so transitive framework leakage is visible. Existing project-edge verification remains separate and registry-driven. Because the registry and harness API were absent at design time, the exact Python/Groovy/Java implementation of registry membership, purity/processor gates, and architecture classpath wiring is written in a post-recovery implementation addendum after the stable task packet is resolved. The addendum must contain complete code against the recovered API and pass review before any of those control-plane files are changed. Entry is fail-closed on a concrete overlay, an actual resolver invocation, matching overlay/packet content hashes, and the resolved rule hash; `--help` output or a prose-only confirmation is not packet evidence. ## 10. Registry-Driven Architecture Analysis `app-bootstrap` gets an `architectureAnalysis` dependency bucket populated from every registered production leaf, independent of `app-default` runtime membership. The architecture test runtime extends this bucket; the application production runtime does not. `CleanArchitectureTest` therefore sees GraphQL, gRPC, WebSocket, fileserver, objectstorage, Mongo, and every other registered leaf. `SampleRemovalSmokeContractTest` reads production module paths from the same registry instead of its current hard-coded list. The architecture configuration is non-consumable and non-resolvable itself; only the dedicated test runtime is resolvable. This prevents it from being published or accidentally used by `bootJar`. ## 11. Configuration Processor Consistency The rule is mechanical: - a leaf with main-source `@ConfigurationProperties` declares `annotationProcessor 'org.springframework.boot:spring-boot-configuration-processor'`; - a leaf without it does not. A verification task scans registered main source roots and compares the result with the declared annotation-processor dependency. This removes the unused GraphQL processor and adds missing processors to settings-owning leaves without relying on a copied module list. Binding/validation tests remain required for every settings class; generated metadata is not a substitute for behavior tests. The post-recovery control-plane addendum inventories that mapping and contains complete focused test code for every uncovered class before processor declarations change. ## 12. Runtime Composition and Component Scanning `app-bootstrap` keeps only registry members marked `app-default` on its production runtime. `opt-in` modules remain independently buildable and architecture-analyzed. Adoption of an opt-in module is an explicit registry and composition-root change with its own focused tests. `CaSkeletonApplication` narrows component and configuration-properties scanning to: - `dev.caskeleton.bootstrap` - `dev.caskeleton.adapter` It removes `dev.caskeleton.application`, `dev.caskeleton.domain`, and `dev.caskeleton.shared` from both scans. Those core packages own no Spring component or configuration-properties class, and the outbox use case remains manually composed. A context test pins this boundary. This is the smallest safe scan change in this refactoring. Converting all adapter configuration to explicit `@Import` or auto-configuration is a separate design change. ## 13. Leaf-Specific Cleanup All 19 leaves receive a characterization record and focused command: | Leaf | Target decision | | --- | --- | | `domain-core` | preserve zero-framework main; isolate pure tests | | `application-core` | consume the approved outbox-reporting result; verify Boot/SLF4J remain absent | | `shared-contract` | preserve stdlib-only production graph | | `adapter:inbound:web` | remove only compile-proven unused core edge; retain transport dependencies | | `adapter:inbound:graphql` | remove compile-proven core/tooling candidates | | `adapter:inbound:grpc` | retain server/health/reflection runtime; remove no-stub candidates only after compile | | `adapter:inbound:websocket` | retain domain-event and WebSocket dependencies; prune unused core edges | | `adapter:outbound:support` | retain only compile-proven core edges and SLF4J API/autoconfigure | | `adapter:outbound:cache-redis` | remove unused core edges and unused Groovy/Spock | | `adapter:outbound:fileserver` | keep application/shared ports; narrow starter only after characterization | | `adapter:outbound:httpclient` | keep shared/support and actual Groovy/Spock tests; prune unused core edges | | `adapter:outbound:identifier` | keep application pseudonymizer port; remove unused domain/uuid-creator | | `adapter:outbound:messaging` | retain the approved outbox reporter plus application/shared/support; remove unused domain and Groovy/Spock | | `adapter:outbound:notification` | keep application/shared/support; remove unused domain and Groovy/Spock | | `adapter:outbound:objectstorage` | keep application/shared/AWS SDK; narrow starter only after characterization | | `adapter:outbound:persistence-jpa` | keep application/shared/JPA/vendor runtime; test domain/Flyway candidates | | `adapter:outbound:persistence-mongo` | remove all adapter-local `Example*` types/tests; keep generic opt-in config/properties and binding/disabled-mode tests; remove application/shared edges | | `app-bootstrap` | preserve composition role; separate architecture classpath; narrow scans and duplicate tests | | `sample-portfolio` | preserve fixture-only isolation; remove generated jqwik state and own sample-only dependencies | ## 14. Generated jqwik State `src/sample-portfolio/.jqwik-database` is deleted from version control. `src/.gitignore` ignores `.jqwik-database` at any module working directory. Property tests remain deterministic from committed seeds/configuration rather than a developer-machine serialization cache. ## 15. Shared ThreadLocal Decision `ThreadLocalDomainContextPropagator` and `DomainContextPropagatorFactory` stay in `shared-contract` for this refactoring. They are Java-stdlib-only operational infrastructure, and moving them changes concurrency composition rather than dependency hygiene. This is a deliberate secondary decision, not an accidental omission. The purity gate pins their zero-external-dependency status. Relocation to bootstrap or an adapter requires a separate design with virtual-thread/context-propagation characterization and is not bundled into graph cleanup. ## 16. Persistence Mongo Decision The production Mongo leaf removes: - `ExampleRecord` - `ExampleMongoDocument` - `ExampleMongoMapper` - `ExampleMongoRepository` - `ExampleMongoRepositoryAdapter` - their example mapping/repository tests `MongoPersistenceConfig` and `MongoPersistenceProperties` remain as generic opt-in Spring Mongo machinery. Tests cover properties binding, disabled-by-default behavior, and mock-backed enabled-mode creation of one Boot 4 `MongoClient` and `MongoTemplate` without a network connection or sample repository. No duplicate Mongo domain is added to `sample-portfolio`; the WorkLog JPA sample remains the sole reference business domain. The Mongo leaf then removes its unused application/shared project edges. ## 17. Locking and Version Ownership After each leaf cleanup: 1. run its compile and focused test; 2. regenerate its lock state through the repository `resolveAndLockAll --write-locks` entrypoint; 3. run `verifyDependencyLocks`; 4. inspect that removed coordinates disappeared from production configurations. `check` or the CI release gate invokes `verifyDependencyLocks`. Spring Boot BOM owns managed Spring, Jackson, Micrometer, Testcontainers, and related versions. Existing root extension values own gRPC, protobuf, and AWS BOM versions. Every remaining non-BOM direct version has one root-level owner. The build remains Groovy DSL with the current root configuration during this work. A version catalog or convention-plugin migration is considered only after the complete refactoring and full `check` are green, so build-system migration cannot mask dependency-removal regressions. ## 18. Verification Strategy Verification proceeds from narrow to broad: 1. CI/harness prerequisite commands. 2. Registry schema, cycle, membership, and 19-leaf parity tests. 3. Before/after dependency reports for each candidate leaf. 4. Leaf `compileJava`, focused test, and configuration-processor check. 5. Pure-core external dependency gate. 6. Registry-driven project dependency and architecture tests. 7. Sample-off and optional-runtime composition tests. 8. Dependency lock verification. 9. Full `test` and `check`. No removal is accepted when a focused command is skipped without a named environmental reason and recorded residual risk. ## 19. Acceptance Criteria - All four prerequisite commands pass before hygiene edits begin. - Registry validation reports exactly 19 unique, acyclic leaves and owns runtime membership. - No copied production-module list remains in architecture or sample-isolation tests. - The approved outbox failure-reporting plan is green before hygiene pruning starts. - `application-core` production dependencies continue to contain no Spring or logging coordinate. - Domain, application, and shared tests run without Spring MVC/context dependencies. - Every production leaf is present on the architecture-analysis test runtime. - No production configuration depends on `sample-portfolio`. - Every adapter peer edge is explicitly allowed by the source module's registry entry; the current graph's peer edges all target `adapter:outbound:support`. - Configuration processor declarations exactly match main-source properties classes. - Every removed project/external dependency has before/after compile and focused-test evidence. - Mongo production source contains no `Example*` domain/document/repository/mapper type. - `.jqwik-database` is untracked and ignored. - `verifyExternalDependencyPurity`, `verifyCleanArchitectureDependencies`, architecture tests, `verifyDependencyLocks`, `test`, and `check` pass. - No convention-plugin or version-catalog migration is included. - Agents do not stage, commit, amend, or push; commit policy remains human-only.