chore: initialize from backend template 0a6dd0e

This commit is contained in:
DongHyeonka
2026-08-13 20:31:02 +09:00
commit e64e701fe5
3223 changed files with 388401 additions and 0 deletions
@@ -0,0 +1,191 @@
> **SUPERSEDED — HISTORICAL PROVENANCE ONLY (2026-07-25):** The user-approved harness-free
> Mode B amendment supersedes this design. Retain the body as historical provenance; it is not
> executable instruction.
# Harness Policy Engine Refactoring Design
- **Date:** 2026-07-20
- **Status:** Approved by user request
- **Scope:** repository-local development harness (`.harness`, `.agents`, `.claude`, `.codex`, root/module guidance, Gradle module registry integration)
- **Source:** user-provided “개발 하네스 분석·리뷰” plus repository evidence gathered on 2026-07-20
## 1. Problem Statement
The repository now has 19 nested Gradle leaf modules, but the write-time import gate,
agent prompts, runner allowlist, and root guidance still contain parts of the previous flat
module topology. Platform variants are copied manually, so commit policy and orchestration
already differ between Claude, Codex, and Antigravity. Verdict validation checks a text
summary but does not consistently require enum fields, non-negative counts, or arithmetic
balance.
The harness must move from duplicated platform prompts to a small policy engine with one
project manifest, deterministic renderers, strict validators, and platform adapters.
## 2. Goals
1. Make the actual nested Gradle topology a single machine-readable source of truth.
2. Resolve a touched file to its nearest owning leaf module without assuming `src/<module>`.
3. Generate write-time import policy and focused Gradle task validation from that registry.
4. Validate machine verdicts with required fields, non-negative integers, arithmetic rules,
upstream evidence, revision identity, and TDD red evidence for behavior changes.
5. Materialize validated evidence as JSON artifacts that platform hooks can share.
6. Render Claude, Codex, and Antigravity agent variants from one canonical source and fail
parity checks when generated files drift.
7. Use one human-only commit policy on every platform.
8. Replace file-count and exhaustive-report rules with risk and review profiles.
9. Add mutation and cross-platform static parity tests.
## 3. Non-Goals
- This change does not run authenticated end-to-end golden tasks inside all three external
products. It supplies the deterministic fixtures and validators those runs will consume.
- It does not add application features or alter production Java behavior.
- It does not require PyYAML, jsonschema, Pydantic, or another runtime dependency. Harness
data files use JSON syntax, which is valid YAML, and validators use Python stdlib only.
- It does not make natural-language agent self-reports authoritative. Hooks convert accepted
reports into evidence artifacts; validators remain authoritative.
## 4. Architecture
```text
.harness/project/modules.yaml ──┬── Gradle settings/includes
├── Gradle dependency verification
├── owning-module resolver
├── import gate
└── Gradle command validator
.harness/agents/*.md + platforms.yaml
└── render_agents.py
├── .claude/agents/*.md
├── .codex/agents/*.toml
└── .agents/agents/*/agent.json
Claude hook ───────────────┐
Antigravity hook adapter ──┼── verdict validator ── evidence JSON
Codex validation command ──┘
```
### 4.1 Project registry
`modules.yaml` contains, per leaf module:
- stable module id
- repository-relative source path
- Gradle path
- role
- Java package roots (informational and import-policy lookup only)
- allowed project dependencies
- focused test command
- profiles/capabilities
- owning `CLAUDE.md` when present
- an intentional mutation import used by gate tests
`src/settings.gradle` reads the registry to declare projects. The
`verifyCleanArchitectureDependencies` task reads the same registry instead of maintaining a
second dependency map.
### 4.2 Owning-module resolution
Owner selection uses the longest filesystem-boundary match among registered leaf source
paths. Package prefixes never decide ownership because `support` owns a broad
`dev.caskeleton.adapter.outbound` package and sample code mirrors production packages.
Instruction discovery walks upward from the touched file and returns the nearest
`CLAUDE.md`; if a leaf has none, root `CLAUDE.md` and `AGENTS.md` are the explicit fallback.
### 4.3 Import gate
The import gate first resolves the registered leaf module, then applies:
- dependency-derived sibling module isolation
- role-specific framework rules for domain, application, inbound, outbound, persistence,
identifier, shared-contract, bootstrap, and sample roles
- global unsafe-pattern checks
All registered production modules receive a mutation test using their real nested source
path. Sample-only exemptions are explicit registry data, not accidental regex misses.
### 4.4 Verdict and evidence
The canonical verdict schema requires `agent`, `verdict`, `task_id`, `revision`, and agent
specific evidence. Non-blocked verdicts require every declared field. Counts are non-negative.
Required equations include:
- spec totals balance
- Gradle `run = passed + failed + skipped`
- ready Gradle results include at least one command and no failed command
- behavior-changing implementation requires at least one observed red test
- quality-ready references validated architecture and spec artifacts
Claude's fenced `ca-verdict` remains a compatibility input, but its accepted form is converted
to the same JSON evidence model. Missing or malformed payloads for detected CA agents fail
closed. Evidence records include a source-message hash and current revision/diff identity.
### 4.5 Platform rendering and hook adapters
Canonical agent Markdown lives under `.harness/agents/`; platform metadata lives in
`.harness/project/platforms.yaml`. Generated files carry `generated_from`, `source_hash`,
`generator_version`, and `do_not_edit` metadata.
Antigravity gains a plugin `hooks.json` and a platform adapter using the documented camelCase
stdin/stdout contract. Claude keeps its native hook entry points but calls the common library.
Codex variants instruct the runner/reviewer to invoke the common validation command because
the repository has no equivalent local lifecycle-hook registration surface.
### 4.6 Risk and review profiles
Risk is determined by change surface, not file count:
- high: security, migration/schema, public contract, module dependency, architecture rule,
transaction/concurrency, CI/deployment
- medium: behavior, multiple modules, external integration
- low: docs/comments, test fixture, local refactor protected by characterization tests
Review profiles:
- `review-lite`: direct diff references; no saved report by default
- `review-standard`: verify blocking citations; one report only when risk or findings justify it
- `audit-deep`: verify all quotes and persist detailed findings
- `regulated`: immutable evidence and full traceability
Option analysis uses a dependency DAG and at most 35 materially distinct alternatives.
Counterarguments are required for judgment-dependent findings, not deterministic failures.
## 5. Commit Policy
All platforms use `human-only`. Implementers never stage or commit. Reviewers may inspect a
working-tree diff before commit or an explicit immutable range after the human commits.
## 6. Verification Strategy
1. Stdlib unit tests for registry loading and owner resolution.
2. Mutation tests for every registered production module path.
3. Strict verdict negative tests: missing fields, negatives, arithmetic imbalance, missing
upstream evidence, revision mismatch, and behavior change without red evidence.
4. Golden renderer tests and `--check` parity validation.
5. JSON validation of generated Antigravity hook and agent files.
6. Gradle `projects`, architecture dependency verification, focused ArchUnit test, and full
`check` after harness tests pass.
## 7. Migration and Compatibility
- Existing fenced verdicts remain parseable only when they satisfy the new required fields.
- Generated platform files are overwritten only by the renderer and documented as generated.
- Root and module guidance is updated to the registered nested topology.
- Actual external cross-platform golden executions remain a follow-up; static parity and seeded
mutation coverage become mandatory in this change.
## 8. Acceptance Criteria
- A seeded forbidden import under every nested production module is rejected.
- No legacy flat adapter path remains in gate tests or agent task allowlists.
- `settings.gradle`, dependency verification, import gate, and Gradle runner resolve the same
19 leaf modules from `modules.yaml`.
- Missing/negative/inconsistent ready verdicts fail validation.
- Claude and Antigravity adapters invoke the shared validator; accepted verdicts produce JSON
evidence.
- Rendering followed by `--check` reports no platform drift.
- Agent variants uniformly state human-only commit policy and risk-based orchestration.
- `N!` enumeration, all-quote routine grep, file-count report splitting, and unconditional
counterargument requirements are absent from active rules.
- Harness tests and Gradle checks pass, or every unrun/failing command is reported with risk.
@@ -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,87 @@
# Application Outbox Failure Reporting — Harness-Free Design
## Context
`application-core` currently carries Spring Boot and SLF4J only because
`PublishPendingOutboxEventsUseCase` renders relay failures itself. That reverses the diagnostic
dependency direction and also permits a duplicate WARN in `OutboxMessagePublishAdapter`.
This change is harness-free: `src/config/architecture/modules.json`, Gradle, ArchUnit, and focused
module tests are the policy and evidence authorities. No `.harness` files or public paths change.
## Boundary
`application-core` owns a specific `OutboxRelayFailureReportPort` and an immutable
`OutboxRelayFailureReport`. The report is an allowlist containing only:
- `OperationalError code`
- event, aggregate, and correlation identifiers
- event type, attempt count, optional next-attempt time
- the originating `RuntimeException`
It never carries the payload, idempotency key, message template, severity, arbitrary fields, or the
whole `OutboxEvent`. Factories and record invariants admit only retryable
`OUTBOX_PUBLISH_FAILED` reports with a next-attempt time and terminal `OUTBOX_DEAD_LETTER` reports
without one.
`adapter:outbound:messaging` owns `Slf4jOutboxRelayFailureReportAdapter`. It maps the typed report to
one canonical SLF4J 2 fluent ERROR with fixed key names and runbook links. Bootstrap only wires the
port.
## Ordering and Failure Semantics
The persisted FAILED or DEAD transition is authoritative:
1. broker publication fails;
2. the application calculates the transition;
3. the store transition succeeds inside `TransactionPort`;
4. only then is the typed report emitted.
A transition failure propagates and emits no report. A reporter `RuntimeException` is contained by
both the adapter and the use case, so it cannot change the relay outcome or prevent later events
from running. Successful publication and `markPublished` failures emit no failure report.
There is no production no-op reporter. `MessagingConfig` always contributes exactly one reporter
bean, using the configured broker name or `disabled` when blank. `OutboxMessagePublishAdapter`
becomes mapping/send-only: runtime failures propagate, checked failures are wrapped with their
cause, and it emits no success or failure log. The general `OutboundMessagePublisher` retains its
existing fail-open dependency logging.
## Structured ERROR Contract
Every confirmed transition produces one ERROR with the common fields:
`error.code`, `error.category`, `dependency_name`, `dependency_type=messaging`, `outcome`,
`event_id`, `event_type`, `aggregate_id`, `correlation_id`, `attempt_count`, and `runbook_link`.
Retryable failures additionally carry `next_attempt_at`. Mappings are:
| Code | Outcome | Runbook |
| --- | --- | --- |
| `OUTBOX_PUBLISH_FAILED` | `FAILED` | `runbook://outbox/publish-failed` |
| `OUTBOX_DEAD_LETTER` | `DEAD` | `runbook://outbox/dead-letter` |
The originating exception is attached as the throwable. Payload, idempotency key, envelope data,
message templates derived from the exception, and arbitrary exception fields are forbidden.
The adapter's fail-open boundary also applies to invalid direct calls: `report(null)` must never
throw. The focused structured-adapter test pins this behavior.
## Enforcement and Tests
- Value tests enforce invariants and reflectively pin the exact record component allowlist.
- Relay tests pin transition-before-report ordering, no-report paths, exact cardinality, and
reporter containment.
- Messaging tests capture Logback events and pin level, fields, throwable, and unsafe-data absence.
- `verifyApplicationCoreDependencyPurity` rejects non-project production declarations and forbidden
Spring/logging/metrics groups on resolved application classpaths.
- `APPLICATION_HAS_NO_DIAGNOSTIC_FRAMEWORK` bans SLF4J, JUL, Logback, Log4j, and Micrometer from
the exact `dev.caskeleton.application..` scope. Its dedicated violation fixture also resides
inside that scope, under `dev.caskeleton.application.architecture.violations`, proving the rule
is non-vacuous.
- `application-core` test dependencies are reduced to JUnit Jupiter and AssertJ; all other leaves
keep the shared Spring Boot test baseline.
## Scope
No public path, CI workflow, module-registry edge, payload shape, outbox persistence schema, or
general publisher logging behavior changes. Agents do not stage, commit, amend, or push.
@@ -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,59 @@
# Harness-Free Mode B Amendment
- **Date:** 2026-07-25
- **Status:** Approved scope amendment
- **Mode:** B — controlled reconstruction from repository evidence
- **Supersedes:** `2026-07-20-harness-policy-engine-design.md` and
`2026-07-20-harness-policy-engine.md` in full as executable guidance; both superseded documents
remain only as historical provenance
## Decision
The repository will recover Gradle configuration and Clean Architecture dependency enforcement
without reconstructing the absent development harness. A Gradle-owned JSON registry at
`src/config/architecture/modules.json` becomes the single source of truth for the current 19 leaf
modules, their repository-relative source paths, Gradle paths, and allowed production project
dependencies.
Both `src/settings.gradle` and `verifyCleanArchitectureDependencies` consume that file. Settings
validation fails closed for malformed, empty, duplicate, unsafe, or missing module entries. The
dependency gate continues to require complete leaf coverage and reject unapproved production
project edges; production leaves may never depend on the `sample-portfolio` fixture consumer.
## Evidence and provenance
Registry entries are reconstructed from the checked-in Gradle topology and each leaf
`build.gradle`'s `api`, `implementation`, `compileOnly`, and `runtimeOnly` project dependencies.
Test-only and fixture-only configurations are not architecture production edges. This is Mode B
provenance: it restores the repository's observable build contract, not unavailable historical
artifacts.
The pre-change RED command is:
```bash
cd src
./gradlew help --console=plain
```
It fails because `src/settings.gradle` requires the absent
`.harness/project/modules.yaml`.
## Explicit non-goals
- No `.harness/` tree, task resolver, task packet, or policy-hash runtime.
- No `.agents/`, `.claude/`, `.codex/`, agent plugin, hook, renderer, or platform parity
reconstruction.
- No production Java or runtime behavior change.
- No byte-identical restoration claim.
- No claim that the earlier Harness Policy Engine plan or the broader refactor is complete.
## Enforcement and workflow
Gradle and CI gates replace harness runtime dependencies for module discovery and dependency
policy. Root and module guidance point to the Gradle-owned registry and retain the eight local
HARD-STOP meanings, architecture responsibilities, focused-test discipline, human-only git
policy, and LLM Wiki capture workflow.
Acceptance requires successful Gradle `help`, `projects`, and
`verifyCleanArchitectureDependencies`, an independent deterministic 19-leaf registry check,
`git diff --check`, and a reviewed working-tree status.
@@ -0,0 +1,97 @@
# Harness-Free Quality and Security CI Design
- **Date:** 2026-07-25
- **Status:** Approved Mode B reconstruction
- **Scope:** Repository-internal quality, dependency-vulnerability, and link-check controls
## Decision and provenance
Mode B reconstructs observable CI contracts from the current Gradle build, active documentation,
and the incomplete `/home/donghyeon/dev/ca-tmpl` checkout. The candidate checkout is evidence, not
an authoritative or byte-identical restoration source. Its useful policy is adapted to the current
`main` branch and current tasks; stale `master`, feature-branch ownership, and absent workflow
claims are removed.
`.github/workflows/` is the canonical workflow path. No `.gitea/workflows` shadow is created. The
origin is Gitea, but server-side Actions is externally disabled, so these files define repository
controls without claiming that remote jobs currently execute.
Every external `uses:` reference is pinned to a verified 40-character commit SHA. Its immutable
release tag remains beside the SHA as an inline review label; moving major-version tags are not an
execution authority.
## Scope boundary
This slice owns:
- pinned Java tool evidence and text/binary normalization;
- structured Trivy suppression governance and CODEOWNERS review surfaces;
- the quality-gate matrix and its drift verifier;
- quality, filesystem vulnerability, and documentation-link workflows;
- human-readable dependency severity, suppression, network, and forge-compatibility policy.
The development harness remains excluded: no `.harness`, `.agents`, `.claude`, or `.codex`
runtime is reconstructed. Build/release supply-chain, tag release, image scanning, signing,
provenance, SBOM, retention, and Docker root-context work belongs to the later Phase A2 slice and
is not represented as a present workflow job.
## Considered approaches
1. Copy the candidate files unchanged. Rejected because they target `master`, refer to missing
supply-chain scripts/jobs, and describe obsolete branch ownership.
2. Reconstruct a minimal current control plane from repository evidence. Selected because every
gate can be checked against a present Gradle task, test, script, or workflow job.
3. Merge all checks into one workflow. Rejected because GitHub-only dependency APIs need forge
guards, scheduled vulnerability scans have different triggers, and link checks are path-scoped.
## Components and gate flow
`ci-quality-gates.yml` runs three required jobs: the aggregate Gradle quality suite, the sample-off
axis, and gate-matrix lint. Before Java setup or Gradle, the quality job requires
`docs/security/public-paths-snapshot.txt` to be committed and non-empty. The worktree now contains
the canonical baseline for `/api/healthcheck`; because agents do not stage or commit, a human must
track and commit it before CI's `git ls-files` precondition can pass. This prevents
`verifyPublicPathSnapshot` from creating a first-run baseline inside CI and passing without
comparison.
`release-gate` uses `if: always()` and accepts only `success` from those three jobs; the advisory
quarantine job is deliberately outside its `needs`.
The quality aggregate runs `check`, `verifyPublicPathSnapshot`, and `verifyDependencyLocks`
explicitly. `check` already pulls in Clean Architecture dependency enforcement, environment/readme
drift checks, Trivy-ignore governance, format/static analysis, normal tests, and quarantine sunset.
`dependency-vulnerability.yml` keeps GitHub Dependency Graph operations behind
`github.server_url == 'https://github.com'`. Platform-neutral `trivy-fs` runs for PR, `main` push,
daily schedule, and manual dispatch. Trivy and jq install into `${RUNNER_TEMP}` and expose their
directories through `${GITHUB_PATH}`. Every Trivy scan names `.trivyignore.yaml`; High/Critical and
KEV matches block, while Medium/Low only report. The KEV gate first rejects blank metadata,
non-positive/non-integral or mismatched counts, empty arrays, invalid CVE identifiers, and duplicate
identifiers. It separately rejects malformed/empty Trivy JSON before extracting candidate IDs.
Dependency review reports through its check only and does not request permission to write a PR
summary comment. Vulnerability DB, tool release, malformed/empty KEV or Trivy data, and KEV feed
network failures remain blocking unless internal mirrors are configured.
`link-check.yml` is path-scoped for PR and `main` push, and remains manually runnable.
## Drift verification and failure behavior
`.github/ci-gate-matrix.yml` lists only current mechanisms/jobs. The verifier resolves the
repository root from its own physical location, rejects incomplete/duplicate records, and checks
referenced Gradle custom tasks, plugins, contract-test files, workflow files, and job IDs.
Delegated-pending is supported only when a row is explicitly marked; no absent supply-chain job is
invented in this slice.
The CI release fan-in fails for failed, cancelled, or unexpectedly skipped required jobs. Trivy's
KEV feed cross-check is fail-closed. GitHub-only jobs may skip by their explicit forge/event
conditions and are not dependencies of the quality release fan-in.
## Verification
Acceptance requires the prescribed RED for the absent `.trivyignore.yaml`, GREEN
`verifyTrivyignore`, proof that the snapshot precondition rejects missing, empty, or untracked
baselines, and a human-tracked canonical snapshot for CI. It also requires strict synthetic KEV
catalog negative/positive cases, shell syntax and matrix verification, workflow YAML/static checks,
evidence that `main` is the only active branch trigger, Trivy ignorefile coverage, exact release
fan-in, absence of harness/Gitea shadow workflows, `git diff --check`, and reviewed working-tree
status. Network Trivy scans are intentionally not run locally.
@@ -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.
@@ -0,0 +1,173 @@
# Harness-Free Module and Gradle Hygiene Design
- **Date:** 2026-07-25
- **Status:** Approved
- **Mode:** B reconstruction without `.harness`
- **Scope:** all 19 Gradle leaves, dependency declarations, test baselines, Mongo scaffolding,
runtime-composition documentation, and dependency locks
- **Topology SSOT:** `src/config/architecture/modules.json`
## 1. Context
The 19-leaf project dependency graph obeys the registered allowed edges, and the three core
production source sets are free of Spring, persistence, transport, logging, and metrics imports.
The audit nevertheless found a wider declared graph than the source graph, Spring WebMVC test
libraries on pure-core test classpaths, Boot 3-era OpenAPI tooling on Spring Boot 4, example-domain
code in the production Mongo adapter, and direct MDC access in sample application services.
This design follows the user-approved Mode B reconstruction. It does not recreate or depend on
`.harness`; settings and verification continue to consume the JSON registry.
## 2. Goals
1. Keep the exact 19 leaves and all allowed project edges in the JSON registry.
2. Remove only dependencies proven unnecessary by source/test inspection plus focused
compile/test verification.
3. Give `domain-core`, `application-core`, and `shared-contract` JUnit/AssertJ-only test
classpaths.
4. Keep Spring Boot 4.0.0 and replace `springdoc-openapi` 2.x with the Boot 4-compatible 3.0.0
line.
5. Remove unused direct Jackson 2 declarations from GraphQL and WebSocket.
6. Require the Spring configuration processor exactly in leaves whose main source declares
`@ConfigurationProperties`.
7. Remove adapter-local `Example*` business concepts from `persistence-mongo`; retain only
opt-in Mongo infrastructure and typed enablement settings.
8. Replace sample application-layer MDC reads with an application-owned correlation-context port
implemented by the inbound web adapter.
9. Remove tracked jqwik runtime state and ignore future `.jqwik-database` files.
10. Describe the default bootstrap as the default runtime composition, not as wiring every
optional leaf.
11. Regenerate only affected strict dependency locks and finish with the full release gates.
## 3. Non-goals
- No endpoint, persistence schema, public response, outbox transition, or sample-domain behavior
change.
- No version catalog, convention-plugin, `buildSrc`, module rename, or registry schema expansion.
- No automatic addition of GraphQL, gRPC, WebSocket, Mongo, file server, or object storage to the
default `app-bootstrap` runtime.
- No stage, commit, amend, or push.
## 4. Approved dependency decisions
An allowed registry edge is permission, not an obligation to declare it.
| Leaf | Remove after focused proof | Preserve |
| --- | --- | --- |
| `application-core` | unused `domain-core` edge | `shared-contract` |
| `inbound:web` | unused `domain-core` edge | application/shared and transport dependencies |
| `inbound:graphql` | application/domain edges, direct Jackson 2, unused processor | shared and GraphQL/web test transport |
| `inbound:grpc` | application/domain edges, unused annotations/direct protobuf declarations | shared, netty, services, configuration processor |
| `inbound:websocket` | application/shared edges, direct Jackson 2 | domain, WebSocket, configuration processor |
| `outbound:support` | domain/application/shared edges | autoconfigure and SLF4J API |
| `outbound:cache-redis` | domain/application, unused Groovy/Spock | shared/support |
| `outbound:httpclient` | domain/application | shared/support, actual Groovy/Spock tests |
| `outbound:identifier` | domain, `uuid-creator` | application, actual Groovy/Spock tests |
| `outbound:messaging` | domain, unused Groovy/Spock | application/shared/support/SLF4J |
| `outbound:notification` | domain, unused Groovy/Spock | application/shared/support/web/SLF4J |
| `outbound:persistence-jpa` | domain; explicit Flyway core only if focused compile proves the starter sufficient | application/shared/JPA/PostgreSQL |
| `outbound:persistence-mongo` | application/shared, `Example*`, example Testcontainers tests | Mongo opt-in infrastructure/settings |
| `outbound:fileserver` | broad Boot starter | application/shared, autoconfigure, SLF4J |
| `outbound:objectstorage` | broad Boot starter | application/shared/AWS, autoconfigure, SLF4J, vendor IT |
Production composition-root dependencies remain even when bootstrap source does not statically
import their types: their purpose is runtime assembly. Duplicate test declarations may be removed
only when the focused test classpath continues to compile and execute.
## 5. Pure-core test and verification policy
`domain-core`, `application-core`, and `shared-contract` receive only JUnit Jupiter, AssertJ, and
the JUnit launcher from the root convention. All other leaves keep the existing Spring test
baseline in this change; family-wide convention plugins are out of scope.
The existing application dependency-purity gate remains. A new registry-driven configuration
processor parity gate applies this Boolean invariant to every leaf and is wired into `check`:
main source contains one or more exact `@ConfigurationProperties(` occurrences if and only if the
leaf `build.gradle` contains exactly one Spring configuration-processor declaration. It must ignore
`@ConfigurationPropertiesScan`; the number of settings classes is not compared with the number of
processor declarations.
## 6. Spring Boot 4 compatibility
The web adapter changes
`org.springdoc:springdoc-openapi-starter-webmvc-api:2.8.6` to `3.0.0`, the first stable
springdoc line released for Spring Boot 4.0.0. The existing sample tests that boot a real server
and call `/v3/api-docs` are the behavior gate. Snapshot changes are accepted only if they are a
deterministic library-version result and retain the public API contract.
Springdoc 3 otherwise widens `ApiError.details` from the committed `type: object` to an
unconstrained OAS 3.1 schema. A web-adapter-owned `OpenApiCustomizer` must restore the object schema
in the final generated document. Both real-server test applications import that production
configuration. `shared-contract` remains free of Swagger annotations and dependencies.
GraphQL and WebSocket remove direct `com.fasterxml.jackson` declarations because neither source
set imports them and Spring Boot 4 owns its JSON stack through the relevant starters.
The web adapter retains the `JsonNullable` value type, but its `0.2.6` artifact also declares
Jackson 2 transitively while this repository supplies explicit Jackson 3 serializers. Before and
after dependency insight plus focused present/null/undefined serialization tests determine whether
that transitive edge can be excluded. Exclusion is applied only if those tests and the real-server
OpenAPI tests pass; springdoc/Swagger's independently required JSON graph is not removed by
assumption.
## 7. Mongo production boundary
Delete the adapter-local `ExampleRecord`, document, mapper, repository, repository adapter, and
their tests. `MongoPersistenceConfig` remains conditional on
`ca-skeleton.persistence-mongo.enabled=true` and explicitly imports the Mongo client/data
auto-configurations without owning a fake business repository.
The starter also registers Mongo auto-configuration directly through Boot metadata, independently
of `MongoPersistenceConfig`. A module-level `AutoConfigurationImportFilter`, registered through
Boot 4's `META-INF/spring.factories` discovery path, must exclude the Boot 4 sync/reactive client,
data, repository, health, and metrics Mongo auto-configurations while the enable property is absent
or false. It must allow them unchanged when the property is true; consumers must not need to set
`spring.autoconfigure.exclude`.
Replacement tests must prove:
- an actual `@EnableAutoConfiguration` context in default/false mode creates no Mongo
infrastructure;
- properties bind the enable flag;
- enabled mode can create the infrastructure with a supplied mock `MongoClient`, without a real
network connection;
- production source contains no `Example*` type.
The Testcontainers dependencies leave this module when the example repository IT is removed.
## 8. Correlation context boundary
`application-core` owns a framework-free `CorrelationIdPort` whose read result is optional.
`adapter:inbound:web` implements it from the sanitized request MDC correlation key.
`CreateWorkLogUseCase` and `PosterEventPublisher` depend only on the port and preserve the current
fallback to the generated event id when no correlation id exists.
Tests first pin present/blank/absent behavior and prove the sample application packages no longer
import SLF4J/MDC. Diagnostic storage remains an adapter concern.
## 9. Runtime composition and generated state
`app-bootstrap` keeps its current default runtime modules. Its build description and README must
state that optional leaves require an explicit registry and composition-root dependency change.
Optional adapters remain independently buildable and testable.
The tracked four-byte `src/sample-portfolio/.jqwik-database` is generated runtime state. Delete it
and add `.jqwik-database` to `src/.gitignore`; retain jqwik itself because property tests use it.
## 10. Verification
Run focused compile/tests before and after each dependency group. Regenerate locks only through
each affected leaf's `:leaf-path:resolveAndLockAll --write-locks` task, then run:
```bash
cd src
./gradlew check --console=plain
./gradlew test --console=plain
./gradlew verifyCleanArchitectureDependencies --console=plain
./gradlew verifyApplicationCoreDependencyPurity --console=plain
./gradlew verifyConfigurationPropertiesProcessor --console=plain
./gradlew verifyDependencyLocks --console=plain
./gradlew verifyPublicPathSnapshot verifyEnvKeys --console=plain
```
Completion requires fresh review, `git diff --check`, and an LLM Wiki branch note or an explicit
capture blocker for the mandated exact vault path.
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
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,546 @@
# Fileserver R2 Control Plane and Provider Selection Design
- Date: 2026-07-28
- Status: 구현·전체 repository gate·독립 spec/quality review 완료
- Scope: provider-neutral R2 control plane, explicit destination/provider selection, first
`local-persistent` qualification provider
- Parent:
[Fileserver Production Capability Deep Design](2026-07-26-fileserver-production-capability-design.md)
## 1. 목표
현재 `LocalFilePublicationAdapter`의 single-node process-restart R1을 운영 topology의 기본값으로
승격하지 않는다. 이번 increment는 다음을 구현한다.
1. application에는 기존 provider-neutral `FilePublicationPort`만 유지한다.
2. adapter 내부에 destination binding, provider descriptor, durable operation/manifest/reference
control plane을 둔다.
3. 활성화된 Fileserver는 정확한 destination과 provider를 명시해야 하며 implicit local fallback을
금지한다.
4. 첫 qualification provider로 pre-provisioned persistent filesystem을 사용하는
`local-persistent`를 구현한다.
5. `shared-mounted``sftp`가 같은 control-plane state machine을 재사용할 수 있게 하되 이번
increment에서 가짜 provider나 동작하지 않는 bean을 만들지 않는다.
`local-persistent`는 container writable layer나 임시 디렉터리를 의미하지 않는다. 단일 노드 또는
node-attached persistent volume과 private owner boundary가 증명된 환경만 대상으로 한다.
## 2. 비범위
이번 increment에 포함하지 않는다.
- NFS 또는 다른 shared mount의 multi-client correctness;
- SFTP SDK, connection pool, credential, OpenSSH qualification;
- cross-node producer fencing;
- background reaper, retention delete, quota reservation;
- metrics/tracing/health implementation;
- optional content read/delete/list API;
- object storage. Object storage는 별도 outbound leaf의 책임이다.
이 항목은 seam만 만들지 않는다. 실제 semantic provider를 구현하는 후속 increment에서만
dependency, bean, setting을 추가한다.
## 3. 검토한 접근
### A. 현재 local adapter를 바로 R2로 표시
설정과 change surface는 작지만 provider selector, terminal manifest, opaque-reference direct
lookup과 strict startup evidence가 없다. R2를 과장하므로 선택하지 않는다.
### B. Local, NFS, SFTP를 동시에 구현
최종 기능은 많지만 서로 다른 보장과 real-service CI가 한 change surface에 결합된다. NFS와
OpenSSH 인프라가 없으면 검증되지 않은 provider가 남으므로 선택하지 않는다.
### C. Provider-neutral control plane + local-persistent 첫 qualification
공통 state machine과 binding을 먼저 고정하고 한 provider를 실제 crash/security 테스트로
qualification한다. 이후 provider가 control-plane 계약을 재사용하면서도 각자의 보장을 별도로
증명할 수 있다. 이 접근을 선택한다.
## 4. 계층과 모듈 경계
```text
application-core
FilePublicationPort
FilePublishRequest
FilePublishReceipt
|
v
adapter:outbound:fileserver
RoutingFilePublicationAdapter
|
+-- DestinationBindingRegistry
+-- FilePublicationProviderRegistry
+-- DurablePublicationCoordinator
+-- ProviderControlPlane
|
+-- LocalPersistentPublicationProvider
```
- application/domain에는 provider ID, filesystem path, manifest locator, Spring 또는 NIO 타입을
추가하지 않는다.
- `RoutingFilePublicationAdapter`만 production `FilePublicationPort` bean이다.
- provider와 control-plane SPI는 fileserver package 내부 타입이다. 범용 filesystem/SDK API를
public bean으로 노출하지 않는다.
- `shared-mounted``sftp` 타입 값은 구현 전까지 accepted setting으로 등록하지 않는다.
## 5. Application 계약 변경
기존 request와 opaque reference를 유지한다. R2 provider가 달성한 보장을 정확히 보고할 수 있도록
`FilePublishReceipt.DurabilityGuarantee`에 다음 값만 추가한다.
```text
FILE_AND_DIRECTORY_SYNC
```
이 값은 startup probe와 process-crash qualification을 모두 통과한 provider만 반환한다.
호출한 sync가 물리 device, volume replica 또는 storage-controller power-loss protection까지
완료됐다는 뜻은 아니다. 그 축은 deployment/storage evidence로 별도 판정한다.
`PROCESS_LOCAL_SYNC` 또는 `PROVIDER_ACK_ONLY`를 요구 보장보다 약한 상태에서 자동으로 R2 값으로
올리지 않는다.
새 opaque reference 형식은 다음 의미를 가지되 application은 내부 segment를 해석하지 않는다.
```text
fsr1.<route-token>.<file-id>.<check-digits>
```
- `route-token`: destination binding의 canonical policy digest에서 재시작 안정적으로 파생한
bounded route allowlist 값. 형식은 `r` + digest의 첫 31 lowercase hex이며 startup에서 token
collision을 거부한다;
- `file-id`: CSPRNG 128-bit 이상;
- `check-digits`: accidental truncation/corruption 검출;
- provider locator, operation ID, tenant/user ID, host/path는 포함하지 않는다.
Reference는 authorization token이 아니다. authorization은 application use case의 책임이다.
## 6. 명시적 설정과 선택
새 canonical prefix는 `app.fileserver`다.
```yaml
app:
fileserver:
enabled: false
destinations:
local-export:
provider-ref: local-primary
required-publication: unique-atomic-create
required-durability: file-and-directory-sync
maximum-rows: 1000000
maximum-encoded-bytes: 1073741824
providers:
local-primary:
type: local-persistent
root-directory: ${APP_FILESERVER_LOCAL_ROOT:}
auto-create: false
strict-path-security: true
expected-file-store-name: ${APP_FILESERVER_LOCAL_EXPECTED_FILE_STORE_NAME:}
expected-file-store-type: ${APP_FILESERVER_LOCAL_EXPECTED_FILE_STORE_TYPE:}
mount-sentinel-name: .ca-fileserver-volume
mount-sentinel-sha256: ${APP_FILESERVER_LOCAL_MOUNT_SENTINEL_SHA256:}
expected-owner: ${APP_FILESERVER_LOCAL_EXPECTED_OWNER:}
maximum-root-mode: "0750"
```
규칙:
- `enabled=true`이면 destination과 provider가 각각 하나 이상 필요하다.
- 모든 destination은 존재하는 provider 하나를 참조한다.
- provider ID별로 provider/control/payload runtime을 정확히 하나만 만들며 같은 provider를
참조하는 destination은 그 인스턴스를 공유한다. 서로 다른 provider ID가 같은 normalized
root를 가리키면 동일 control namespace의 이중 소유가 되므로 startup에서 거부한다.
- request destination에 binding이 없으면 producer 호출 전에 실패한다.
- provider type의 기본값은 없다.
- `local-persistent` root는 absolute, existing, pre-provisioned directory여야 한다.
- `auto-create=true``local-persistent`에서 거부한다.
- root와 mount sentinel은 operator가 미리 만든다. Root attestation이 끝난 뒤 adapter가 private
top-level control/data directory와 bounded hash shard를 restrictive POSIX creation mode로
생성할 수 있으며, 생성할 때마다 parent identity와 directory sync를 확인한다.
- container ephemeral 경로를 위한 `local-dev`는 별도 후속 profile이다. production 설정과
같은 guarantee를 공유하지 않는다.
- 기존 `ca-skeleton.fileserver.*`는 R1/legacy compatibility selector로만 남는다. 새 R2 설정과
동시에 활성화되면 어느 쪽 filesystem 초기화보다 먼저 startup을 실패시킨다. 양쪽 bean
factory가 같은 ambiguity validator를 호출해 Spring bean 생성 순서에 의존하지 않으며, 암묵
migration이나 conditional precedence를 두지 않는다.
- R2 settings는 unknown field를 거부해 provider/destination 키 오타를 silent fallback으로
취급하지 않는다.
## 7. Startup capability compilation
application traffic을 받기 전에 destination별 effective descriptor를 한 번 compile한다.
Descriptor compilation과 first reservation은 새 설정 키 없이 같은 canonical digest helper를
사용한다.
- startup descriptor는 destination ID, provider ID, limits, required guarantees,
format/encoder revision을 length-prefixed canonical encoding으로 직렬화한
`effectivePolicyDigest`를 freeze한다;
- ordered schema ID/version/column contract를 같은 canonical encoding 규칙으로 계산하는
request별 `schemaDigest`는 first reservation에서 계산한다;
- startup descriptor는 format/encoder revision과 canonical options의 `formatPolicyDigest`
freeze한다;
- `r` + `effectivePolicyDigest`의 첫 31 lowercase hex로 만든 32-character deterministic route
token.
문자열 단순 연결이나 JVM/JSON map iteration order에 digest를 의존시키지 않는다. 같은 startup
allowlist 안에서 route token이 충돌하면 더 긴 prefix로 임의 복구하지 않고 startup을 실패시킨다.
기존 operation은 journal에 freeze된 revision/digest/token으로만 복구하며 현재 설정으로 조용히
재해석하지 않는다.
`local-persistent`는 다음을 모두 검증한다.
1. root와 모든 ancestor가 symbolic link가 아니다.
2. root real path가 설정 absolute path와 일치한다.
3. configured owner와 실제 owner가 일치한다.
4. POSIX permission이 configured maximum보다 넓지 않고 group/world writable이 아니다.
5. `FileStore.name()``type()`이 설정 값과 일치한다.
6. mount sentinel이 regular no-follow file이고 configured SHA-256와 일치한다.
7. data, staging, operations, manifests, references, quarantine directory가 같은
`FileStore`에 있다.
8. control directory는 private owner boundary이며 symlink가 아니다.
9. `SecureDirectoryStream`을 열 수 있다.
10. exclusive create, file force, hard-link create, directory force가 private probe directory에서
성공한다.
Probe artifact는 unique name만 사용하며 successful cleanup과 parent directory force까지
완료해야 한다. Probe 실패는 capability downgrade가 아니라 startup failure다.
JDK가 directory-relative hard-link primitive를 제공하지 않으므로 hard-link publish는 다음
boundary에서만 허용한다.
- root/control/data directories가 adapter owner 전용이고 untrusted writer가 없음;
- publish 직전과 직후 root identity, directory file key, mount sentinel을 다시 확인;
- target은 CSPRNG unique name;
- pre/post identity가 바뀌면 성공을 반환하지 않고 `PUBLISH_INDETERMINATE`;
- privileged host administrator 또는 same-owner malicious process와의 경쟁은 guarantee 범위가
아니며 deployment isolation requirement로 기록한다.
untrusted writer가 같은 root에 entry를 만들 수 있는 환경은 strict local R2가 아니다.
## 8. Durable control plane
```text
.ca-fileserver/
operations/<prefix>/<operation-id>.json
manifests/<prefix>/<file-id>.json
references/<prefix>/<file-id>.json
staging/<prefix>/<operation-id>.part
quarantine/
probe/
data/<prefix>/<generated-file-name>
```
모든 locator는 validated single segment 또는 adapter가 생성한 bounded relative segment다.
Caller path를 받지 않는다. Manifest/reference의 `internalLocator`는 generated filename 한
segment만 저장하고, data shard는 `fileId`의 첫 두 hex에서 파생한다. 따라서 실제 lookup은
`data/<file-id-prefix>/<internalLocator>`이며 control record에 slash를 저장하지 않는다.
### 8.1 Operation journal v2
필수 필드:
```text
schemaVersion
stateRevision
state
operationId
requestFingerprint
effectivePolicyRevision
effectivePolicyDigest
destinationId
providerId
fileId
routeToken
publishedFileName
stageFileName
byteSize
rowCount
columnCount
sha256
formulaMitigatedCount
manifestDigest
referenceDigest
createdAt
sealedAt
publishedAt
lastFailureCode
receiptSnapshot
```
State는 `WRITING`, `SEALED`, `DATA_PUBLISHED`, `MANIFEST_PUBLISHED`,
`REFERENCE_PUBLISHED`, `PUBLISHED`, `QUARANTINED`다.
### 8.2 Private manifest v1
Manifest는 operation/file/provider/reference/fingerprint, schema·format·policy digest, byte/count,
SHA-256, achieved guarantees, internal relative locator를 기록한다. Absolute path, raw row/cell,
credential, raw tenant/user ID는 저장하지 않는다.
### 8.3 Reference index v1
Reference index는 opaque `file-id`에서 operation ID, file version, manifest digest와 internal
relative locator로 direct lookup한다. Directory scan은 receipt restoration의 authority가 아니다.
### 8.4 Record update
각 control record는:
1. sibling private temp file을 `CREATE_NEW`;
2. bounded canonical JSON encoding;
3. file `force(true)`;
4. same-directory atomic replace;
5. parent directory force;
6. read-back schema/revision/digest verification;
순서로 갱신한다. 낮은 revision, fingerprint mismatch, newer schema는 자동 덮어쓰지 않는다.
Operation schema v2는 별도 `formatPolicyDigest` snapshot을 저장하지 않으므로 recovery는 저장된
`effectivePolicyRevision``effectivePolicyDigest`가 현재 compiled destination과 정확히 같을
때만 현재 format-policy digest를 사용한다. Encoder/policy 변경으로 digest가 달라지면 과거
format을 추정하지 않고 indeterminate로 중단한다. 여러 format revision에 대한 forward
recovery는 non-secret policy snapshot을 포함하는 후속 operation schema에서만 지원한다.
Operation direct lookup은 같은 secure relative read에서 schema를 typed dispatch한다. Schema v2는
현재 R2 record로만 decode/write하고, schema v1은 strict UTF-8 decode 후 canonical v1 re-encode
byte equality를 만족하는 terminal compatibility record만 read-only로 반환한다. Unknown/newer
schema, malformed UTF-8, non-canonical v1은 absent로 취급하지 않는다.
Crash qualification을 위해 control-plane fault context는 package-private로 record kind,
record identity, 해당하는 경우 operation state/revision, force boundary를 함께 전달한다.
Production 기본 callback은 no-op이며 runtime 설정이나 public bean으로 노출하지 않는다.
## 9. Publication ordering
```text
J-WRITING
-> stage stream/force
J-SEALED
-> exclusive hard-link data publish
-> data directory force
J-DATA_PUBLISHED
-> private manifest publish/force
J-MANIFEST_PUBLISHED
-> reference index publish/force
J-REFERENCE_PUBLISHED
-> terminal journal + receipt snapshot publish/force
J-PUBLISHED
-> receipt return
```
- Producer는 accepted attempt에서 최대 한 번 호출한다.
- `SEALED` 이후 retry/recovery는 staged bytes만 사용한다.
- terminal journal force 전에는 receipt를 반환하지 않는다.
- target collision, digest mismatch 또는 root identity change는 자동 overwrite하지 않는다.
- final data가 있어도 manifest/reference가 없으면 아직 terminal success가 아니다.
- staging/data shard 생성, stage force, stable no-follow read/digest, exact delete는
package-private `PayloadOperations`를 통해 `SecureDirectoryStream` 상대 연산으로 수행한다.
Portable relative primitive가 없는 hard-link와 directory force만 private-owner boundary 안에서
root/directory/file identity pre/post 검증으로 감싼다.
- hard-link 뒤 journal 갱신 전에 중단된 `SEALED + matching data` 복구는 기존 data shard를 다시
identity 검증하고 directory force한 뒤에만 `DATA_PUBLISHED`로 전이한다. 이미 존재하는 data를
overwrite-capable publication 경로에 다시 넣지 않는다.
- `WRITING` 저장 뒤 producer 또는 stage/write가 실패하면 partial stage를 exact cleanup하고
unsealed `QUARANTINED` evidence를 남긴다. 원래 producer exception은 보존하고 cleanup/control
failure는 suppressed로 연결한다. Retry 진입 시 기존 `WRITING` 또는 unsealed
`QUARANTINED`가 보이면 producer를 다시 호출하지 않고 indeterminate/quarantine으로
fail-closed한다.
## 10. Deterministic recovery
Recovery는 operation ID direct lookup으로 실행하며 startup full scan에 의존하지 않는다.
| 확인된 상태 | 조치 |
| --- | --- |
| terminal journal + matching manifest/reference/data | 저장된 receipt 복원 |
| SEALED + valid stage, data 없음 | data publication부터 재개 |
| SEALED + matching data | manifest publication부터 재개 |
| DATA_PUBLISHED + matching data | manifest publication 재개 |
| MANIFEST_PUBLISHED + matching manifest/data | reference publication 재개 |
| REFERENCE_PUBLISHED + all matching | terminal journal 완성 |
| non-terminal data/manifest/reference digest mismatch | `QUARANTINED`, integrity failure |
| `PUBLISHED` artifact/metadata/receipt mismatch | terminal journal과 artifacts를 불변 보존하고 typed integrity/indeterminate |
| required manifest/reference/data 누락 | 성공 복원 금지, fail-closed indeterminate/quarantine |
| marker/manifest/reference schema newer | 보존 후 fail-fast/quarantine |
| fingerprint conflict | typed conflict, 기존 artifact 보존 |
| root/mount identity change | indeterminate, write/recovery 중단 |
Truth priority:
```text
matching data + private manifest + reference
> terminal operation record
> non-terminal operation record
> in-memory state
```
모순이 있으면 임의 성공이나 삭제를 하지 않는다. Non-terminal operation은 기존 operation
journal을 `QUARANTINED`로 전이할 수 있다. 이미 `PUBLISHED`인 operation은 terminal
receipt snapshot을 지우거나 journal을 덮지 않고 관련 data/manifest/reference도 보존한 채 typed
integrity/indeterminate로 실패한다. 별도 immutable quarantine incident record는 후속 설계 전까지
가정하지 않는다.
Recovery verifier는 operation, incoming request, data, manifest, reference, receipt snapshot의
identity/digest/locator/count/time/guarantee를 모두 교차검증한다. Terminal receipt는 verified
manifest/reference에서 재구성한 expected receipt와 전체 equality가 확인될 때만 반환한다.
Operation record의 일부 필드만 맞거나 durability/publication guarantee, file version,
format/media/charset가 다르면 terminal success가 아니다. Crash 뒤 먼저 발견한 immutable
manifest/reference의 verified `publishedAt`은 새 clock 값으로 덮지 않고 recovery context로
재사용한다. 새 attempt에만 현재 configured maximum을 적용하고, sealed recovery artifact는
operation에 freeze된 exact byte size로 bounded inspection한다. Stage와 data가 함께 있으면
digest equality만이 아니라 stable file key가 같은 hard-link인지 확인한 뒤에만 stage를
exact-delete한다.
## 11. Compatibility
- R1 compatibility는 별도 미설정 root나 동시에 활성화된 legacy bean이 아니다. Operator가 기존
R1 root를 owner/mode/FileStore/sentinel 등 R2 attestation 조건에 맞춰 명시적으로
pre-provision한 뒤, 그 root를 R2 destination으로 전환하는 in-place read-only migration이다.
- R1과 R2 operation journal은 같은 hashed path를 사용하므로 secure relative typed schema
dispatch로 schema v1을 읽고 schema v2만 쓴다.
- R1 journal schema v1은 strict UTF-8와 canonical re-encode byte equality를 만족하는 terminal
record만 읽을 수 있어야 한다.
- R1 terminal receipt는 기존 `PROCESS_LOCAL_SYNC` 보장 그대로 복원한다.
- R1 root-level artifact도 attested root의 `SecureDirectoryStream` 상대 no-follow bounded
streaming inspection으로 journal의 byte size와 SHA-256을 확인한 뒤에만 receipt를 복원한다.
- R1 artifact를 자동으로 R2 manifest/reference로 승격하지 않는다.
- R2 writer는 journal v2만 생성한다.
- 기존 overwrite-capable legacy port는 별도 root와 opt-in을 유지하며 R2 control plane에 접근하지
않는다.
- R1과 R2 selector가 동시에 활성화되면 ambiguous composition으로 startup을 실패시킨다.
## 12. Failure semantics
- 설정/보장 mismatch: startup failure;
- destination 없음: producer 전 deterministic request failure;
- stage 이전 capacity/validation failure: not applied;
- stage/write failure: failed, partial stage는 recovery evidence가 아니면 exact cleanup하고
unsealed `QUARANTINED`로 producer replay를 차단;
- sealed 이후 filesystem timeout/IO/root identity change: indeterminate;
- non-terminal published data와 metadata 불일치: integrity/quarantine;
- terminal `PUBLISHED` data/metadata/receipt 불일치: terminal evidence 불변 보존 후 typed
integrity/indeterminate;
- journal/control record corruption: provider exception을 노출하지 않고 typed indeterminate;
- guarantee를 낮춰 성공시키는 fallback은 없다.
## 13. 테스트와 증거
### 13.1 Unit/contract
- exact destination/provider selection과 no-default;
- R1/R2 simultaneous activation rejection;
- reference grammar/check digits/forged route rejection;
- journal v2, manifest, reference canonical round-trip;
- deterministic route token collision rejection과 canonical policy/schema/format digest;
- same operation path의 strict canonical R1 read-only/v2 write-only typed dispatch;
- state revision과 fingerprint conflict;
- achieved durability value invariants.
### 13.2 Local integration
- pre-provisioned root requirement;
- owner/mode/FileStore/sentinel mismatch startup failure;
- symlink ancestor/control/data rejection;
- staging/final/control same `FileStore`;
- successful capability probe와 cleanup;
- partial final visibility 0건;
- same operation concurrency와 producer once;
- unsealed `WRITING` failure quarantine와 retry producer 0회;
- target collision no overwrite;
- non-terminal data/manifest/reference digest mismatch quarantine;
- terminal mismatch의 PUBLISHED journal/artifact 불변 보존과 typed integrity/indeterminate.
### 13.3 Crash qualification
Forked JVM helper를 사용해 다음 force boundary 직후 process를 강제 종료하고 새 JVM에서 같은
operation을 재시도한다.
```text
J-WRITING
stage force
J-SEALED
data link
data directory force
manifest force
manifest directory force
reference force
reference directory force
terminal journal force
terminal journal directory force
```
각 boundary에서 결과는 다음 중 하나여야 한다.
- producer 재실행 없이 동일 receipt 복원;
- verified sealed bytes로 publication 완성;
- typed indeterminate/quarantine.
partial final, overwrite, 다른 receipt, silent guarantee downgrade는 허용하지 않는다.
같은 attested root와 operation ID에 대해 process A가 OS operation lock을 보유하는 동안 forked
process B의 bounded non-blocking/timed acquire가 critical section에 진입하지 못하고, A의
release 또는 강제 종료 뒤 B가 획득하는지도 별도로 증명한다. 이 증거는 동일 JVM stripe 테스트로
대체하지 않는다.
### 13.4 플랫폼
- Linux/POSIX + `SecureDirectoryStream` + directory force qualification lane에서만
`FILE_AND_DIRECTORY_SYNC`을 검증한다.
- capability가 없는 일반 unit-test filesystem에서는 R1 보장만 테스트하며 R2 service test를
skip 성공으로 처리하지 않는다.
## 14. 완료 기준
이번 increment의 완료는 “Fileserver 전체가 모든 운영환경에서 R2”라는 뜻이 아니다.
완료를 주장하려면:
1. provider 기본값 없이 exact binding이 동작한다.
2. `local-persistent` startup probe가 모든 required capability를 증명한다.
3. terminal manifest/reference direct lookup이 구현된다.
4. 모든 publication force boundary의 crash test가 deterministic result를 낸다.
5. strict path/mount identity/security tests가 통과한다.
6. public path와 clean architecture gate가 통과한다.
7. R1 compatibility artifact를 R2로 자동 승격하지 않는다.
8. 문서와 receipt는 `local-persistent` qualification만 R2라고 표시한다.
후속 순서는 Phase 3 maintenance/resource limits, Phase 4 SFTP, Phase 5 shared-mounted/NFS evidence다.
## 15. 구현 및 readiness 판정
2026-07-28 구현은 다음 경계를 만족한다.
- application에는 provider/path/framework 타입이 없는 `FilePublicationPort`만 유지한다.
- adapter 내부의 canonical operation/manifest/reference model, opaque reference, provider SPI,
exact destination router는 provider-neutral control/selection boundary로 구현되었다.
- `app.fileserver.enabled`는 disabled-default이며, enable 시 destination/provider를 exact
compile한다. Unknown destination은 producer 호출 전에 실패하고 implicit local fallback은
없다.
- 같은 provider ID를 참조하는 destination은 하나의 provider/control/payload runtime을
공유한다. 서로 다른 provider ID가 같은 normalized root를 소유하면 startup에서 실패한다.
- R2 provider는 `local-persistent` 하나만 구현·qualification한다. Absolute/existing
pre-provisioned root와 owner/mode/FileStore/sentinel/path/capability attestation이 모두
성공해야 bean이 구성된다.
- operation v2, private manifest, direct reference index, ordered force publication과
deterministic recovery를 구현했다. Forked-process qualification은 각 force boundary와 OS
operation lock을 대상으로 하며, focused/module/full gate 결과와 함께 완료 증거를 판정한다.
- 기존 schema-v1 terminal record와 root-level R1 artifact는 strict UTF-8/canonical/direct
read-only compatibility다. 원래 `PROCESS_LOCAL_SYNC` receipt만 복원하며 schema-v2 rewrite,
manifest/reference 생성, `FILE_AND_DIRECTORY_SYNC` 자동 승격을 하지 않는다.
`FILE_AND_DIRECTORY_SYNC`는 attested local filesystem protocol에서 file과 관련 directory
force가 성공했다는 의미다. Physical device, volatile storage-controller cache, volume replica,
backup 또는 site 단위 power-loss protection을 주장하지 않는다. 그 보장은 Fileserver 코드가
아니라 선택한 storage/deployment의 별도 evidence가 필요하다.
다음 capability는 구현되지 않았고 setting/env/bean으로 노출하지 않는다.
- `shared-mounted`/NFS multi-client correctness와 cross-node producer fencing;
- SFTP SDK, connection/session pool, host-key/credential, remote reconciliation;
- background reconcile/reaper, managed retention/delete;
- quota reservation, backpressure, capacity admission;
- Fileserver 전용 readiness/health, metrics, tracing, audit.
따라서 이 increment의 운영 claim은 “모든 Fileserver topology가 R2”가 아니라
“strictly attested `local-persistent` profile만 R2”다.
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
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,137 @@
# Redis Cache Resilience Increment Design
**Status:** approved for implementation
**Parent:** `2026-07-26-redis-production-capability-design.md` §§15, 16, 18
## Goal
Complete one coherent production-facing cache increment on top of the current standalone R1 Redis
runtime:
1. a framework-free cache-aside policy in `application-core`;
2. bounded local single-flight and source bulkhead protection;
3. deterministic TTL jitter plus soft/hard expiry and stale lookup semantics in the Redis adapter.
This increment does not promote Redis beyond standalone cache R1. Distributed refresh leases,
generation invalidation, rate limiting, owner-safe locks, idempotency, sessions, Sentinel/Cluster,
TLS/ACL and fault qualification remain later increments.
## Architecture boundary
- `application-core` owns lookup interpretation, source-result classification, cache-aside
sequencing, stale-if-error, local coalescing and source admission policy.
- `adapter:outbound:cache-redis` owns physical TTL, envelope timestamps, deterministic jitter,
serialization and Redis command outcomes.
- The application contract contains no Redis/Lettuce/Lua/Spring type.
- Cache fallback never becomes unlimited source fallback. A miss, provider outage and waiter burst
all pass through the same bounded source path.
## Application contract
`CacheSourceLoader<K,V>` returns a typed `SourceLoadOutcome<V>`:
- `Loaded(value, sourceRevision)`;
- `AuthoritativeAbsent(reason, sourceRevision)`;
- `TransientFailure(SourceFailure)`;
- `PermanentFailure(SourceFailure)`;
- `Cancelled`.
`SourceFailure` carries a bounded code and the original cause. It never serializes the cause message
into Redis or metric tags. An unclassified thrown exception is rethrown unchanged and is never
negative-cached or converted to stale success.
`CacheResult<V>` distinguishes:
- fresh cache hit;
- source-loaded value and its cache-record outcome;
- authoritative absence and its cache-record outcome;
- stale fallback after a classified transient source failure;
- source failure;
- bounded overload/timeout rejection;
- cancellation.
`CacheAsidePolicy` is immutable and constructed once per semantic region. It contains maximum
in-flight source keys, waiter limit per key, source concurrency, admission wait, load deadline and
whether transient source failure may serve stale.
## Cache-aside state machine
1. `Hit(FRESH)` returns immediately.
2. `NegativeHit` returns immediately.
3. `Hit(STALE)` retains the value and attempts a bounded refresh.
4. `Miss`, an `IncompatibleSchema(QUARANTINE_AND_RELOAD)` carrying a usable opaque observation
token, and `Unavailable` enter the same bounded source path. `FAIL_FAST` schema results and
unobservable incompatible values are not overwritten.
5. A local single-flight elects one leader per semantic key. Waiters share the typed source outcome.
6. The leader must acquire the source bulkhead before calling the loader.
7. A miss records with `ONLY_IF_ABSENT`. A stale or quarantined observation records with
`ONLY_IF_OBSERVED`, which atomically compares the digest captured by lookup before replacing the
value. No lookup-then-delete sequence is used, so a concurrent writer is never deleted.
8. Only `AuthoritativeAbsent` records a negative entry, using the same absent/observed condition as
a positive source result.
9. `TransientFailure` may return the retained stale value when policy allows it.
10. `PermanentFailure`, unclassified exceptions and cancellation are never hidden by negative cache.
11. Entries are removed from the flight map after success or failure. In-flight keys and waiters are
bounded; waiting uses a finite deadline and preserves thread interruption.
The loader is synchronous and cancellation is cooperative. Its token exposes deadline/interruption;
the executor bounds admission and waiter time but cannot safely terminate arbitrary source code.
## Redis envelope and TTL policy
The positive envelope moves to version 2 and stores:
- source revision;
- `softExpiresAt` epoch milliseconds;
- `hardExpiresAt` epoch milliseconds;
- payload and SHA-256 integrity digest.
Negative envelopes store only the hard expiry. Lookup behavior is:
- `now < softExpiresAt`: `Hit(FRESH)`;
- `softExpiresAt <= now < hardExpiresAt`: `Hit(STALE)`;
- `now >= hardExpiresAt`: `Miss(EXPIRED)`;
- negative `now < hardExpiresAt`: `NegativeHit`;
- expired negative: `Miss(EXPIRED)`.
Version 1 becomes an explicit retired schema result. Future versions and corrupt envelopes fail
fast. Digest-valid retired/unknown envelopes carry an opaque observation token so an approved
quarantine reload can compare-and-replace the exact observation. Structurally invalid current
envelopes remain corrupt/fail-fast even when their digest is valid. Unknown envelopes remain typed
incompatibility results and are not silently treated as misses. Envelope integrity is checked
before the version byte is trusted.
The policy contains positive soft TTL, positive hard TTL, negative TTL, jitter ratio, minimum hard
TTL and maximum value bytes. Construction rejects:
- non-positive or over-30-day TTLs;
- soft TTL greater than hard TTL;
- jitter outside `0.0..0.5`;
- minimum hard TTL greater than either configured hard TTL.
- configured hard TTL plus maximum positive jitter greater than 30 days.
Jitter is deterministic from the HMAC-derived physical key and the compiled policy revision. It
uses a symmetric bounded factor. The actual positive soft/hard TTLs use the same factor so ordering
is preserved. Physical Redis TTL equals the encoded hard expiry duration in the same `SET`.
Negative TTL is jittered independently and also respects the hard minimum.
## Evidence
Tests must prove:
- fresh/negative hits do not call the source;
- concurrent same-key misses call the loader once;
- in-flight-key, waiter, bulkhead and deadline bounds;
- completion/failure cleanup and exception/interruption behavior;
- only authoritative absence is negative-cached;
- stale is served only after a classified transient failure;
- fresh/stale/expired boundaries with an injected `Clock`;
- deterministic bounded jitter and hard minimum;
- version 1/future/corrupt envelope behavior;
- Redis physical TTL matches the encoded hard expiry.
- observed replace reads only the trailing digest and never overwrites a concurrent writer;
- the exact 16MiB opt-in payload is accepted while 16MiB+1 is rejected before dispatch;
- mutation interruption restores the thread flag and maps to indeterminate certainty.
Focused checks run before the repository-wide architecture, dependency, env and public-path gates.
@@ -0,0 +1,144 @@
# Redis Distributed Rate-Limit Increment Design
**Status:** implemented as standalone R1
**Parent:** `2026-07-26-redis-production-capability-design.md` §§1921
## Goal and readiness
Provide three selectable, bounded distributed rate-limit algorithms:
- fixed window;
- sliding-window counter;
- token bucket.
This increment is a standalone Redis R1 provider. It does not claim R2 topology/security/failover
qualification and does not implement sliding log, GCRA, leaky bucket, evaluation dedup, hierarchical
all-or-nothing policies or local emergency fallback.
## Ownership
- `shared-contract` owns the edge-enforcement semantic port and provider-neutral request, policy,
decision and failure outcomes. Business quotas remain application use-case policy and do not use
this port.
- `adapter:outbound:cache-redis` owns Redis keys, atomic Lua programs, structured reply parsing,
failure certainty and the provider implementation.
- `app-bootstrap` owns the explicit provider/policy selection.
- The existing inbound-web local limiter remains a compatibility path until a separate inbound
migration. Its types do not cross into the Redis provider.
The rate-limit runtime does not reuse `app.cache.redis`, the cache connection or cache fail-open
decorators. Coordination has different failure and deployment semantics.
## Shared semantic contract
`EdgeRateLimitPort.evaluate(RateLimitRequest)` accepts:
- bounded `policyId`;
- already pseudonymized/bounded `subjectDigest`;
- positive request cost;
- optional evaluation ID (rejected in this non-deduplicating revision);
- finite caller deadline.
`RateLimitPolicy` freezes policy ID/revision, one algorithm-specific parameter subtype, maximum
cost, cleanup grace, maximum clock regression and `FAIL_CLOSED`. Construction rejects mismatched
algorithm/parameters, arithmetic outside Lua's exact integer range and unsupported failure/dedup
claims.
The outcome is one of:
- `Evaluated(decision)`;
- `Unavailable(policyId, retryAfter, category)` for known pre-send/no-mutation failures and unsafe
server clock;
- `Indeterminate(policyId, retryAfter)` for post-dispatch uncertain mutation;
- `Incompatible(policyId, category)` for state/program/reply mismatch.
`RateLimitDecision` includes allow/deny, limit, remaining, retry-after, reset-at, policy ID/revision,
`GLOBAL_REDIS` source and certainty. Fixed window and token bucket are `CERTAIN`;
sliding-window counter is `APPROXIMATE_ALGORITHM`.
## Atomic programs
Each v1 program uses one versioned hash key and calls Redis `TIME` exactly once.
```text
rate-fixed-window-v1.lua
rate-sliding-counter-v1.lua
rate-token-bucket-v1.lua
```
Every program returns exactly seven bounded scalar fields:
```text
status, serverNowMillis, effectiveNowMillis,
limit, remaining, retryAfterMillis, resetAtMillis
```
Statuses are `ALLOWED`, `DENIED`, `CLOCK_UNSAFE`, `STATE_INCOMPATIBLE`, `INVALID`.
Unknown arity/status/numeric syntax/range is a compatibility failure, never allow/fail-open.
Common rules:
- Redis server time drives enforcement;
- small backward movement clamps to stored `lastObservedMillis`;
- regression beyond policy threshold returns `CLOCK_UNSAFE` without consuming state;
- policy/schema/algorithm mismatch returns `STATE_INCOMPATIBLE`;
- denied requests do not consume quota;
- state receives a finite TTL;
- all arithmetic stays within `2^53-1`;
- raw principal/IP/API-key/route never appears in the physical key.
The existing scalar Lua executor stays intact. A structured program path adds bounded MULTI reply
support and uses `EVALSHA`, falling back to the exact compiled source only on `NOSCRIPT`.
## Algorithm rules
Fixed window stores window ID and consumed count. Allow increments only when
`consumed + cost <= limit`; retry/reset points to the current window end.
Sliding counter stores previous/current window IDs and counts, using scale `1_000_000` and
conservative ceiling weight. It reports approximate certainty and a bounded conservative retry.
Token bucket stores scaled tokens, last refill time and the sub-token division remainder. Refill is
therefore independent of evaluation frequency, uses quotient/remainder arithmetic without an
unsafe `numerator + denominator - 1` intermediate, and saturates at capacity. Denial does not
subtract tokens; retry and full-reset use integer ceiling.
## Physical key
The existing canonical builder is reused with:
```text
capability=rate
region=<policyId>
kind=state
digest(policyId, policyRevision, algorithm, subjectDigest)
```
Policy revision appears in both digest input and stored state. A policy revision therefore rolls to
a new key while old state expires naturally.
## Runtime and composition
`app.rate-limit` is disabled by default. Enabling requires:
- `provider=redis`;
- one default policy and an exact policy definition;
- a dedicated Redis coordination endpoint and HMAC secret;
- finite command/admission bounds.
Only `role=coordination` and `failure-policy=fail-closed` are accepted in v1. Disabled mode creates
no connection, thread or semantic port. Cache Redis settings/beans are never an implicit fallback.
## Evidence
Unit tests cover contract bounds, policy arithmetic, key privacy/revision, structured reply
validation, `NOSCRIPT`, boundary vectors, denial-no-consume, clock regression, pre/post-dispatch
failure certainty and disabled composition. The explicit Redis 7.4 service lane executes all three
programs, exact-boundary admission after a denied non-consuming request, excessive clock-regression
state immutability, `TYPE` response normalization, token refill-remainder carry, malformed hash-state
classification, cache `NX`, and observation-token compare-and-replace. Redis 7.4 is the minimum
version declared by the program manifests until a lower-version service lane exists. The caller
deadline is an admission precheck against the fixed command timeout; R1 does not claim per-command
dynamic timeout or hard cancellation after dispatch. Missing TLS/ACL, Sentinel/Cluster, failover and
persistence/eviction evidence keeps the provider at R1.
@@ -0,0 +1,160 @@
# Release Hygiene Refactoring Design
**Date:** 2026-08-01
**Status:** approved by the user's instruction to apply the preceding review
**Scope:** release-blocking architecture test, Gradle wrapper supply-chain integrity, Docker build configuration inputs, SpotBugs analysis completeness, and the observed Gradle 10 deprecation
## Context
The repository-wide review found that the 19-leaf Clean Architecture dependency model is healthy,
but the release surface is not green:
- `:app-bootstrap:sampleOffTest` fails because a whole-composition Object Storage ArchUnit rule is
evaluated on the intentionally sample-free classpath with `allowEmptyShould(false)`.
- the two Dockerfiles run Gradle before copying configuration-time registry inputs, while the root
build also requires a Git checkout during configuration even though the Docker context excludes
`.git`;
- `gradle-wrapper.properties` selects Gradle 9.0.0 while the checked-in wrapper JAR is from another
official Gradle release, and the distribution checksum is absent;
- clean SpotBugs analysis reports missing Spring Session, Micrometer Context Propagation, and
protobuf classes;
- a root task calls `Task.project` during execution, which is deprecated and scheduled to fail in
Gradle 10.
This design deliberately closes those release-hygiene defects before changing idempotency, outbox,
security, or sample data behavior. Each later subsystem gets a separate design and plan so that a
reviewer can accept or revert it independently.
## Considered Approaches
### Approach A: weaken the existing global gates
Set ArchUnit rules to allow empty matches, ignore SpotBugs missing-class messages, and make Docker
configuration registries optional. This is the smallest diff, but it makes the architecture and
static-analysis gates less trustworthy. Rejected.
### Approach B: patch each symptom in place
Condition the ArchUnit rule on a sample flag, copy only the two currently missing registry files,
and add the three currently missing SpotBugs JARs manually. This would pass today's cases but would
recur whenever another leaf, registry, source set, or dependency is added. Rejected because it
duplicates ownership knowledge.
### Approach C: align ownership and derive inputs from the owning model
Move the leaf-specific architecture rule to the Object Storage leaf, keep root tests responsible
for cross-leaf registration, treat `config/**` as a declared Docker configuration input, move Git
evidence checks to the evidence task execution phase, align the wrapper artifacts to one version,
and derive SpotBugs auxiliary inputs from each analyzed source set's runtime classpath. Selected.
## Architecture Test Ownership
`adapter-outbound-objectstorage` owns rules about the public types of its production adapter methods.
The rule moves out of `app-bootstrap` and runs in the Object Storage module's normal test suite.
It remains strict: the Object Storage module must contain matching production classes and the rule
must not globally allow an empty `should` clause.
`app-bootstrap` continues to own cross-module rules. Its sample-off suite verifies that production
composition works without `sample-portfolio`; it does not require sample-only leaves to be present.
The existing module registry and dependency verification remain the SSOT for leaf coverage.
## Gradle Wrapper Integrity
Gradle 9.0.0 remains the selected version for this refactoring. The wrapper scripts, properties, and
JAR are regenerated from Gradle 9.0.0 in a trusted environment. The official 9.0.0 binary
distribution SHA-256 is recorded as:
```text
8fad3d78296ca518113f3d29016617c7f9367dc005f932bd9d93bf45ba46072b
```
The wrapper properties are one exact ordered eight-line byte contract, preventing Java Properties
duplicate-key, separator, escape, and continuation semantics from overriding the reviewed values.
The complete six-file workflow path set and every workflow's SHA-256 are embedded as a reviewed
byte lock in the verifier. This is the primary completeness boundary: YAML has aliases, encoded
keys, duplicate-key overrides, custom shells, and other equivalent representations that a partial
Bash parser cannot safely model. Any workflow addition, removal, rename, symlink replacement, or
byte change fails until the complete workflow diff is intentionally reviewed and the sorted lock
is refreshed in the same change.
The restricted block-style workflow grammar remains defense in depth and supplies actionable
diagnostics for ordinary drift. Every Gradle-running job uses an unconditional validation step
with a stable ID and the action pinned by commit SHA. Checkout and validation precede every Gradle
invocation, not only the first; a cleanup/sanitizer step that intentionally uses `always()` also
requires the validation step's successful outcome. This is consistent with the repository's
existing pinned `actions/setup-java` policy and prevents wrapper failure from being bypassed by
step conditions.
## Docker Configuration Contract
Both Docker build dependency-cache stages preserve the repository layout with `WORKDIR /build/src`
and copy the complete `config/**` tree before invoking Gradle. The parent `/build` is therefore the
repository root expected by registry `source_path: src/**` entries. This is intentional: Gradle
configuration registries and their repository-relative path base are build inputs, while the
registry's exact internal file list may evolve.
Git revision validation no longer runs unconditionally while the build script is being configured.
A root-owned resolver is invoked once from each root evidence action or leaf evidence test's
root-suite completion action; eager scalar evidence properties are removed. Only evidence-producing
tasks resolve the checkout revision during their execution. Docker builds provide
`-PgitRevision=<40 lowercase hex>` and do not copy `.git` into the image context.
The boot JAR path is obtained from Gradle's archive output contract rather than selecting the first
filesystem match. The final images retain the existing digest-pinned base image, non-root user,
read-only root filesystem, and JRE-only runtime.
## SpotBugs and Gradle 10 Compatibility
Every SpotBugs task analyzes a named source set and receives that source set's runtime classpath as
its auxiliary analysis classpath, excluding its own compiled output. Custom test source sets are
covered by the same rule. No production dependency scope is widened merely to silence SpotBugs.
Missing-analysis-class output is treated as a gate failure. The clean gate must produce zero
`classes needed for analysis were missing` messages.
The observed Gradle 10 deprecation is removed by capturing the application-core project during
configuration instead of calling `Task.project` from the task action. The dependency-purity gate
still traverses that project's configurations during execution, so it explicitly opts out of the
configuration cache rather than claiming serializable declared inputs it does not have.
## Error Handling and Failure Semantics
- sample-off fails only for a real production composition or architecture violation;
- an empty Object Storage rule in its owning module is a test failure;
- a wrapper JAR or distribution checksum mismatch fails before Gradle build logic executes in CI;
- missing Docker configuration input fails with a named build-contract test rather than an opaque
settings error;
- invalid or absent `gitRevision` fails only an evidence task that requires it;
- SpotBugs missing classes fail static analysis instead of producing a successful partial report.
## Verification Design
The implementation follows red-green-refactor. Each behavior has a regression test or executable
contract that fails before the production/configuration change:
1. reproduce `sampleOffTest` failure, then add an owner-module architecture test and remove the
misplaced global rule;
2. add wrapper property and workflow contract assertions before regenerating the wrapper;
3. extend Docker contract tests so a cache-stage Gradle configuration fixture requires `config/**`
and accepts an attested `gitRevision` without `.git`;
4. add Gradle build-contract coverage for source-set-derived SpotBugs auxiliary classpaths, the
removed execution-time `Task.project` access, and the explicit configuration-cache opt-out;
5. run focused gates, then the clean repository-wide gate and gate-matrix script.
## Non-Goals
- no dependency version upgrade beyond aligning the wrapper to the already selected Gradle 9.0.0;
- no business/domain behavior changes;
- no idempotency, outbox, Poster publication, security, DTO, or database migration changes;
- no broad extraction of the 3,768-line root build script in this phase;
- no agent-created branch, stage, commit, amend, or push.
## Decision Summary
- Object Storage-specific ArchUnit rules live with Object Storage.
- Root architecture rules remain strict and cross-module only.
- Gradle stays at 9.0.0 and gains exact wrapper/distribution validation.
- Docker copies `config/**`; Git evidence is execution-scoped and supplied by `gitRevision`.
- SpotBugs uses source-set runtime classpaths and fails on missing analysis classes.
- The dependency-purity task avoids execution-time `Task.project` access and truthfully declares
its configuration-cache incompatibility while it still inspects project configurations.
@@ -0,0 +1,74 @@
# Client-Safe Error Boundary Design
**Date:** 2026-08-02
**Status:** approved by the user's instruction to apply the detailed P1/P2 review sequentially
**Scope:** HTTP error envelopes in `adapter:inbound:web` and the `sample-portfolio` domain advice
## Context
Several handlers pass `Exception#getMessage()`, rejected request values, or a raw request URL into
the public error envelope. Those values are not a stable API contract and can contain identifiers,
tokens, uploaded values, configuration details, or internal diagnostics. Persistence and outbound
dependency failures already use fixed client-safe messages; the rest of the HTTP boundary must
follow the same rule.
## Decision
The inbound adapter owns a message allowlist keyed by stable error code. Handlers may expose only:
- stable `code`, `category`, HTTP status, and `retryable` from `ApiErrorCode`;
- fixed, code-specific client messages;
- bounded structural details such as field name, validation reason code, expected Java type,
supported HTTP methods, or supported media types.
They must not expose exception messages, rejected values, raw request URLs, adapter/configuration
diagnostics, opaque cursors, authentication diagnostics, resource identifiers, or duplicate domain
values. Bean Validation interpolated/default messages are also discarded because custom templates
can include the validated value. Validation details contain only normalized server-owned property
names plus allowlisted reason codes and fixed messages; collection/map keys and indices are removed.
`ClientSafeErrorMessages` is extended for skeleton-wide operational codes. The sample keeps its
domain wording in a separate package-private `PortfolioClientSafeErrorMessages`, preserving the
rule that production modules do not know sample business concepts.
## Public Messages
Representative mappings are fixed as follows:
- `MAPPING_FAILED``Request data could not be mapped`;
- `BAD_PARAMETER``Request parameter is invalid`;
- `INVALID_TOKEN``Authentication token is invalid`;
- `UNAUTHENTICATED``Authentication is required`;
- authorization denials → `Access is denied`;
- `PRECONDITION_FAILED``Resource state changed; refresh and retry`;
- page/cursor failures → generic corrective text, with safe field/reason details retained;
- `ADAPTER_DISABLED` and internal classifications → `Internal server error`;
- domain not-found/conflict/invariant codes → fixed noun-level text with no ID/title value.
Transport overrides use fixed wording and retain only safe protocol metadata. For example, 405
still emits `Allow`, while both controller-route (`NoHandlerFoundException`) and static-resource
(`NoResourceFoundException`) 404s use the same envelope without echoing the request URL.
## Testing
Tests inject conspicuous secret sentinels into exception messages, rejected values, URLs, tokens,
IDs, and duplicate titles. Every resulting response must preserve its status/code/category while
excluding the sentinel from both `error.message` and `error.details`.
Validation tests additionally place sentinels in interpolated/default messages and iterable
keys/indices. A real MockMvc resource-resolution request verifies the Spring 7
`NoResourceFoundException` path rather than calling the advice method directly.
The focused module suites remain the primary verification:
- `:adapter:inbound:web:test` for operational and transport handlers;
- `:sample-portfolio:test` for domain advice and sample wire behavior;
- `verifyCleanArchitectureDependencies` for dependency direction.
## Non-Goals
- no change to error codes, categories, statuses, or retryability;
- no suppression of server-side logs or tracing in this batch;
- no application/domain dependency on HTTP response types;
- no generic exception-message sanitizer based on regexes or truncation;
- no staging, commit, amend, or push by an agent.
@@ -0,0 +1,118 @@
# Conditional Inbound Transport Boundary Design
**Date:** 2026-08-02
**Status:** approved by the user's instruction to apply the detailed P1/P2 review sequentially
**Scope:** the opt-in GraphQL, gRPC, and WebSocket leaf modules and their release evidence
## Context
The three leaves are registered and tested independently, but neither `app-bootstrap` nor
`sample-portfolio` has a production dependency on them. That omission is intentional: adding a
classpath edge today would activate GraphQL, start a plaintext/reflection-enabled gRPC server by
default, and unconditionally expose a wildcard-origin STOMP broker that serializes arbitrary domain
events. The leaf documentation nevertheless describes sample contributions that do not exist, and
the ordinary root `check` can become `NO-SOURCE` without a transport-specific positive-count and
zero-skip qualification gate.
P1 therefore makes opt-in status executable and makes accidental activation fail closed. It does
not add these leaves to the default runtime or claim the P2 production baselines.
## Runtime Membership SSOT
Every entry in `config/architecture/modules.json` gains an exact `runtime_memberships` array whose
values are limited to the two composition roots: `app-bootstrap` and `sample-portfolio`.
- A composition root includes itself in its membership.
- Direct production `api`/`implementation`/`compileOnly`/`runtimeOnly` project dependencies must
equal the registry members for that root, excluding the root itself.
- An empty array means the leaf is built and architecture-checked but absent from both shipped
runtime graphs. GraphQL, gRPC, WebSocket, and Mongo remain in this state.
- Test fixtures and custom qualification configurations do not change production membership.
Settings validation is fail-closed for missing, duplicate, or unknown membership names. A Gradle
verification task compares the registry to both composition roots and is part of `check`.
## Explicit Qualification Composition
`app-bootstrap` owns a `conditionalTransportTest` source set whose classpath explicitly includes
the three opt-in leaves. It proves that the opt-in artifacts resolve together while the registry
still declares them absent from both default runtime graphs. It is evidence composition, not a new
production dependency edge.
The root registers exact qualification `Test` tasks for GraphQL, gRPC, and WebSocket. Each task:
- names required test classes rather than broad discovery;
- fails on no match or no discovery;
- always reruns in UTC;
- fails if the root suite reports any skipped test.
An aggregate `conditionalTransportQualification` task depends on the composition contract and all
three exact lanes. CI invokes it explicitly from the existing release-blocking quality job, and the
gate matrix records the task.
## gRPC P1 Boundary
gRPC activation becomes explicit and local-only until a later TLS/mTLS design exists:
- `enabled=false` and `reflectionEnabled=false` are defaults; missing properties create no runner,
health manager, reflection service, or listener.
- The current insecure credential mode requires an explicit local-development override and a
loopback bind address. Non-loopback insecure bind fails startup.
- Feature services require a caller-supplied authentication policy/interceptor. Missing or invalid
metadata returns stable `UNAUTHENTICATED`; valid metadata reaches the service.
- Health remains a local lifecycle probe; reflection is a separate explicit flag.
- The error interceptor wraps `ServerCall.close`, so handler throws, listener throws, ordinary
`responseObserver.onError`, and raw `StatusRuntimeException` all pass the same sanitizer.
Recognized `ApiErrorCarrier` causes produce stable code/category trailers; unrecognized status
descriptions become fixed `INTERNAL_ERROR` with no raw diagnostic.
A real ephemeral Netty unary service verifies authentication, reflection-off, all error paths, and
sentinel redaction. TLS/mTLS, external bind, deadlines, streaming, and protobuf compatibility are
P2 and remain unclaimed.
## GraphQL P1 Boundary
GraphQL remains classpath-selected: its absence from the default runtime is the disable mechanism,
and the qualification classpath is the explicit opt-in mechanism. The wire lane starts a real
random-port MVC server and crosses HTTP JSON, Spring Security, and CORS.
It verifies unauthenticated rejection, authenticated health success, allowed/disallowed origins,
GraphiQL disabled, production-style introspection disabled, stable carrier errors, unknown errors,
and absence of distinct secret sentinels from the complete response body. The existing resolver is
changed only if a failing wire contract proves unsafe behavior.
Feature schema/resolvers, field authorization, depth/cost, persisted queries, DataLoader, schema
compatibility, and subscriptions remain P2.
## WebSocket P1 Boundary
WebSocket gains `ca-skeleton.websocket.enabled=false`; both configuration and broadcaster are
conditional. Enabled settings reject wildcard/blank origins and invalid endpoint/destination
shapes.
The inbound channel requires an authenticated handshake principal, permits subscription only to
the configured server topic, permits authenticated application sends under `/app/**`, and rejects
client sends to `/topic/**`. A custom STOMP error handler emits only a fixed client-safe code.
The broadcaster no longer serializes arbitrary `@DomainEvent` objects. It consults an explicit
projection allowlist; an event without exactly one projection is not sent. Projection output is a
bounded primitive map, not the domain object graph.
A real random-port WebSocket/STOMP lane verifies disabled absence, origin/auth/connect/subscribe,
server push, broker-send rejection, error redaction, and no projection/no broadcast. The simple
broker remains local/R1 only; broker relay, cross-node durability, replay, backpressure, and a
domain-specific versioned projection catalog remain P2.
## Documentation Truthfulness
Leaf READMEs and CLAUDE files describe only code that exists. Sample GraphQL schemas, gRPC services,
and WebSocket publishers are future adoption examples, not current runtime features. Each document
states the activation switch, exact P1 evidence, and unimplemented P2 limits.
## Non-Goals
- adding any of the three leaves to a shipped default runtime;
- adding a production project dependency edge outside the registry;
- claiming production readiness from local loopback/simple-broker tests;
- implementing sample feature APIs or domain payloads;
- staging, committing, amending, or pushing changes.
@@ -0,0 +1,79 @@
# P2 Verification Governance Refactoring Design
## Goal
Remove the remaining fail-open verification paths without changing production behavior or adding
unadopted runtime capabilities. P2 strengthens qualification tasks, tracked contract resources,
CI parser evidence, JSON Schema conformance, registry ownership, and bounded documentation debt.
## Scope and sequence
1. Move strict qualification `Test` registration to each owner leaf through one shared convention.
2. Resolve tracked repository contract resources from an explicit repository root and fail when
tracked files or directories are absent.
3. Exercise the real gate-matrix shell validator through isolated mutation fixtures.
4. Validate every Redis program manifest with the committed Draft 2020-12 schema.
5. Make the tracked registry set explicit, resolve every `required_test` identifier, and govern
temporary runbook stubs with owners and expiry dates.
6. Apply bounded P2 cleanup: module-doc link coverage, migration-neutral gate labels, and
deterministic outbound HTTP timeout tests.
Each item is independently reviewable. A later item may reuse infrastructure from an earlier item,
but no batch may weaken an existing check while waiting for a subsequent batch.
## Qualification convention
The owner project applies `gradle/strict-qualification-test.gradle` and registers its own exact
qualification tasks. The root project only aggregates absolute task paths and validates resulting
JUnit XML.
Every strict qualification task must:
- name at least one required FQCN;
- depend on compilation and fail before test execution when any required class file is absent;
- use exact JUnit filters with no-match and no-discovery failures enabled;
- force fresh execution in UTC and emit JUnit XML;
- reject skipped tests and require a positive, failure-free XML count.
This applies to conditional transports, Messaging evidence lanes, object-storage release lanes,
the Poster migration lane, and the app-bootstrap conditional-composition proof. Ordinary optional
or quarantine tests are deliberately excluded.
## Repository contract resources
`app-bootstrap` injects `ca.repository.root` into contract tests. A package-private resolver
normalizes the root, rejects traversal, and exposes `requireTrackedFile` and
`requireTrackedDirectory`. Missing tracked resources are assertion failures, never assumptions.
Assumptions remain valid only for truly optional external infrastructure.
## CI parser evidence
The gate-matrix validator accepts an optional repository-root argument. Contract tests construct a
minimal temporary repository fixture and invoke the actual shell script. Mutations for deceptive
step names, execution-suppressing flags, missing or duplicated gates, and unregistered tasks must
produce non-zero exits with stable diagnostics. Java must not contain a second parser.
## Schema and registry governance
- Redis manifests are validated by a Draft 2020-12 implementation in addition to existing catalog
cross-checks.
- A registry catalog has an exact one-to-one relationship with tracked `docs/registries/*.yaml`.
- Stable `required_test` IDs resolve through a tracked catalog to a single owner Gradle path and
source test/method. Unknown, duplicate, and dangling mappings fail.
- Temporary runbook stubs are listed in tracked debt data with owner, issue, start, and sunset.
Missing or expired debt entries fail.
## Non-goals
- No GraphQL feature schema, cost/depth policy, gRPC TLS/streaming, WebSocket relay, or other
production capability is introduced.
- No lockfile consolidation, version-catalog migration, JVM test-suite migration, or broad module
boundary change is included.
- Root Gradle capability extraction and a typed settings/build registry model remain separate
refactors unless their benefit can be proven without expanding this verification change.
## Verification
Each batch starts with a focused failing contract and finishes with its owner `check`. Final
verification runs root `test`, `check`, architecture/dependency/runtime membership gates, CI shell
validators, dependency locks, public-path/env gates, and `git diff --check`.
@@ -0,0 +1,79 @@
# Redis Session HTTP Boundary Design
**Date:** 2026-08-02
**Status:** approved by the user's instruction to apply the reviewed P1/P2 work sequentially
**Scope:** composition of inbound browser-session security with the outbound versioned Redis session repository
## Context
Inbound-web unit contracts prove CSRF, fixation, hardened cookie settings, and primitive security
snapshot behavior with `MockHttpSession`/in-memory repositories. Cache-redis contracts prove the
versioned session repository and Lua semantics against Redis. No test currently crosses the actual
Spring Session filter, production SecurityFilterChain, real Redis, and a second application context.
Putting this test in inbound-web would require a forbidden dependency on the outbound Redis leaf.
The composition root already depends on both leaves and owns the `redisCompositionTest` source set,
so app-bootstrap is the correct boundary owner.
## Decision
Add a tagged `redis-session-http` integration contract under app-bootstrap's existing
`redisCompositionTest` source set. Ordinary `redisCompositionTest` excludes the tag. A new explicit
`redisSessionHttpIntegrationTest` task includes only that tag, fails on no discovery or any skip,
always reruns, pins UTC, and passes the checked-in Redis image registry path.
The task is deliberately not attached to ordinary local `check`, because it requires Docker. It is
added to the existing release-blocking `redis-standalone` CI job, which is the Docker-capable Redis
lane. Docker availability and container startup are attempted directly; no condition, assumption,
or environment flag may convert absence into a skip.
The test loads `redis.approved.image` from `src/gradle/redis-test-images.properties` and rejects an
unpinned reference. It creates an ephemeral CA/server certificate and a named, least-privilege ACL
user, then connects with TLS, full hostname verification, and explicit CA trust. A
runtime-generated Redis password and 32-byte HMAC are supplied through caller-owned versioned
material; no secret value is checked in, passed on the Redis command line, or logged. Missing
Docker or OpenSSL is a hard failure, not a skip.
The custom source set needs the Spring Session API at compile time. App-bootstrap therefore adds
`spring-session-core` only to `redisCompositionTestImplementation`; the existing version is reused
and the lockfile records the new custom compile configuration without changing a dependency
version.
## HTTP/Session Contract
1. A state-changing request without CSRF is 403.
2. Accessing the CSRF endpoint emits the configured Secure, non-HttpOnly CSRF cookie.
3. Login with matching cookie/header creates only the bounded primitive authentication snapshot.
4. The session cookie is host-only, Secure, HttpOnly, SameSite=Lax, path `/`, and session-scoped.
5. After the first web context closes, a second independent context restores `/whoami` from the
same cookie through real Redis.
6. Logout force-revokes/tombstones the session; the old cookie is unauthenticated and a previously
loaded stale session object cannot save over the tombstone.
7. If Redis becomes unavailable during session lookup, the request fails closed before the
protected controller and the surfaced exception graph contains only the repository's fixed
availability message, not endpoint/password/session material.
The RED run exposed two production composition gaps which are part of this boundary:
- the primitive security-context repository must wrap the response and persist before response
commit, otherwise a successful response can commit before the first session is created;
- the API security chain disables Spring Security's request cache, otherwise an unauthenticated
request stores a `DefaultSavedRequest` framework graph that the primitive session codec correctly
rejects.
## Architecture
- Inbound-web remains provider-neutral and has no outbound dependency.
- Cache-redis keeps Redis keys, Lua, codec, HMAC, and tombstone policy private.
- App-bootstrap assembles both adapters only for a cross-module composition contract.
- No production dependency edge or dependency version changes; only a custom-test compile
configuration is added to the existing lock entry.
## Non-Goals
- Redis Sentinel/Cluster sessions (production activation explicitly rejects them today);
- browser-engine proof of SameSite behavior;
- credential/certificate rotation qualification (the fixture still uses mandatory TLS, full
hostname verification, explicit trust, and a named ACL user);
- attaching Docker work to ordinary `check`;
- staging, commit, amend, or push by an agent.
@@ -0,0 +1,84 @@
# Verification Purity Refactoring Design
**Date:** 2026-08-02
**Status:** approved by the user's instruction to apply the P1/P2 review sequentially
**Scope:** stale traceable JAR verification/cleanup and public-path snapshot verification/update
## Context
Two root Gradle verification paths currently mutate files while they are expected to be safe gates:
- every `Jar` task deletes stale traceable archives in `doFirst`, and
`verifyNoStaleTraceableJars` depends on `cleanStaleTraceableJars`;
- `verifyPublicPathSnapshot` creates a missing snapshot and updates drift when
`-PapprovePublicPathChange` is supplied.
That makes `check` capable of hiding the state it is meant to detect. This batch restores the
standard contract: verification observes and fails, while explicitly named maintenance tasks own
writes.
## Considered Approaches
### Keep the root build logic in place and inspect source text in tests
This is the smallest diff, but a source assertion cannot prove task side effects. Rejected.
### Invoke the entire repository build from a copied checkout
This tests the actual root build but requires copying all 19 leaves and resolving every root plugin
for two small contracts. It is slow and couples the tests to unrelated configuration. Rejected.
### Extract only the two task concerns into applied Gradle scripts and exercise them with TestKit
Selected. The production root applies the same scripts that an isolated functional fixture uses.
The fixture observes exit status and filesystem state, so it proves behavior rather than source
shape. This is a bounded extraction required for testability, not the broad P2 root-build rewrite.
## Archive Hygiene Contract
`gradle/archive-hygiene.gradle` owns stale traceable archive discovery and the two root tasks:
- `verifyNoStaleTraceableJars` reports every stale archive and fails without deleting anything;
- `cleanStaleTraceableJars` deletes only names matching the traceable archive pattern for a known
`Jar` task and never deletes the current archive;
- normal `jar`/`bootJar` execution never performs cleanup.
The existing traceable version naming and manifest metadata remain unchanged.
## Public-Path Snapshot Contract
`gradle/public-path-snapshot.gradle` owns canonicalization and two root tasks:
- `verifyPublicPathSnapshot` fails when the env file or committed snapshot is missing, when content
drifts, or when the update-only approval property is passed to the verifier. It never creates
directories or writes files;
- `updatePublicPathSnapshot` requires `-PapprovePublicPathChange` and writes the canonical snapshot.
A clean-worktree requirement is intentionally not used: the normal update workflow necessarily has
an intentional `.env` change. Explicit task naming, the approval property, and the resulting diff
are the review boundary.
The canonical header names `updatePublicPathSnapshot`, so documentation and the committed snapshot
do not instruct users to mutate through a verification task.
## Testing
`BuildVerificationPurityContractTest` runs from an isolated `functionalTest` source set using Gradle
TestKit against temporary projects that apply the production scripts directly. Keeping TestKit off
the ordinary `testRuntimeClasspath` prevents Gradle's SLF4J provider from replacing Logback during
Spring tests. It proves:
1. a normal `jar` leaves a matching stale archive untouched;
2. verification fails and preserves the stale archive;
3. explicit cleanup deletes the stale archive but preserves the current archive;
4. missing/drifted public-path snapshots cause read-only failure;
5. the verifier rejects the update approval property;
6. only the explicit updater with approval creates or changes the snapshot.
## Non-Goals
- no change to archive naming, versions, manifests, production dependency versions, or project edges;
- only the new isolated functional-test configurations are added to `app-bootstrap/gradle.lockfile`;
- no public-path allow-list value change;
- no broad root Gradle convention-plugin migration;
- no staging, commit, amend, or push by an agent.
@@ -0,0 +1,448 @@
# Warning-Zero Build Refactoring Design
**Date:** 2026-08-02
**Status:** Approved design, pending written-spec review
**Scope:** Java compilation, Error Prone, Checkstyle, SpotBugs, test JVM diagnostics, expected-negative
shell-contract output, and intentional legacy/architecture-test compatibility seams.
## Goal
Make the standard repository build both functionally green and warning-clean. A successful build
must no longer conceal compiler warnings, test-source SpotBugs findings, ignored Checkstyle
findings, deprecated third-party API calls, or expected-negative subprocess diagnostics that look
like real failures.
The final local proof is a fresh `./gradlew clean build --warning-mode=all --no-daemon
--console=plain` with:
- exit code zero;
- zero compiler/Error Prone warnings;
- zero Checkstyle and SpotBugs findings in every executed source set;
- zero `SpotBugs ended with exit code 1` messages;
- zero OpenJDK CDS warnings from test JVMs;
- no successful Redis lab contract printing its expected-negative child diagnostics;
- only the five currently intentional optional-adapter/TestKit skips, with no qualification lane
silently skipped.
## Baseline Evidence
The fresh pre-change command completed successfully in 20 minutes 26 seconds with 283 of 283 tasks
executed. Success did not mean warning-clean:
- 123 compiler warning diagnostics across 19 warning rules (122 distinct file-line/rule
coordinates because one line emits two separate removal diagnostics);
- one test-source SpotBugs `DMI_RANDOM_USED_ONLY_ONCE` finding;
- ten OpenJDK CDS warning lines from Mockito-using test JVMs;
- 82 `redis-lab:` expected-negative stderr lines;
- five intentional skipped tests;
- no test failure, compiler error, Checkstyle finding, SpotBugs analysis error, or missing analysis
class.
The Gradle Problems report is an informational index over compiler diagnostics, not a separate
defect. It must become empty as a consequence of removing the underlying warnings; it must not be
hidden.
### Warning inventory traceability
| Rule | Diagnostic instances | Required resolution |
| --- | ---: | --- |
| `removal` | 46 | Exact legacy lifecycle/suppression policy in section 4 |
| `MissingOverride` | 16 | Add annotations to the implementing test fakes in section 2 |
| `StringCaseLocaleUsage` | 10 | `Locale.ROOT` behavior fixes and test cleanup in sections 12 |
| `SameNameButDifferent` | 9 | Qualify the two Redis nested enum types in section 2 |
| `DefaultCharset` | 9 | Explicit UTF-8 test data in sections 12 |
| `ArrayRecordComponent` | 7 | Exact record policies and copy regressions in section 2 |
| `CanonicalDuration` | 5 | `Duration.ofDays(3)` in section 2 |
| `StringSplitter` | 4 | ETag scanner plus three grammar-specific test fixes in sections 12 |
| `EmptyCatch` | 4 | Cleanup failure propagation in section 1 |
| `StringConcatToTextBlock` | 2 | Byte-identical text blocks in section 2 |
| `InvalidBlockTag` | 2 | Inline-code annotation names in section 2 |
| `BigDecimalLiteralDouble` | 2 | Method-only intentional-fixture suppressions in section 5 |
| `TypeParameterUnusedInFormals` | 1 | Spring Session method-only suppression in section 2 |
| `ThreadLocalUsage` | 1 | Instance-isolation regression and field-only suppression in section 2 |
| `ReferenceEquality` | 1 | Redis catalog identity regression and constructor-only suppression in section 2 |
| `MissingSummary` | 1 | Public Javadoc summary in section 2 |
| `JavaTimeDefaultTimeZone` | 1 | Fixed date/explicit zone in section 1 |
| `FutureReturnValueIgnored` | 1 | Observe the future in section 1 |
| `BooleanLiteral` | 1 | Literal assertion cleanup in section 2 |
This table accounts for all 123 Error Prone/compiler-warning diagnostics. The separate
`-Xlint:deprecation,unchecked` inventory is covered by the third-party migrations and exact legacy
seam policy below; it is not allowed to disappear through a source-set suppression.
## Non-Goals
- Do not remove the legacy poster-image endpoint, `StoredObjectResponse`, raw-key compatibility
data, or legacy object-storage adapters during warning cleanup.
- Do not switch the sample runtime from legacy to publication mode without the separately required
API, data-adoption, dual-read, and external-consumer approvals.
- Do not apply module-wide or task-wide suppression for `removal`, `deprecation`, `unchecked`, or
Error Prone rules.
- Do not weaken architecture rules or change deliberately forbidden bytecode merely to silence a
fixture warning.
- Do not make quarantine tests blocking; their separate sunset and reporting policy remains
unchanged.
## Design Principles
1. Fix behavior defects at their source before applying any suppression.
2. Use suppression only where a framework signature, identity invariant, intentional violation
fixture, or approved compatibility seam makes the warning inapplicable.
3. Scope every suppression to the smallest class, method, field, constructor, or fixture that
explains it, with a nearby rationale.
4. Replace deprecated third-party APIs with their typed current equivalents and verify behavior,
not only compilation.
5. Capture expected-negative diagnostics and assert them exactly; never discard stderr globally.
6. Add blocking gates only after the current warning inventory is clean.
## Component Design
### 1. Real behavior defects
#### Locale-independent identifiers
Use `Locale.ROOT` for security roles, notification configuration keys, repository ACL names, and
test comparisons. Add Turkish-default-locale regressions that restore the original default locale
in `finally`:
- `JwtToAuthenticatedPrincipalConverter`: `admin` must always become `ROLE_ADMIN`;
- `RoutingNotifier`: diagnostic keys for `EMAIL` must remain `app.notification.routes.email...`;
- `RepoStatsAclMapper`: `IDEA/Repo` must normalize to `idea/repo`.
This is a correctness fix: the current code can generate dotless/dotted Turkish-I variants in
authorization and operational identifiers.
#### Quote-aware ETag list parsing
Do not replace `String.split(",")` with another delimiter-only splitter. A comma is legal inside a
quoted opaque entity tag. `ETags` will use a small scanner that:
- splits only on commas outside a quoted string;
- preserves weak-tag prefixes and the existing trimming behavior;
- treats malformed/unclosed quotes as non-matching input rather than guessing a token;
- preserves wildcard and ordinary multi-value behavior.
Regressions cover a single comma-bearing tag, a mixed list containing a weak comma-bearing tag,
ordinary lists, wildcard, stale values, blank input, and malformed quoting.
#### Asynchronous and cleanup failures
- `AsyncGracefulShutdownBehaviorTest` retains the returned `Future<?>` and observes `get()` so a
background assertion or exception cannot disappear.
- Outbox test cleanup methods propagate or wrap resource-destruction failures with the original
cause instead of using empty catches.
- Tests use fixed dates, UTF-8, and explicit locale rather than host defaults.
### 2. Production warning cleanup with preserved invariants
#### Redis primitive ownership
`RedisPrimitiveInvocation` intentionally requires descriptor object identity. Value equality would
admit a descriptor created by another catalog and weaken the closed-catalog invariant. Keep the
reference comparison, add an exact constructor-level `ReferenceEquality` suppression, and add a
regression proving value-equal but non-identical cross-catalog descriptors are rejected.
Qualify both nested `ExpectedKind` types with their enclosing record names rather than renaming the
types. This removes `SameNameButDifferent` without changing bytecode or package-local consumers.
#### Framework-owned generic signature
`RedisVersionedSession.<T>getAttribute(String)` must retain Spring Session's inherited signature.
Apply a method-only `TypeParameterUnusedInFormals` suppression with the interface-contract reason.
#### Instance-owned retry context
`OutboundRetryPolicy` keeps its instance `ThreadLocal`. Making it static would leak call context
between policy instances on the same thread. Add a field-only `ThreadLocalUsage` suppression and a
regression proving policy A's context is invisible to policy B and is cleared by `endCall()`.
#### Array-bearing records
- `NotificationCiphertext` retains its public array components because it already clones inputs and
accessors, implements content-based equality/hash code, and redacts `toString`. Add focused
defensive-copy/equality/redaction tests and an exact record-level suppression.
- The four internal session command/outcome records in `VersionedRedisSessionStore` remain internal
transport envelopes. Preserve defensive copies, document that generated record equality is not
their contract, add constructor/accessor copy tests, and suppress `ArrayRecordComponent` on each
exact record.
- The private test fake in `RedisVersionedSessionRepositoryTest` receives the same exact nested-type
treatment; no public type is changed.
#### Mechanical behavior-neutral fixes
- Express 72 hours as `Duration.ofDays(3)` in application/bootstrap/sample settings and matching
tests.
- Add the missing public Javadoc summary in `TracingSampleRateResolver`, and render annotation names
such as `@WebMvcTest` as inline `{@code ...}` rather than accidental block tags.
- Add missing `@Override` annotations in sample test fakes.
- Replace readability-only string concatenations with text blocks where the literal bytes remain
identical.
- Replace Boolean wrapper comparisons with boolean literals.
- For the three test-only delimiter warnings, preserve each existing grammar explicitly: retain CSV
empty-token filtering with a limit-bearing split or scanner, scan mapping-path segments without
changing leading/trailing-empty behavior, and parse the single HTTP byte-range hyphen with an
asserted `indexOf` boundary. These are not allowed to inherit the ETag scanner because their
grammars differ.
### 3. Third-party API migration
#### Jackson 3
In `LocalJsonSchemaRegistry`, replace deprecated `JsonNode.isTextual()`/`textValue()` with
`isString()`/`stringValue()`. Existing type guards remain, and JSON schema identity/reference/value
tests prove identical acceptance and rejection behavior.
In `DeterministicEnvelopeWriter`, replace the deprecated convenience call with
`jsonFactory.createGenerator(ObjectWriteContext.empty(), output, JsonEncoding.UTF8)`, the
non-deprecated Jackson 3.0.2 overload. Preserve canonical byte output; the existing deterministic
envelope golden tests are the behavior gate.
#### Lettuce
Convert both finite canonical scores to `BigDecimal`, build one inclusive
`Range<? extends Number>` for each invocation, and call the typed `zcount(key, range)` and
`zrangebyscoreWithScores(key, range, Limit.create(offset, count))` overloads. Preserve inclusive
bounds, offset, count, and exact reply mapping. A dynamic-proxy regression verifies both typed
overloads are selected; sorted-set primitive contract tests verify results.
#### AWS SDK retry
Replace old `RetryPolicy` and core `EqualJitterBackoffStrategy` with `StandardRetryStrategy`, the
retries API half-jitter exponential backoff, `maxAttempts`, and
`ClientOverrideConfiguration.Builder.retryStrategy`. Tests assert maximum attempts and normal versus
throttling backoff configuration. The focused object-storage check must cover provider assembly;
compile-only success is insufficient.
#### Testcontainers Toxiproxy
Use the Testcontainers 2 toxiproxy package and a typed `ToxiproxyClient`/`Proxy` with an explicit
exposed proxy port. Fault tests must still prove cut and restore behavior against MinIO. Dependency
and lock changes stay inside the object-storage leaf.
#### Remaining JDK/generic deprecations
- Replace deprecated `new URL(String)` test construction with `URI.create(...).toURL()`.
- Replace the varargs `thenReturn(firstFuture, secondFuture)` stub in
`S3ConditionalObjectControlStoreTest` with two chained single-value `thenReturn(...)` calls, so
Mockito does not create the unchecked generic `CompletableFuture<PutObjectResponse>[]` array.
- Resolve every `-Xlint:deprecation,unchecked` location individually; do not suppress the source
set.
### 4. Legacy object-storage compatibility seam
The canonical object-storage ports and sample publication path already exist. The legacy runtime is
still selected in local/test configuration and cannot be deleted solely to silence warnings.
Keep `@Deprecated(forRemoval = true)` on the genuinely replaced whole-byte contracts:
- `ObjectStoragePort`;
- `StoredObject`;
- `ObjectStorageSettings`.
Apply `removal` suppression only to exact compatibility owners:
- `ObjectStoragePort` for its legacy receipt return type;
- `FilesystemObjectStorageAdapter` and `S3ObjectStorageAdapter`;
- `UploadPosterImageUseCase`;
- the legacy bean method in `PosterImageApiConfig`;
- `LegacyPosterImageController`;
- `PosterWebMapper.toStoredObjectResponse`;
- named legacy characterization test classes and single legacy-receipt test methods.
The six `application.storage.migration` types and `AdoptLegacyPosterImageUseCase` are the mechanism
used to complete data adoption and currently have no replacement. Change their lifecycle marker
from `@Deprecated(forRemoval = true)` to plain `@Deprecated`; use exact `deprecation` suppression
only inside adoption implementation/configuration. Keep the application-core architecture contract
requiring `forRemoval=true` only for `ObjectStoragePort` and `StoredObject`. Keep the adapter-owned
`ObjectStorageSettings` marker and add its lifecycle assertion in the object-storage leaf.
This keeps migration debt visible without falsely claiming that the migration mechanism itself is
ready for removal.
### 5. Test/static-analysis/output cleanup
#### SpotBugs
Reuse one static `SecureRandom` in `RedisPrimitiveRuntimeServiceTest` rather than constructing a
one-shot generator. After all test reports are clean, make every ordinary and custom test-source
SpotBugs task included by `check` blocking. SpotBugs analysis errors and missing classes remain
separately fail-closed.
#### Intentional architecture fixtures
Keep prohibited `BigDecimal(double/float)` constructor bytecode and apply method-only
`BigDecimalLiteralDouble` suppressions. Fix unrelated warnings in allowed fixtures normally. A
suppression must never replace the forbidden operation the ArchUnit test is supposed to detect.
#### Redis lab expected failures
Change `assert_fails` to capture stdout/stderr per case, assert a non-zero exit and the exact expected
diagnostic, reject extra lines, and print the capture only when the assertion fails. Do not redirect
to `/dev/null` and do not silence the Gradle `Exec` task globally.
#### Mockito/CDS
Provide `mockito-core` to test JVMs as an explicit startup `-javaagent` through a relocatable Gradle
argument provider. This removes reliance on Java 21+ runtime self-attachment. Add test-JVM-only
`-Xshare:off` because Mockito's bootstrap append otherwise prints the harmless CDS warning. No
production JVM argument changes.
#### Skips
Retain exactly these five intentional app-bootstrap contract skips:
- `emailNotificationAdapterRunsOnlyWhenConfigured()`;
- `slackNotificationAdapterRunsOnlyWhenConfigured()`;
- `redisCacheAdapterRunsOnlyWhenEnabled()`;
- `messagingBrokerAdapterRunsOnlyWhenConfigured()`;
- `DisabledOptionalAdapterFixture.wouldFailIfItEverRan()`.
Qualification tasks continue to require positive discovery, at least one executed test, zero skips,
and fresh XML, so this policy cannot turn a selected qualification lane green without execution.
Any additional skip, or any of these five moving outside its named optional-adapter contract, fails
the inventory check.
### 6. Warning-zero enforcement
After all existing warnings are removed:
- configure every leaf `JavaCompile` task with `-Werror`, `-Xlint:deprecation`, and
`-Xlint:unchecked` in the root build policy;
- retain Error Prone on the same compile tasks so its warnings are promoted by `-Werror`;
- remove the root `checkstyleTest`/`spotbugsTest` warning-only policy and the app-bootstrap
`sampleOffTest`, `functionalTest`, and `conditionalTransportTest` Checkstyle/SpotBugs
`ignoreFailures` overrides, making every such task included by `check` blocking;
- retain exact suppression comments as the only approved exception mechanism;
- run Gradle with `--warning-mode=fail` in the warning-clean CI lane so Gradle API deprecations also
fail rather than print.
`quarantineTest` remains non-blocking by design. Protected AWS/Docker qualifications remain separate
environment evidence and are not converted into local unit tests.
## File Ownership and Expected Change Groups
### Root build policy
- `src/build.gradle`
- `src/gradle/test-jvm-agents.gradle`, defining the relocatable Mockito `-javaagent` argument
provider and test-only `-Xshare:off` policy, applied once by the root build
- `.github/workflows/ci-quality-gates.yml`, adding `--warning-mode=fail` to the blocking
`quality-gates` Gradle invocation
### Production leaves
- `src/application-core`
- `src/adapter/inbound/web`
- `src/adapter/outbound/cache-redis`
- `src/adapter/outbound/fileserver`
- `src/adapter/outbound/httpclient`
- `src/adapter/outbound/identifier`
- `src/adapter/outbound/messaging`
- `src/adapter/outbound/notification`
- `src/adapter/outbound/objectstorage`
- `src/adapter/outbound/persistence-jpa`
- `src/app-bootstrap`
- `src/sample-portfolio`
- `src/shared-contract`
Every focused command is derived from the owning leaf's `gradle_path` in
`src/config/architecture/modules.json`; no production dependency edge changes are permitted unless
the registry is deliberately updated and its architecture verifier passes.
### Tests and shell contract
- owning leaf tests adjacent to every behavior change
- exact architecture violation fixtures under app-bootstrap test sources
- `infra/redis-lab/test/redis-lab-contract.sh`
## Implementation Sequence
1. Add failing behavioral regressions for locale, ETag parsing, async exception observation,
cleanup propagation, Redis descriptor identity, and retry-context isolation.
2. Implement those behavior fixes and run owner-focused tests.
3. Remove behavior-neutral compiler/Error Prone warnings per leaf, using only exact justified
suppressions.
4. Migrate Jackson, Lettuce, AWS SDK, Testcontainers, URL, and generic stubs; run their focused
behavior/qualification tests.
5. Correct legacy lifecycle markers and exact compatibility suppressions; run application-core,
object-storage, sample, and architecture contracts.
6. Clean test-only warnings, SpotBugs, Mockito/CDS, and Redis-lab output.
7. Enable blocking compiler, Checkstyle, SpotBugs, and Gradle warning gates.
8. Run focused checks, architecture validators, dependency locks, full tests, full check, and the
fresh warning-clean build.
9. Update the LLM Wiki branch note and the warning-debt error note with resolved evidence or exact
remaining environmental blockers.
## Verification Strategy
### Focused verification
- Each behavior change follows RED → GREEN with the owning leaf test.
- Static-only warning fixes use the exact `compileJava`, `compileTestJava`, Checkstyle, or SpotBugs
task as the failing/passing executable contract.
- Third-party API migrations run behavior tests that exercise request mapping, retry/backoff,
sorted-set bounds, schema parsing, or network-fault cut/restore semantics.
- Legacy suppressions are checked by architecture tests that reject old imports outside the named
compatibility surface.
### Repository verification
Run from `src/`:
```bash
./gradlew test --no-daemon --console=plain
./gradlew check --no-daemon --console=plain
./gradlew build --warning-mode=fail --no-daemon --console=plain
./gradlew clean build --warning-mode=all --no-daemon --console=plain
./gradlew verifyCleanArchitectureDependencies verifyRuntimeModuleMembership \
verifyDependencyLocks verifyPublicPathSnapshot verifyEnvKeys \
--no-daemon --console=plain
```
Also verify the real gate matrix, wrapper contract, shell syntax, warning-report XML, skipped-test
inventory, and `git diff --check`.
## Failure Handling
- If a suggested warning fix changes a public signature or weakens an identity/security invariant,
retain the behavior and use an exact documented suppression backed by a regression.
- If three attempted fixes in one warning family fail or expose cross-module coupling, stop that
family and revisit the design instead of stacking suppressions.
- If the AWS retry or Toxiproxy migration cannot reproduce old behavior, report that qualification
as blocked; do not claim warning-zero by suppressing the deprecation.
- If a warning originates only in generated code, prove the generated source owner and configure
that exact generated boundary; do not disable warnings for handwritten sources.
## Risks and Mitigations
- **ETag grammar regression:** use quote-aware focused tests before replacing the parser.
- **Authorization drift:** test role normalization under Turkish locale.
- **Redis catalog weakening:** retain identity comparison and test cross-catalog rejection.
- **AWS retry semantic drift:** assert maximum attempts and backoff classes/policies, then run the
object-storage provider tests.
- **Legacy data stranding:** preserve legacy activation and characterization until the separate
data/API migration gates are approved.
- **Hidden diagnostics:** capture-and-assert expected stderr; never discard it.
- **Suppression creep:** exact annotations plus architecture/import checks prevent module-wide
exemptions.
- **Build duration:** use owner-focused RED/GREEN loops and reserve full clean builds for integration
checkpoints and final proof.
## Acceptance Criteria
The work is complete only when:
1. All behavior regressions and focused owner checks pass.
2. Every compiler task passes with `-Werror`, deprecation lint, unchecked lint, and Error Prone.
3. Every ordinary/custom Checkstyle and SpotBugs task included by `check` is blocking and clean.
4. Legacy warnings are limited to no output because exact compatibility code is explicitly and
locally justified; no module/task-wide suppression exists.
5. The Redis lab successful contract prints only its success summary and unexpected child
diagnostics still fail the test with captured evidence.
6. Test JVMs print no CDS/self-attachment warning.
7. Full test, check, build, dependency, architecture, runtime-membership, env, public-path, wrapper,
gate-matrix, shell, and diff validators pass.
8. The final fresh clean-build log contains no `warning:`, deprecated/unchecked `Note:`, SpotBugs
non-zero message, OpenJDK warning, or leaked expected-negative Redis diagnostic.
9. LLM Wiki capture records commands, results, resolved warning counts, suppressions, and any
environment-only qualification not executed locally.
@@ -0,0 +1,72 @@
# Web Security Boundary Design
**Date:** 2026-08-02
**Status:** approved by the user's instruction to apply the reviewed P1/P2 work sequentially
**Scope:** JWT/OIDC/JWKS and CORS behavior at the `adapter:inbound:web` Spring Security filter boundary
## Context
The module has unit contracts for JWT validators, exception classification, envelope writers, and
CORS settings. It does not yet prove that a real bearer request crosses issuer discovery, JWKS
retrieval, signature/claim validation, principal conversion, `SecurityFilterChain`, and the public
error envelope. CORS configuration is likewise untested at the filter boundary, where preflight
ordering relative to authentication is the important behavior.
These are release-boundary checks and must not silently skip because an external IdP, environment
variable, or optional flag is absent.
## Decision
Add a dedicated `webSecurityBoundaryTest` task that reuses the ordinary test output/classpath and
runs only JUnit tests tagged `security-boundary`. Ordinary `test` excludes that tag so each contract
runs once. The dedicated task:
- fails when no tests are discovered;
- disables up-to-date reuse;
- fails the root suite when any test reports `SKIPPED`;
- is required by the inbound-web `check` task;
- uses UTC and no environment-dependent conditions or assumptions.
JWT tests use a JDK loopback `HttpServer` bound to `127.0.0.1` on an ephemeral port. It serves the
minimum OIDC discovery document and JWKS response. Tests generate ephemeral RSA keys and compact
RS256 JWTs with the already-resolved Nimbus dependency; no new library or external network is
allowed. Each failure case uses a fresh server and Spring context to prevent decoder/JWK cache
cross-contamination.
CORS tests build the production `SecurityConfig` and real `springSecurityFilterChain` with direct
configuration properties. They issue real preflight and actual-origin MockMvc requests. A test JWT
decoder bean is allowed here because CORS ordering—not token decoding—is the owned boundary.
## JWT/JWKS Contract
- application context startup performs zero discovery/JWKS calls (lazy decoder);
- a correctly signed token reaches a protected controller and exposes the expected
`AuthenticatedPrincipal` subject/roles;
- expiry beyond the configured 60-second skew, issuer mismatch, audience mismatch, wrong
signature, and unknown `kid` produce their exact stable 401 error codes and bounded
`WWW-Authenticate`/`Retry-After` headers;
- deterministic JWKS 503 produces `AUTH_JWKS_UNAVAILABLE`, HTTP 503, and `Retry-After: 30`;
- after that first-request 503, the same lazy decoder/context retries initialization and succeeds
once the JWKS endpoint recovers;
- discovery metadata that is fetched successfully but is internally inconsistent produces the
fixed 500 `INTERNAL_AUTH_MISCONFIGURATION` envelope rather than a raw initialization exception;
- responses never contain the bearer token, issuer URL, `kid`, JWK material, or internal decoder
diagnostics.
## CORS Contract
- an approved credentialed preflight to an authenticated endpoint succeeds before bearer
authentication and emits exact origin/credentials/method/header/max-age policy;
- an unapproved origin receives 403 without allow-origin or allow-credentials reflection;
- disabled CORS emits no CORS response headers;
- wildcard origin without credentials returns `*` and no credentials header;
- an approved actual-origin request receives matching CORS and bounded `Vary` headers;
- wildcard plus credentials remains a settings startup failure (already covered by settings tests).
## Non-Goals
- external IdP/TLS/rotation rehearsal;
- browser-engine SameSite behavior;
- Redis-backed session continuity (the next P1 batch);
- new test libraries, Docker, or changes to production dependency direction;
- staging, commit, amend, or push by an agent.
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff