chore: plan 파일 업로드
This commit is contained in:
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.
|
||||
Reference in New Issue
Block a user