chore: plan 파일 업로드

This commit is contained in:
donghyeon-ka
2026-07-26 13:36:32 +09:00
parent 821fe00c32
commit 7363b2aa1e
6 changed files with 6621 additions and 0 deletions
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,431 @@
# Application Outbox Failure Reporting Refactoring Design
- **Date:** 2026-07-25
- **Status:** Approved
- **Scope:** `application-core` outbox failure reporting, its messaging adapter, bootstrap wiring,
dependency purity enforcement, tests, and affected module documentation
- **Source:** user-requested Clean Architecture refactoring review plus repository evidence gathered
on 2026-07-25
## 1. Problem Statement
`application-core` declares `org.springframework.boot:spring-boot-starter`, although its production
sources use no Spring type or annotation. The only external observability types in the module are
`org.slf4j.Logger` and `org.slf4j.LoggerFactory` in
`PublishPendingOutboxEventsUseCase`. The broad starter consequently places Boot autoconfiguration,
Spring Context/AOP, Micrometer Observation, Logback, Log4j bridges, JUL bridges, and SnakeYAML on a
core application classpath for two logging calls.
This contradicts the module's framework-free design statement and weakens the dependency direction
the template is intended to teach. It also hides an important semantic distinction: the two log
lines are not arbitrary diagnostic messages. They report confirmed `FAILED` and `DEAD` outbox state
transitions that feed operational alerts and runbooks.
The build currently cannot resolve dependencies or run tests because `src/settings.gradle` fails
when `.harness/project/modules.yaml` is absent. Harness registry recovery is therefore a prerequisite
for implementation and verification, not part of this refactoring.
## 2. Evidence
- `src/application-core/build.gradle:14` declares `spring-boot-starter`.
- `src/application-core/src/main/java/dev/caskeleton/application/outbox/PublishPendingOutboxEventsUseCase.java:17-18`
imports the only production observability framework types in the module.
- The same use case creates an SLF4J logger at lines 38-39 and emits the two failure records at
lines 139-165.
- `src/application-core/README.md:10-13` says the module is framework-free and accesses
infrastructure only through `*Port` interfaces.
- `src/application-core/CLAUDE.md:27-30` and `src/application-core/build.gradle:3-5` instead claim
the starter is retained for optional `@Service` registration, although application-core contains
no Spring stereotype.
- The relay is manually constructed and must not be a Spring bean:
`src/application-core/README.md:332-337`.
- The outbox registry assigns `OUTBOX_PUBLISH_FAILED` and `OUTBOX_DEAD_LETTER` to the infrastructure
owner layer and ERROR severity: `docs/registries/error-codes.yaml:724-749`.
- The runbooks require structured `error.code`, `event_id`, `event_type`, and `correlation_id`
fields: `docs/runbooks/outbox-publish-failed.md:17-27` and
`docs/runbooks/outbox-dead-letter.md:17-24`.
- The current project-dependency verifier inspects only `ProjectDependency` instances, so it cannot
reject an external starter added to a core module: `src/build.gradle:612-619`.
## 3. Goals
1. Make `application-core` free of Spring, SLF4J, Logback, Log4j, JUL logging, and Micrometer types
and main/test classpath dependencies.
2. Express confirmed outbox publication failures as a typed application-owned outbound port.
3. Keep report data safe by construction: no payload, idempotency key, arbitrary field map, log
level, message template, or framework logger crosses the port.
4. Implement structured failure reporting in `adapter:outbound:messaging`.
5. Keep `app-bootstrap` limited to final wiring and runtime logging configuration.
6. Preserve outbox state-machine behavior, transaction boundaries, per-event continuation, and
at-least-once semantics.
7. Ensure a reporting backend failure cannot change a persisted `FAILED`/`DEAD` outcome or stop the
remaining relay batch.
8. Remove the duplicate, misleading fail-open WARN currently emitted by the fail-closed outbox
publisher.
9. Add source- and dependency-level guardrails that prevent framework observability from returning
to application-core.
## 4. Non-Goals
- This change does not redesign outbox claiming, FIFO ordering, retry backoff, in-flight recovery,
or broker selection.
- It does not turn operational failures into domain events or business audit records.
- It does not deliver a failure report through the same broker/outbox path; that would recurse when
the broker is the failing dependency.
- It does not add a generic `LoggerPort`, severity API, string template API, or untyped field map.
- It does not add a twentieth module or a general observability adapter family.
- It does not recover the missing `.harness` policy registry; the implementation waits for that
independently governed recovery.
- It does not change public HTTP response contracts.
## 5. Semantic Classification
An outbox publish failure is an **application operational event**:
- the application state machine decides whether the confirmed result is retryable `FAILED` or
terminal `DEAD`;
- the persistence transition is authoritative;
- an infrastructure adapter renders that fact as a structured operational record;
- metrics and runbooks consume the result for operations.
It is not a business audit event. It has no actor/action audit semantics, is not retained as an
immutable audit ledger, and must not be used as proof of a business transaction. It is also not a
domain event: feeding it into the same outbox publisher would recursively fail.
## 6. Architecture
```text
PublishPendingOutboxEventsUseCase
├── OutboxStorePort ──────────────> persistence adapter
├── OutboxMessagePublishPort ─────> messaging publisher adapter
└── OutboxRelayFailureReportPort ─> SLF4J structured reporter adapter
(adapter:outbound:messaging)
app-bootstrap
└── injects all three ports when manually constructing the relay use case
```
Dependency direction remains:
```text
app-bootstrap
-> adapter:outbound:messaging
-> application-core
-> shared-contract
-> domain-core
```
`application-core` owns the port and safe report value. The messaging module owns the concrete
rendering because its local responsibility explicitly includes outbox publication adaptation, it
already depends on application/shared contracts, and it already has the SLF4J API. Bootstrap
selects and injects the adapter but does not implement the port.
## 7. Application Contract
### 7.1 Port
```java
@FunctionalInterface
public interface OutboxRelayFailureReportPort {
/**
* Attempts to report a confirmed FAILED or DEAD relay transition.
*
* <p>The implementation must not throw. The report is operational evidence, while the persisted
* outbox state and returned relay outcome remain authoritative.
*/
void report(OutboxRelayFailureReport report);
}
```
The use case also contains a defensive non-throwing invocation boundary. This makes the invariant
explicit even if a custom implementation violates the port contract.
### 7.2 Safe immutable report
```java
public record OutboxRelayFailureReport(
OperationalError code,
String eventId,
String eventType,
String aggregateId,
String correlationId,
int attemptCount,
Instant nextAttemptAt,
RuntimeException cause) {
public static OutboxRelayFailureReport retryableFailure(
String eventId,
String eventType,
String aggregateId,
String correlationId,
int attemptCount,
Instant nextAttemptAt,
RuntimeException cause);
public static OutboxRelayFailureReport deadLetter(
String eventId,
String eventType,
String aggregateId,
String correlationId,
int attemptCount,
RuntimeException cause);
}
```
Invariants:
- `code` is exactly `OUTBOX_PUBLISH_FAILED` or `OUTBOX_DEAD_LETTER`.
- identifiers and type names are non-null and non-blank.
- `attemptCount` is at least one.
- `nextAttemptAt` is required for `OUTBOX_PUBLISH_FAILED` and absent for
`OUTBOX_DEAD_LETTER`.
- `cause` is required.
- the record has no `payload`, `idempotencyKey`, logger, severity, template, or arbitrary map.
Static factories remove invalid combinations from ordinary call sites. The record deliberately
accepts an allowlisted set of safe operational metadata plus the original cause rather than an
`OutboxEvent`, whose full shape includes payload and idempotency data.
## 8. Relay Flow and Failure Semantics
The report attempt happens only after the corresponding write transaction succeeds:
```text
publish throws
├── retry remains
│ ├── inWrite(markFailed) succeeds
│ ├── attempt OUTBOX_PUBLISH_FAILED report
│ └── return FAILED
└── attempts exhausted
├── inWrite(markDead) succeeds
├── attempt OUTBOX_DEAD_LETTER report
└── return DEAD
```
Behavior matrix:
| Situation | Persistence | Report | Relay behavior |
|---|---|---|---|
| Publish succeeds, `markPublished` succeeds | `PUBLISHED` | none | return `PUBLISHED` |
| Publish succeeds, `markPublished` fails | remains recoverable `IN_FLIGHT` | none | propagate store failure; scheduler retries later |
| Publish fails, `markFailed` succeeds | `FAILED` | attempt retryable report | return `FAILED`; continue batch |
| Publish fails, `markDead` succeeds | `DEAD` | attempt dead-letter report | return `DEAD`; continue batch |
| Publish fails, status transition fails | no confirmed FAILED/DEAD transition | none | propagate store failure; do not emit a false report |
| Reporter violates its contract and throws | already `FAILED` or `DEAD` | attempted | contain reporter exception; preserve outcome and continue batch |
The report is mandatory as an **attempt** after every confirmed failure transition. A production
NOOP binding is forbidden. No logging system can guarantee durable emission, so metrics and
persistence remain independent evidence when the logging backend itself is impaired.
## 9. Structured Logging Contract
The messaging adapter emits ERROR through SLF4J 2's fluent key-value API. The runtime Logstash
encoder serializes key-value pairs as top-level JSON fields, while the message also carries a short
safe summary for local pattern output.
Common fields:
- `error.code`
- `error.category`
- `dependency_name`
- `dependency_type=messaging`
- `outcome`
- `event_id`
- `event_type`
- `aggregate_id`
- `correlation_id`
- `attempt_count`
- `runbook_link`
Retry-only field:
- `next_attempt_at`
Mappings:
| Code | Outcome | Runbook |
|---|---|---|
| `OUTBOX_PUBLISH_FAILED` | `FAILED` | `runbook://outbox/publish-failed` |
| `OUTBOX_DEAD_LETTER` | `DEAD` | `runbook://outbox/dead-letter` |
The throwable is attached as the log cause. Payload, idempotency key, message envelope, recipient,
and arbitrary exception-derived key/value fields are never added. Existing runtime masking remains
defense in depth rather than the primary privacy boundary.
## 10. Duplicate Logging Removal
`OutboxMessagePublishAdapter` currently uses `FailOpenDependencyLogger` for a fail-closed operation:
it emits WARN and rethrows. That logger's documented meaning is an optional dependency failure where
the use case still succeeds. The relay then emits a second ERROR after deciding `FAILED` or `DEAD`.
After this refactoring:
- `OutboundMessagePublisher` keeps `FailOpenDependencyLogger` because its contract is genuinely
fail-open.
- `OutboxMessagePublishAdapter` maps/sends and surfaces failures without logging.
- `Slf4jOutboxRelayFailureReportAdapter` emits the single canonical ERROR after the application
state transition succeeds.
This removes duplicate records and makes severity match the confirmed outcome.
## 11. Dependency Purity
### 11.1 Gradle declarations
`application-core` production declarations become:
```groovy
dependencies {
implementation project(':domain-core')
implementation project(':shared-contract')
}
```
The root test baseline gives application-core JUnit Jupiter and AssertJ directly rather than Spring
Boot test starters. Spring dependency-management may remain build tooling, but Spring/logging/
Micrometer artifacts must not appear on application-core main or test compile/runtime classpaths.
Static-analysis tool configurations are outside this classpath rule.
### 11.2 Gradle verification
A blocking `verifyApplicationCoreDependencyPurity` task checks both:
1. application-core production configurations contain no declared external module dependency;
2. `compileClasspath`, `runtimeClasspath`, `testCompileClasspath`, and `testRuntimeClasspath`
resolve no Spring, SLF4J, Logback, Log4j, or Micrometer component.
The task is wired into `:application-core:check`.
### 11.3 Source verification
ArchUnit adds an application logger/metrics ban covering:
- `org.slf4j..`
- `java.util.logging..`
- `ch.qos.logback..`
- `org.apache.logging.log4j..`
- `io.micrometer..`
An intentional application-package fixture proves the rule is not vacuous. The Gradle task remains
necessary because ArchUnit cannot detect an unused starter that is merely present on the classpath.
## 12. Module Ownership Alternatives
### 12.1 Messaging adapter — selected
Advantages:
- highest cohesion with outbox publication failure;
- existing application/shared/support and SLF4J dependencies;
- no new project edge;
- reusable by composition roots other than app-bootstrap;
- preserves bootstrap as wiring rather than an adapter collection.
Counterargument: reporting is observability rather than broker transport. The selected design
answers this by keeping the contract application-owned and the concrete class narrowly
outbox-specific; generic logging support does not move into messaging.
### 12.2 Shared outbound support — rejected
Advantages:
- already owns reusable correlation and fail-open dependency logging;
- would centralize logging backend calls.
Counterargument: the support module explicitly keeps feature-specific behavior in the owning leaf.
Putting `FAILED`/`DEAD` outbox semantics there makes a low-level shared module feature-aware.
Generalizing the interface would create the forbidden logger abstraction.
### 12.3 App bootstrap — viable fallback, not selected
Advantages:
- owns runtime logging bootstrap and final wiring;
- already contains outbox metrics and structured Logstash usage.
Counterargument: each feature-specific reporter placed there expands the composition root into an
adapter implementation module and prevents straightforward reuse by another composition root.
### 12.4 Direct `slf4j-api` in application-core — rejected
This is the smallest dependency diff and would remove Spring Boot transitive dependencies, but it
retains framework coupling, contradicts the module rule, and tests formatting calls instead of
application meaning.
### 12.5 Generic operational event publisher — deferred
A typed cross-feature operational event sink could become valuable when several application
features need the same routing. Introducing it for two outbox outcomes is premature, risks an
untyped field bag, and must never be implemented through the failing outbox broker.
## 13. Testing Strategy
### Application contract tests
- report factory happy paths and invariant rejection;
- record component whitelist proving payload and idempotency key are absent;
- transient failure reports only after `markFailed`;
- dead-letter failure reports only after `markDead`;
- transition failure emits no report;
- successful publish and `markPublished` failure emit no failure report;
- a throwing reporter does not change `FAILED`/`DEAD` result and does not stop later events.
### Messaging adapter tests
- exactly one ERROR record;
- code/category/outcome/runbook mapping;
- required snake_case identifiers and attempt fields;
- retry-only `next_attempt_at`;
- throwable preservation;
- no payload or idempotency key;
- reporter bean exists when messaging is disabled and when a broker is active;
- outbox publisher propagates runtime and checked broker failures without emitting the old
fail-open WARN.
### Architecture and Gradle tests
- intentional application logger fixture is rejected;
- production application packages pass the new rule;
- application-core dependency purity task passes only with clean main/test classpaths;
- lock verification passes after regeneration.
### Regression tests
- focused application, messaging, and bootstrap tests;
- outbox PostgreSQL lifecycle tests when Docker is available;
- full `test` and `check`.
## 14. Migration Sequence
1. Recover and validate the harness module registry so Gradle can configure.
2. Add red tests for safe report contracts and relay semantics.
3. Add the application report value and port.
4. Inject the port into the relay and make report failures outcome-neutral.
5. Add red messaging adapter and wiring tests.
6. Implement the structured messaging reporter.
7. Remove fail-open logging from the fail-closed outbox publisher.
8. Remove application-core Boot/Spring/logging dependencies and give it a pure test baseline.
9. Add source and resolved-classpath purity guards.
10. Regenerate dependency locks and update module/runbook documentation.
11. Run focused, architecture, dependency, full test, and full check gates.
12. Capture implementation evidence in the LLM Wiki as required by repository policy.
## 15. Acceptance Criteria
- No application-core main or test source imports Spring, SLF4J, Logback, Log4j, JUL logging, or
Micrometer.
- No forbidden framework artifact appears on application-core main/test compile/runtime
classpaths.
- `spring-boot-starter` is absent from `src/application-core/build.gradle`.
- Every confirmed `FAILED`/`DEAD` transition attempts exactly one typed report.
- A status-transition failure emits no success-like failure report.
- A reporter exception cannot change a relay outcome or stop the next claimed event.
- Production wiring contains exactly one non-NOOP `OutboxRelayFailureReportPort`.
- Structured ERROR fields and runbook links match the documented registry conventions.
- Payload and idempotency key cannot cross the report contract and do not appear in adapter logs.
- The fail-closed publisher no longer uses `FailOpenDependencyLogger`.
- Application logger ArchUnit mutation and dependency purity checks are blocking.
- Focused tests, dependency locks, architecture checks, full tests, and `check` pass after harness
registry recovery, or any environmental blocker is reported with its remaining risk.
@@ -0,0 +1,476 @@
# CI Control Plane Recovery Design
- **Date:** 2026-07-25
- **Status:** Approved
- **Scope:** repository control-plane recovery, Gitea Actions enablement, Gradle/CI gates,
container build context, dependency automation, and documentation parity
- **Depends on:**
[`2026-07-20-harness-policy-engine-design.md`](2026-07-20-harness-policy-engine-design.md)
- **Current revision audited:** `821fe00c323b5335980f271c7ee47b92ac2168f2`
- **Task-packet state:** unavailable. `.harness/validators/resolve_task.py` and its policy inputs
are absent from the audited revision, so no task-packet hash or formal evidence profile can be
produced before control-plane recovery.
## 1. Problem Statement
The repository describes a high-assurance CI and architecture-governance control plane, but the
audited Git tree does not contain the hidden root paths that implement it. The current revision
contains no `.harness`, `.agents`, `.claude`, `.codex`, or `.github` tree. It also lacks the root
`.tool-versions`, `.trivyignore.yaml`, and `.gitattributes` contracts referenced by the tracked
guidance and Gradle build.
This is not a Gradle wrapper failure. The tracked wrapper downloads and starts Gradle 9.0.0 under
Java 21, but every project task stops while evaluating `src/settings.gradle` because
`.harness/project/modules.yaml` is missing. The same missing registry also prevents the architecture
dependency gate and task-packet resolution from running.
The CI host is Gitea 1.27.0, not GitHub. The public repository API reports `has_actions: false`, so
the repository Actions unit is disabled. An unauthenticated request to the runner API returns
`401`, which proves that runner state must be checked with repository or administrator
authorization; it does not prove that a usable runner exists. Gitea Actions requires both the
repository Actions unit and an online runner.
The container build has an additional independent defect. Compose and both Dockerfile examples use
`src/` as the build context, while Gradle resolves the registry from the repository root. Even after
the hidden assets are restored, a build with the current context cannot copy the registry into the
builder.
## 2. Audit Baseline
| Surface | Command or source | Observed result |
| --- | --- | --- |
| Git revision | `git rev-parse HEAD` | `821fe00c323b5335980f271c7ee47b92ac2168f2` |
| Hidden assets | `git cat-file -e HEAD:.harness` and equivalent checks | `.harness`, `.agents`, `.claude`, `.codex`, `.github` absent |
| Ignore rules | `git check-ignore -v --no-index ...` | exit `1`; missing paths are not ignored |
| Recoverable local objects | `git fsck --full --no-reflogs --unreachable` | exit `0`; no unreachable objects reported |
| Gradle launcher | `cd src && ./gradlew --version` | exit `0`; Gradle 9.0.0 and Java 21 |
| Gradle project task | `cd src && ./gradlew tasks --console=plain` | exit `1`; missing module registry at `src/settings.gradle:12` |
| Gradle release gate | `cd src && ./gradlew check --console=plain` | exit `1`; same settings failure |
| Task resolver | `python3 .harness/validators/resolve_task.py ...` | impossible; resolver file absent |
| Compose syntax | `docker compose -f docker-compose.yml -f docker-compose.local.yml config --quiet` | exit `0` |
| Gitea version | `GET /api/v1/version` | `1.27.0` |
| Repository Actions | public repository API | `has_actions: false` |
| Runner API | unauthenticated runner request | `401`; authorized runner inventory still required |
The most recent commit added 971 files over a parent that contained only a two-line README. The
root tree contains no dot-prefixed entry, while nested files such as `src/.env`,
`src/.dockerignore`, and `src/.gitignore` were included. This is consistent with a top-level shell
glob used during copying or staging. That is a falsifiable root-cause hypothesis, not proof of the
exact command that was used.
## 3. Goals
1. Prefer byte-for-byte recovery of the authoritative hidden control-plane assets; when that source
is unavailable or incomplete, require an explicit human reconstruction decision and record new
provenance without presenting reconstruction as restoration.
2. Restore the 19-leaf module registry and the harness behavior approved in the 2026-07-20 design.
3. Make a fresh checkout fail early with a precise control-plane error before Gradle configuration.
4. Enable repository Actions on Gitea 1.27.0 and provide an isolated, repository-scoped runner.
5. Keep `.github/workflows` as the canonical workflow directory while preventing an accidental
`.gitea/workflows` shadow.
6. Restore a single release-blocking fan-in status and the complementary vulnerability status.
7. Make public-path, Trivy suppression, dependency-lock, and architecture gates fail closed.
8. Make Docker builds consume the same root registry without duplicating the registry under `src/`.
9. Pause Renovate automerge until required CI statuses and lockfile refresh behavior are proven.
10. Bring README, gate-matrix, workflow, and physical-path claims back into parity.
## 4. Non-Goals
- This recovery does not change production Java behavior or module boundaries.
- It does not redesign the 19-leaf registry approved in the 2026-07-20 harness policy design.
- It does not silently synthesize hidden policy files or present reconstructed content as recovered
authority.
- It does not store a Gitea API token, runner registration token, or repository secret in Git.
- It does not enable deployment to a production environment. Release artifact construction and
scanning are restored, but a separate deployment decision remains human-owned.
- It does not re-enable Renovate automerge merely because workflow files exist; branch protection
and a successful dependency-update exercise are also required.
## 5. Governing Invariants
### 5.1 Recovery mode is an explicit human decision
The original working tree, archive, or source repository that produced the 2026-07-20 harness
design is the preferred recovery authority. Before copying anything into this repository, the
recovery source must be inventoried and hashed outside the worktree.
The minimum authoritative set is:
- `.harness/`
- `.agents/`
- `.claude/`
- `.codex/`
- `.github/`
- `.tool-versions`
- `.trivyignore.yaml`
- `.gitattributes`
The human owner chooses one of two modes and records it before repository writes:
**Mode A — authoritative restore.** All minimum paths exist in the recovery source. The executor
hashes them, copies them byte-for-byte, proves source/destination equality, and preserves their
native provenance.
**Mode B — controlled reconstruction.** The original source is unavailable or incomplete. The
human records that fact and explicitly authorizes reconstruction from the approved
[`2026-07-20-harness-policy-engine-design.md`](2026-07-20-harness-policy-engine-design.md),
[`2026-07-20-harness-policy-engine.md`](../plans/2026-07-20-harness-policy-engine.md), the tracked
Gradle/module sources, and this design. Reconstructed artifacts receive new hashes and a
`controlled-reconstruction` provenance record. Schema, renderer parity, mutation coverage, Gradle
project discovery, and architecture dependency checks must pass before the new artifacts can act as
authority.
The first Mode B inventory covers the reconstructed harness and generated platform assets only.
After Tasks 3-6 and every later change to a covered path are final, the executor regenerates one
complete, sorted SHA-256 inventory for `.harness`, `.agents`, `.claude`, `.codex`, `.github`,
`.tool-versions`, `.trivyignore.yaml`, `.gitattributes`, `.dockerignore`, and
`docs/security/public-paths-snapshot.txt`. The provenance record names that final evidence path.
Only this post-change inventory is used for the human handoff.
Implementation stops only until the human chooses Mode A or Mode B. An incomplete Mode A export
must never be filled silently. It may instead cause the human to switch the recorded decision to
Mode B. The tracked `AGENTS.md`, `CLAUDE.md`, and design documents are evidence of intended
behavior, but reconstructed schemas, agents, registries, and workflows become authoritative only
after the required new evidence passes.
### 5.2 Human-only commit policy
Recovery and implementation may leave reviewed changes in the working tree, but agents do not
stage, commit, amend, or push. A human decides commit boundaries after reviewing recovery hashes,
generated-file parity, test evidence, and Gitea status checks.
### 5.3 One registry and one workflow source
`.harness/project/modules.yaml` remains the only module-edge and focused-command registry.
Container builds copy that file from the repository root; they do not create a second copy under
`src/`.
`.github/workflows` remains the canonical workflow directory because the repository documentation
and portability contract already point there. Gitea's default `WORKFLOW_DIRS` value is
`.gitea/workflows,.github/workflows`, and Gitea uses the first directory that exists. Therefore
`.gitea/workflows` must remain absent unless the project later adopts a generated-mirror design
with an explicit parity check and a separate approved specification.
### 5.4 Fail closed before expensive work
CI runs a repository control-plane preflight before invoking build or container work. Missing
policies, workflow shadowing, a missing committed public-path baseline, or an incomplete structured
Trivy contract fail immediately. Once Java/Gradle is available, that same blocking preflight runs
`verifyTrivyignore`; the vulnerability scanner must explicitly consume `.trivyignore.yaml`.
## 6. Target Architecture
```text
authoritative hidden-asset export
|
v
recovery inventory + SHA-256 comparison
|
v
.harness/.agents/.claude/.codex/.github restored
|
v
control-plane preflight
|-- required paths
|-- generated-agent parity
|-- canonical workflow directory
|-- committed security baselines
`-- task-packet resolver availability
|
v
resolved high-risk CI/deployment task packet
|
+-----------------------------+
| |
v v
Gradle quality gates Docker root-context builds
|-- dependency locks |-- production bootJar
|-- architecture edges `-- sample bootJar
|-- focused/ArchUnit tests
|-- test/check
`-- public/env/security contracts
| |
+--------------+--------------+
v
CI quality release-gate
+
dependency-vulnerability required status
|
v
Gitea protected-branch decision
```
## 7. Design Decisions
### 7.1 Recovery gate and physical control-plane manifest
After Mode A restore parity or Mode B reconstruction evidence passes, the harness gains a small
physical manifest at `.harness/project/control-plane.yaml`. It lists required files, required
directories, the canonical workflow directory, and the forbidden shadow directory. The file uses
JSON syntax, matching the 2026-07-20 design's stdlib-only JSON-as-YAML convention.
`.harness/validators/validate_control_plane.py` reads the manifest and reports every missing path in
one deterministic result. It also rejects `.gitea/workflows`. Its tests live at
`.harness/tests/test_control_plane.py`.
This validator checks physical availability only. It does not duplicate module edges, risk rules,
or workflow gate semantics. Module semantics stay in `modules.yaml`; the gate matrix stays in
`.github/ci-gate-matrix.yml`.
### 7.2 Gitea Actions and runner control
The repository owner enables `Enable Repository Actions` in the repository settings. The public API
must then report `has_actions: true`.
The runner is registered at repository scope, uses an isolated Docker execution mode, and exposes
the exact `ubuntu-22.04` label used by the workflows. Registration credentials remain in the runner
host's protected state or secret manager. They never enter workflow YAML, shell history captured by
CI, Docker image layers, or repository files.
An authenticated repository runner inventory must show at least one enabled, online runner before
the first required workflow is treated as operational. The previous unauthenticated `401` remains
an expected access-control result.
### 7.3 Canonical workflow directory and Gitea shadowing
The recovery restores canonical workflows under:
- `.github/workflows/ci-quality-gates.yml`
- `.github/workflows/build-release-supply-chain.yml`
- `.github/workflows/dependency-vulnerability.yml`
No workflow is copied to `.gitea/workflows`. With Gitea's default directory ordering, the mere
existence of `.gitea/workflows` would cause `.github/workflows` to be ignored. The preflight
validator makes that shadow a blocking failure.
Instance administration must confirm that `[actions].WORKFLOW_DIRS` still contains
`.github/workflows`. If the instance has a non-default value that excludes it, the administrator
changes the instance setting or the project stops before enabling required checks.
### 7.4 Preflight and release-gate topology
`ci-quality-gates.yml` starts with `control-plane-preflight`. No Gradle, test, or Docker job runs
unless preflight succeeds.
The release-blocking fan-out includes:
- restored harness unit and mutation suite
- `validate_modules.py`, renderer parity, policy parity, and `verify-gate-matrix.sh`
- structured Trivy validation through `verifyTrivyignore`
- Gradle wrapper launch and Java 21 assertion
- `verifyDependencyLocks`
- `verifyCleanArchitectureDependencies`
- focused ArchUnit coverage
- `verifyPublicPathSnapshot`
- `test`
- `check`
- production and sample Docker builds
- reproducible artifact verification when owned by the restored workflow contract
The workflow ends with a single `release-gate` job that uses `if: always()` and fails unless every
release-blocking dependency succeeded. Quarantine remains non-blocking and is intentionally absent
from the fan-in.
Gitea cannot express `needs` across separate workflow files. The dependency vulnerability workflow
therefore publishes its own blocking status. Protected branches require both the quality
`release-gate` status and the vulnerability status.
### 7.5 Missing contracts
The recovery must restore `.tool-versions`, `.gitattributes`, `.trivyignore.yaml`, workflow scripts,
gate matrix, CODEOWNERS, and vulnerability policy from the authoritative source.
The structured empty Trivy contract is retained even when there are no suppressions:
```yaml
vulnerabilities: []
licenses: []
misconfigurations: []
secrets: []
```
The quality preflight and release-tag preflight both run `verifyTrivyignore`. The independent
vulnerability workflow also runs that verifier and passes
`trivyignores: .trivyignore.yaml` to the pinned Trivy action, so a present-but-unconsumed or
malformed suppression file cannot satisfy a required status.
`docs/security/public-paths-snapshot.txt` becomes a committed baseline. The current approved value
derived from `src/.env` is `/api/healthcheck`. A missing baseline is a failure, not an instruction to
create one during verification.
The read-only `verifyPublicPathSnapshot` task compares the committed baseline to `src/.env`.
Generation moves to a separate, explicitly approved update task. `check` and the quality workflow
both depend on the read-only verification task.
### 7.6 Dependency locks
All 19 leaf modules retain Gradle strict locking. CI runs `verifyDependencyLocks` before compile or
test jobs so an incomplete Renovate update fails with a direct lock error.
The only supported lock refresh command remains:
```bash
cd src
./gradlew resolveAndLockAll --write-locks --console=plain
```
A dependency update is acceptable only when the declaration, all affected `gradle.lockfile` files,
and the quality gate agree. CI never runs `--write-locks`.
### 7.7 Docker root context
Compose changes the app build context from `src/` to the repository root and addresses the
Dockerfile as `src/Dockerfile`. Both Dockerfiles keep the Gradle project at `/build/src` and copy:
1. `.harness/project/modules.yaml` to `/build/.harness/project/modules.yaml`;
2. wrapper, build descriptors, and lockfiles to `/build/src`;
3. the complete `src/` tree only after dependency verification.
A root `.dockerignore` replaces the context role previously owned by `src/.dockerignore`. It
excludes Git metadata, build output, IDE state, environment files, and secrets, while explicitly
allowing the module registry, wrapper, build descriptors, lockfiles, and Java/resources trees.
This preserves a single registry and makes local Compose, production image, sample image, and CI use
the same context contract.
### 7.8 Renovate safety state
`renovate.json` sets `automerge: false` for every update type during recovery. The current comment
already states that automerge requires trustworthy CI, while the repository currently has no
operational Actions unit.
Limited patch/pin/digest automerge can be reconsidered only after all of the following are observed:
1. repository API reports `has_actions: true`;
2. an authenticated runner inventory reports an online runner;
3. protected branches require both blocking statuses;
4. a real Renovate dependency pull request updates strict lock state and passes;
5. a deliberately stale lockfile fails `verifyDependencyLocks`.
The configuration description must also stop claiming that the project has
`gradle/libs.versions.toml` unless the project separately adopts a version catalog.
### 7.9 Documentation parity
Root and `src/` README files must point to paths that exist in Git and commands that pass from a
fresh checkout. The control-plane validator covers required physical paths, and the restored
README-command and gate-matrix checks cover executable behavior.
The documentation must distinguish:
- Gitea repository Actions enablement from workflow files;
- unauthenticated runner API access from authorized runner health;
- `.github/workflows` as canonical from `.gitea/workflows` as a shadow risk;
- lock verification from lock regeneration;
- read-only public-path verification from approved baseline update.
## 8. Phased Recovery
### Phase 0 — Preserve evidence
Capture the current revision, clean status, missing-path evidence, Gitea version, repository
Actions state, and runner authorization behavior. Hash the authoritative recovery source before
copying it.
### Phase 1 — Establish authority
The human chooses Mode A or Mode B. Mode A restores the hidden asset set byte-for-byte and verifies
source equality. Mode B reconstructs from the two approved 2026-07-20 documents, records new
provenance/hashes, and runs schema, renderer-parity, mutation, Gradle discovery, and architecture
checks. The executor then writes a controller-approved overlay to the recorded recovery evidence
path, invokes the recovered resolver with that file, persists the resolved packet plus packet/rule
checksums, and proves deterministic re-resolution. Task 3 cannot start until those exact artifacts
verify.
### Phase 2 — Establish fail-fast local gates
Add the physical control-plane manifest and validator. Restore missing security contracts and make
the public-path baseline fail closed. Run harness checks before Gradle.
### Phase 3 — Repair build paths
Switch Docker to the repository-root context, add the root ignore contract, and verify production
and sample images.
### Phase 4 — Activate Gitea
Enable repository Actions, register the isolated runner, confirm workflow directory configuration,
and run the preflight workflow. Do not configure required statuses until job names are stable and a
successful run exists.
### Phase 5 — Enforce merge controls
Enable the quality `release-gate` and vulnerability status as protected-branch requirements. Seed
negative exercises for a missing required path, public-path drift, forbidden module edge, stale
lockfile, and failed Docker build.
### Phase 6 — Reassess automation
Run a real Renovate dependency update with automerge disabled. Re-enable limited automerge only by a
separate human decision backed by the acceptance evidence.
## 9. Verification Strategy
1. Mode A hashes match the authoritative export, or Mode B records the human decision, new hashes,
and `controlled-reconstruction` provenance.
2. Harness unit, mutation, schema, renderer, and parity tests pass under the selected mode.
3. Control-plane validator passes on the complete tree and fails on each seeded missing/shadow
mutation.
4. Task-packet resolver emits a stable high-risk CI/deployment packet from the recorded overlay;
the packet and governing-rule checksums verify again at the Task 3 boundary.
5. Gradle wrapper, project discovery, architecture dependency verification, focused ArchUnit,
dependency locks, `test`, and `check` pass.
6. Public-path verification passes with the committed baseline and fails when it is absent or
changed.
7. Production and sample Docker images build from repository-root context.
8. Gitea reports repository Actions enabled and at least one authorized runner online.
9. The preflight and quality fan-in statuses appear on a real pull request.
10. Protected branches reject seeded failures.
11. Renovate config validation and a real dependency update pass without automerge.
## 10. Risks and Countermeasures
| Risk | Countermeasure |
| --- | --- |
| Reconstructed policy differs from the lost authority | Require the human Mode B decision, label provenance as reconstruction, assign new hashes, and require schema/parity/mutation/Gradle evidence |
| `.gitea/workflows` silently shadows canonical workflows | Block the directory in the physical preflight and confirm instance `WORKFLOW_DIRS` |
| Runner can expose host Docker authority | Use a repository-scoped isolated runner, restrict fork execution, and keep registration credentials outside jobs |
| Workflow exists but repository Actions remains disabled | Require API `has_actions: true` and a real run before branch-protection setup |
| Required status name changes and bypasses protection | Keep stable job names in the gate matrix and verify protection after workflow changes |
| Public-path baseline is regenerated in CI | Separate update and verify tasks; verification fails when the committed file is missing |
| Docker root context sends secrets | Root `.dockerignore` excludes environment/secret paths and CI checks the context contract |
| Renovate updates declarations without strict locks | Run `verifyDependencyLocks` before tests and keep automerge disabled through a real update exercise |
| Restored workflows assume GitHub-only behavior | Exercise every event, context, action, and fan-in on Gitea 1.27.0 before making the status required |
## 11. Acceptance Criteria
- Mode A has a byte-identical authoritative inventory, or Mode B has a human-recorded reconstruction
decision, new provenance, preliminary hashes, and a post-change complete hash inventory.
- The 2026-07-20 harness registry, validators, generated agents, mutation suite, and parity checks
pass under the selected mode.
- A stable task packet is resolved after recovery; no implementation-complete claim relies on the
pre-recovery state. Its overlay, output packet, packet checksum, and rule checksums are retained in
the recovery evidence directory.
- Fresh checkout control-plane preflight reports no missing required path.
- `.gitea/workflows` is absent and `.github/workflows` is recognized by the Gitea instance.
- Repository API reports `has_actions: true`.
- An authorized runner inventory reports an enabled online runner with the workflow label.
- Gradle `projects`, dependency locks, architecture gates, focused tests, `test`, and `check` pass.
- Missing or changed public-path baseline fails read-only verification.
- Production and sample images build from repository-root context without a duplicated registry.
- The quality `release-gate` and vulnerability status are required on the protected branch.
- Release-blocking preflights run the full harness unit/mutation, module validation, renderer/parity,
gate-matrix, and structured Trivy checks; Trivy explicitly consumes `.trivyignore.yaml`.
- Renovate automerge remains disabled until the explicit five-part re-enable condition is met.
- README, gate matrix, workflow jobs, and physical repository paths agree.
- No agent stages, commits, amends, or pushes the recovery.
## 12. External Authorities
- [Gitea Actions quick start](https://docs.gitea.com/usage/actions/quickstart): repository Actions
enablement, runner requirement, and the `.gitea/workflows` quick-start location.
- [Gitea configuration cheat sheet](https://docs.gitea.com/administration/config-cheat-sheet):
`[actions].ENABLED` and the default
`WORKFLOW_DIRS=.gitea/workflows,.github/workflows` first-existing-directory behavior.
- [Gitea runner documentation](https://docs.gitea.com/usage/actions/act-runner): repository-scoped
registration, runner modes, credential handling, and Docker isolation trade-offs.
@@ -0,0 +1,389 @@
# 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.