390 lines
21 KiB
Markdown
390 lines
21 KiB
Markdown
# 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.
|