chore: record pre-existing uncommitted repository state

Snapshot of the in-flight state that already existed, identically, in both
this worktree and the main checkout before this session began: the initial
HTTP Client platform implementation (previously untracked), the redis-lab
removal, and the JPA / object-storage / notification integration work.

Kept separate from this session's HTTP Client review response, which lands
in the following commit, so the two bodies of work stay reviewable apart.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
DongHyeonka
2026-08-11 16:48:43 +09:00
co-authored by Claude Opus 5
parent 1a3b560678
commit 5f10b791d3
1857 changed files with 130925 additions and 72491 deletions
@@ -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