Files
tech-log-backend/docs/superpowers/specs/2026-07-25-module-gradle-hygiene-design.md
T

21 KiB

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:

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.