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:
co-authored by
Claude Opus 5
parent
1a3b560678
commit
5f10b791d3
@@ -0,0 +1,510 @@
|
||||
# Release Hygiene Refactoring Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Make every release-hygiene path truthful by fixing the sample-off architecture gate, aligning the Gradle 9.0.0 wrapper and CI validation, making Docker cache stages valid without `.git`, completing SpotBugs analysis classpaths, and removing the observed Gradle 10 deprecation.
|
||||
|
||||
**Architecture:** Leaf-specific architecture rules move to their owning leaf while root tests remain cross-module. Build inputs become explicit: Docker copies registry inputs, evidence-only Git validation executes only in evidence tasks, wrapper bytes/checksums are fixed, and SpotBugs derives auxiliary inputs from the source set it analyzes.
|
||||
|
||||
**Tech Stack:** Java 21, Spring Boot 4.0.0, Gradle 9.0.0 Groovy DSL, ArchUnit 1.3.0, SpotBugs Gradle plugin 6.5.6/SpotBugs 4.10.2, Bash, Docker/BuildKit, GitHub Actions.
|
||||
|
||||
## Global Constraints
|
||||
|
||||
- Preserve all 19 leaf identities and production dependency edges from `src/config/architecture/modules.json`.
|
||||
- `domain-core` and `application-core` gain no framework, transport, database, or cloud dependency.
|
||||
- Do not weaken an architecture rule with a global `allowEmptyShould(true)`.
|
||||
- Keep Gradle at exactly `9.0.0` in this plan.
|
||||
- Set `distributionSha256Sum=8fad3d78296ca518113f3d29016617c7f9367dc005f932bd9d93bf45ba46072b`.
|
||||
- The official Gradle 9.0.0 wrapper JAR SHA-256 is `76805e32c009c0cf0dd5d206bddc9fb22ea42e84db904b764f3047de095493f3`.
|
||||
- Pin `gradle/actions/wrapper-validation` to commit `3f131e8634966bd73d06cc69884922b02e6faf92` in workflows that invoke Gradle.
|
||||
- Docker images do not receive `.git`; full evidence revisions arrive through `-PgitRevision`/CI attestation.
|
||||
- SpotBugs dependency scopes are not widened to silence missing-class output.
|
||||
- Agents do not stage, commit, amend, or push; commit steps from the generic workflow are replaced by diff/status evidence.
|
||||
|
||||
---
|
||||
|
||||
### Task 1: Move the Object Storage Architecture Rule to Its Owning Leaf
|
||||
|
||||
**Files:**
|
||||
- Create: `src/adapter/outbound/objectstorage/src/test/java/dev/caskeleton/adapter/outbound/objectstorage/ObjectStorageArchitectureTest.java`
|
||||
- Modify: `src/adapter/outbound/objectstorage/build.gradle`
|
||||
- Modify: `src/adapter/outbound/objectstorage/gradle.lockfile`
|
||||
- Modify: `src/app-bootstrap/src/test/java/dev/caskeleton/bootstrap/architecture/CleanArchitectureTest.java:1586-1608`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: production classes under `dev.caskeleton.adapter.outbound.objectstorage..` and application/shared contracts already on the Object Storage test classpath.
|
||||
- Produces: an owner-local ArchUnit rule named `OBJECT_STORAGE_ADAPTER_METHOD_RETURNS_ONLY_APPLICATION_OR_PRIMITIVES`; a sample-off root suite with no Object Storage presence requirement.
|
||||
|
||||
- [ ] **Step 1: Reproduce the existing failing regression**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
cd src
|
||||
./gradlew :app-bootstrap:sampleOffTest --tests '*CleanArchitectureTest' --console=plain
|
||||
```
|
||||
|
||||
Expected: FAIL only at `OBJECT_STORAGE_ADAPTER_METHOD_RETURNS_ONLY_APPLICATION_OR_PRIMITIVES` because no matching classes are present.
|
||||
|
||||
- [ ] **Step 2: Add the owner-local test before removing the root rule**
|
||||
|
||||
Create a package-local ArchUnit test that imports production classes from the Object Storage package and applies this rule:
|
||||
|
||||
```java
|
||||
@AnalyzeClasses(packages = "dev.caskeleton.adapter.outbound.objectstorage")
|
||||
class ObjectStorageArchitectureTest {
|
||||
@ArchTest
|
||||
static final ArchRule OBJECT_STORAGE_ADAPTER_METHOD_RETURNS_ONLY_APPLICATION_OR_PRIMITIVES =
|
||||
methods()
|
||||
.that()
|
||||
.areDeclaredInClassesThat()
|
||||
.resideInAPackage("..adapter.outbound.objectstorage..")
|
||||
.and()
|
||||
.areDeclaredInClassesThat()
|
||||
.haveSimpleNameEndingWith("Adapter")
|
||||
.and()
|
||||
.arePublic()
|
||||
.and()
|
||||
.areNotStatic()
|
||||
.should()
|
||||
.notHaveRawReturnType(
|
||||
JavaClass.Predicates.resideInAnyPackage(
|
||||
"..adapter.outbound..",
|
||||
"..adapter.inbound.web..",
|
||||
"..adapter.outbound.persistence.."))
|
||||
.allowEmptyShould(false);
|
||||
}
|
||||
```
|
||||
|
||||
Add the owner-local test dependency:
|
||||
|
||||
```groovy
|
||||
testImplementation 'com.tngtech.archunit:archunit-junit5:1.3.0'
|
||||
```
|
||||
|
||||
Refresh only the Object Storage leaf lock state with its existing `resolveAndLockAll --write-locks`
|
||||
task. This is a test-scope dependency; do not add a production project or external dependency edge.
|
||||
|
||||
- [ ] **Step 3: Run the owner test while the root regression remains red**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
cd src
|
||||
./gradlew :adapter:outbound:objectstorage:resolveAndLockAll --write-locks --console=plain
|
||||
./gradlew :adapter:outbound:objectstorage:test --tests '*ObjectStorageArchitectureTest' --console=plain
|
||||
```
|
||||
|
||||
Expected: PASS with matching production adapter methods.
|
||||
|
||||
- [ ] **Step 4: Remove only the misplaced root rule**
|
||||
|
||||
Delete the `OBJECT_STORAGE_ADAPTER_METHOD_RETURNS_ONLY_APPLICATION_OR_PRIMITIVES` field from `CleanArchitectureTest`; do not change neighboring cross-module rules.
|
||||
|
||||
- [ ] **Step 5: Verify both ownership paths**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
cd src
|
||||
./gradlew :adapter:outbound:objectstorage:test :app-bootstrap:sampleOffTest --console=plain
|
||||
```
|
||||
|
||||
Expected: PASS, zero failed tests.
|
||||
|
||||
- [ ] **Step 6: Record diff evidence without committing**
|
||||
|
||||
Run `git diff --check` and `git status --short`; retain the output for the task review.
|
||||
|
||||
### Task 2: Align and Validate the Gradle 9.0.0 Wrapper
|
||||
|
||||
**Files:**
|
||||
- Create: `.github/scripts/verify-gradle-wrapper.sh`
|
||||
- Modify: `src/gradle/wrapper/gradle-wrapper.properties`
|
||||
- Regenerate: `src/gradle/wrapper/gradle-wrapper.jar`, `src/gradlew`, `src/gradlew.bat`
|
||||
- Modify: `.github/workflows/ci-quality-gates.yml`
|
||||
- Modify: `.github/workflows/dependency-vulnerability.yml`
|
||||
- Modify: `.github/workflows/jpa-r2-evidence.yml`
|
||||
- Modify: `.github/workflows/object-storage-qualification.yml`
|
||||
- Modify: `.github/workflows/redis-production-readiness.yml`
|
||||
- Lock without modification: `.github/workflows/link-check.yml`
|
||||
- Modify: `src/app-bootstrap/src/test/java/dev/caskeleton/bootstrap/contract/DeveloperExperienceContractTest.java`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: repository root as argument 1, wrapper properties/JAR, and every YAML workflow under `.github/workflows`.
|
||||
- Produces: executable `verify-gradle-wrapper.sh` with exit 0 only for the exact Gradle 9.0.0 wrapper, the reviewed six-file workflow path/SHA-256 lock, the repository's restricted canonical workflow grammar, and jobs where an unconditional pinned validation step gates every reachable Gradle invocation.
|
||||
|
||||
- [ ] **Step 1: Write failing executable-contract tests**
|
||||
|
||||
Add a `DeveloperExperienceContractTest` case that runs:
|
||||
|
||||
```java
|
||||
Process process =
|
||||
new ProcessBuilder("bash", ".github/scripts/verify-gradle-wrapper.sh", REPOSITORY_ROOT.toString())
|
||||
.directory(REPOSITORY_ROOT.toFile())
|
||||
.redirectErrorStream(true)
|
||||
.start();
|
||||
assertThat(process.waitFor()).as(new String(process.getInputStream().readAllBytes(), UTF_8)).isZero();
|
||||
```
|
||||
|
||||
Add a second case that copies wrapper properties/JAR and workflows to `@TempDir`, changes the distribution checksum, runs the script against that fixture root, and asserts a non-zero exit. The production mutation this test catches is accepting a wrong wrapper or distribution checksum.
|
||||
|
||||
- [ ] **Step 2: Verify RED**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
cd src
|
||||
./gradlew :app-bootstrap:test --tests '*DeveloperExperienceContractTest' --console=plain
|
||||
```
|
||||
|
||||
Expected: FAIL because `.github/scripts/verify-gradle-wrapper.sh` does not exist and the checked-in wrapper is not the Gradle 9.0.0 JAR.
|
||||
|
||||
- [ ] **Step 3: Implement the wrapper verifier**
|
||||
|
||||
The Bash script must:
|
||||
|
||||
```text
|
||||
1. require exactly one repository-root argument;
|
||||
2. require the exact ordered eight-line wrapper-properties file, including the Gradle 9.0.0 URL
|
||||
and distribution checksum from Global Constraints;
|
||||
3. reject duplicate, alternate-separator, escaped, continued, reordered, or extra properties;
|
||||
4. compare the wrapper JAR SHA-256 with the exact Gradle 9.0.0 JAR hash;
|
||||
5. enumerate every top-level `.yml`/`.yaml` workflow, reject symlinks/special files, and compare the
|
||||
exact sorted six-path set and SHA-256 values to the verifier's embedded reviewed workflow lock;
|
||||
additions, removals, renames, or byte changes are failures;
|
||||
6. structurally validate the supported block grammar before admission and emit specific diagnostics
|
||||
for recognized noncanonical `jobs`/job/`steps` containers, flow collections, aliases, anchors,
|
||||
tags, merge keys, encoded or multiline action scalars, and quoted/escaped run scalars; YAML
|
||||
semantics outside this deliberately partial diagnostic parser remain covered by the primary
|
||||
byte lock rather than an overclaim of complete Bash YAML parsing;
|
||||
7. require every Gradle-running job to order checkout, the exact wrapper-validation action with
|
||||
stable `id: gradle-wrapper-validation`, and every Gradle invocation;
|
||||
8. accept the validation step only with its exact canonical name/id/uses fields and no `if`,
|
||||
`continue-on-error`, `with`, `env`, timeout, or other weakening field;
|
||||
9. finalize every Gradle step, not only the first. A Gradle step may have no condition or exactly
|
||||
`${{ always() && steps.gradle-wrapper-validation.outcome == 'success' }}`; bare `always()`,
|
||||
failure/cancelled paths, `continue-on-error`, and other reachability expressions fail closed;
|
||||
10. treat literal run-block body text only as shell data, never as an action field, and require each
|
||||
raw Gradle reference admitted by the gate to resolve to a canonical job;
|
||||
11. print `gradle-wrapper-contract: PASS` only when every check succeeds.
|
||||
```
|
||||
|
||||
For an intentional workflow edit, review the complete workflow diff, verify that no workflow path
|
||||
is a symlink/special file, regenerate the entire sorted `sha256sum` list with:
|
||||
|
||||
```bash
|
||||
find .github/workflows -mindepth 1 -maxdepth 1 \
|
||||
\( -name '*.yml' -o -name '*.yaml' \) ! -type f -print # must print nothing
|
||||
find .github/workflows -mindepth 1 -maxdepth 1 -type f \
|
||||
\( -name '*.yml' -o -name '*.yaml' \) -print0 \
|
||||
| LC_ALL=C sort -z | xargs -0 sha256sum
|
||||
```
|
||||
|
||||
Replace the complete sorted embedded array in the same reviewed change. Never refresh only the
|
||||
failing digest as a build-unblock shortcut.
|
||||
|
||||
- [ ] **Step 4: Regenerate the wrapper twice and add the distribution checksum**
|
||||
|
||||
Run in `src/`:
|
||||
|
||||
```bash
|
||||
./gradlew wrapper --gradle-version 9.0.0 --distribution-type bin
|
||||
./gradlew wrapper --gradle-version 9.0.0 --distribution-type bin
|
||||
```
|
||||
|
||||
Then add the exact `distributionSha256Sum` property immediately after `distributionUrl`.
|
||||
|
||||
- [ ] **Step 5: Add the pinned validation action to every Gradle workflow job**
|
||||
|
||||
After each checkout step and before setup/cache/build invokes Gradle, add:
|
||||
|
||||
```yaml
|
||||
- name: Validate Gradle wrapper
|
||||
id: gradle-wrapper-validation
|
||||
uses: gradle/actions/wrapper-validation@3f131e8634966bd73d06cc69884922b02e6faf92 # gradle/actions@v6
|
||||
```
|
||||
|
||||
Jobs without a Gradle invocation do not need the action. A sanitizer that intentionally executes
|
||||
after a failed test must use the exact guarded condition shown above so wrapper-validation failure
|
||||
still prevents Gradle. Preserve that behavior in Redis rather than using bare `always()`.
|
||||
|
||||
- [ ] **Step 6: Verify GREEN and mutation rejection**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
bash .github/scripts/verify-gradle-wrapper.sh .
|
||||
cd src
|
||||
./gradlew :app-bootstrap:test --tests '*DeveloperExperienceContractTest' --console=plain
|
||||
```
|
||||
|
||||
Expected: script prints `gradle-wrapper-contract: PASS`; focused tests pass; executable mutations
|
||||
reject checksum/property overrides, missing validation per job, named/anonymous/quoted/escaped and
|
||||
continued action variants, encoded run scalars, block/alias/merge/flow YAML forms, validation-step
|
||||
control fields, Gradle steps reachable after validation failure, custom-shell or alternate-wrapper
|
||||
paths, duplicate encoded jobs, workflow additions/removals/symlinks, and otherwise innocuous byte
|
||||
drift through the primary workflow lock.
|
||||
|
||||
- [ ] **Step 7: Record diff evidence without committing**
|
||||
|
||||
Run `sha256sum src/gradle/wrapper/gradle-wrapper.jar`, `git diff --check`, and `git status --short`.
|
||||
|
||||
### Task 3: Make Docker Build Configuration Inputs Explicit
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/Dockerfile:39-66`
|
||||
- Modify: `src/Dockerfile.sample:50-75`
|
||||
- Modify: `src/build.gradle:2153-2181` and all Redis evidence consumers
|
||||
- Modify: `src/adapter/outbound/cache-redis/build.gradle` (leaf evidence consumers)
|
||||
- Modify: `src/app-bootstrap/build.gradle`
|
||||
- Modify: `src/sample-portfolio/build.gradle`
|
||||
- Modify: `src/app-bootstrap/src/test/java/dev/caskeleton/bootstrap/contract/DeveloperExperienceContractTest.java`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `config/**`, Gradle source/build files, `-PgitRevision`, and the `bootJar` archive provider.
|
||||
- Produces: `:app-bootstrap:stageDockerJar` and `:sample-portfolio:stageDockerJar`, each writing exactly `build/docker/application.jar`; evidence metadata is resolved only when a Redis evidence task executes.
|
||||
|
||||
- [ ] **Step 1: Write failing build-contract tests**
|
||||
|
||||
Add tests that split each Dockerfile at its first `RUN ./gradlew` and assert the preceding section
|
||||
uses repository-preserving `WORKDIR /build/src` and contains `COPY config/ ./config/`. Add tests
|
||||
that require the Dockerfiles to run `stageDockerJar` and copy the exact
|
||||
`build/docker/application.jar`, with no `ls | grep | head` selection. Add a test that runs
|
||||
`./gradlew help -PgitRevision=0123456789abcdef0123456789abcdef01234567` from a temporary Git-less
|
||||
copy containing the same files as the dependency-cache stage. Add three self-contained evidence-task
|
||||
fixtures under temporary repository roots: one uses a `.git` directory, one uses a worktree `.git`
|
||||
metadata file, and one uses a dangling `.git` symlink. All prepend a fake `git` to `PATH` and require
|
||||
the exact named failure for `rev-parse` or `status` process errors; the symlink fixture must also prove
|
||||
the link entry exists with `NOFOLLOW_LINKS`. These tests must copy the minimum build/registry inputs
|
||||
and invoke the fixture wrapper; they must not assert or execute the ambient checkout's `.git`.
|
||||
|
||||
- [ ] **Step 2: Verify RED**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
cd src
|
||||
./gradlew :app-bootstrap:test --tests '*DeveloperExperienceContractTest' --console=plain
|
||||
```
|
||||
|
||||
Expected: FAIL because neither cache stage copies `config/**`, both select JARs with shell matching, and Git is resolved during configuration.
|
||||
|
||||
- [ ] **Step 3: Add deterministic Docker staging tasks**
|
||||
|
||||
In both executable modules register:
|
||||
|
||||
```groovy
|
||||
tasks.register('stageDockerJar', Sync) {
|
||||
dependsOn tasks.named('bootJar')
|
||||
from(tasks.named('bootJar').flatMap { it.archiveFile })
|
||||
into(layout.buildDirectory.dir('docker'))
|
||||
rename { 'application.jar' }
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Update both Dockerfiles**
|
||||
|
||||
Use `WORKDIR /build/src` so repository-relative registry paths resolve under `/build/src/**`, copy
|
||||
`config/` before the first Gradle invocation, invoke the correct `stageDockerJar` task with the
|
||||
existing release/revision properties, and copy only the fixed `build/docker/application.jar` path
|
||||
into the runtime stage.
|
||||
|
||||
- [ ] **Step 5: Move Redis Git evidence resolution to execution time**
|
||||
|
||||
Replace the eager `String` values with closures/providers invoked from evidence task actions:
|
||||
|
||||
```groovy
|
||||
Closure<Map<String, String>> resolveRedisSourceEvidence = {
|
||||
File gitMetadata = rootProject.file('../.git')
|
||||
if (!java.nio.file.Files.exists(
|
||||
gitMetadata.toPath(), java.nio.file.LinkOption.NOFOLLOW_LINKS)) {
|
||||
String attested = providers.gradleProperty('gitRevision')
|
||||
.orElse(providers.environmentVariable('GITHUB_SHA'))
|
||||
.orElse(providers.environmentVariable('GIT_SHA'))
|
||||
.getOrElse('')
|
||||
if (!(attested ==~ /[0-9a-f]{40}/)) {
|
||||
throw new GradleException(
|
||||
'Redis evidence requires an exact 40-character source revision.')
|
||||
}
|
||||
return [revision: attested, treeState: 'ATTESTED']
|
||||
}
|
||||
|
||||
String headFailure = 'Redis evidence failed to resolve checked-out Git HEAD.'
|
||||
def headExecution
|
||||
try {
|
||||
headExecution = providers.exec {
|
||||
commandLine 'git', 'rev-parse', 'HEAD'
|
||||
ignoreExitValue = true
|
||||
}
|
||||
if (headExecution.result.get().exitValue != 0) {
|
||||
throw new GradleException(headFailure)
|
||||
}
|
||||
} catch (GradleException exception) {
|
||||
if (exception.message == headFailure) {
|
||||
throw exception
|
||||
}
|
||||
throw new GradleException(headFailure, exception)
|
||||
}
|
||||
String checkedOut = headExecution.standardOutput.asText.getOrElse('').trim()
|
||||
if (!(checkedOut ==~ /[0-9a-f]{40}/)) {
|
||||
throw new GradleException(headFailure)
|
||||
}
|
||||
String supplied = providers.gradleProperty('gitRevision')
|
||||
.orElse(providers.environmentVariable('GITHUB_SHA'))
|
||||
.orElse(providers.environmentVariable('GIT_SHA'))
|
||||
.orElse(checkedOut)
|
||||
.getOrElse('')
|
||||
if (!(supplied ==~ /[0-9a-f]{40}/)) {
|
||||
throw new GradleException('Redis evidence requires an exact 40-character source revision.')
|
||||
}
|
||||
if (!checkedOut.isBlank() && supplied != checkedOut) {
|
||||
throw new GradleException('Redis evidence source revision does not match checked-out HEAD.')
|
||||
}
|
||||
|
||||
String statusFailure = 'Redis evidence failed to inspect checked-out Git status.'
|
||||
def statusExecution
|
||||
try {
|
||||
statusExecution = providers.exec {
|
||||
commandLine 'git', 'status', '--porcelain', '--untracked-files=normal'
|
||||
ignoreExitValue = true
|
||||
}
|
||||
if (statusExecution.result.get().exitValue != 0) {
|
||||
throw new GradleException(statusFailure)
|
||||
}
|
||||
} catch (GradleException exception) {
|
||||
if (exception.message == statusFailure) {
|
||||
throw exception
|
||||
}
|
||||
throw new GradleException(statusFailure, exception)
|
||||
}
|
||||
String treeState = statusExecution.standardOutput.asText.getOrElse('').isBlank()
|
||||
? 'CLEAN'
|
||||
: 'DIRTY'
|
||||
[revision: supplied, treeState: treeState]
|
||||
}
|
||||
```
|
||||
|
||||
Each evidence-producing root `doLast` and each leaf evidence test's root-suite `afterSuite` resolves
|
||||
this once and uses the returned values for all generated/validated artifacts. The resolver is
|
||||
exposed as `rootProject.ext.resolveRedisSourceEvidence`; eager scalar ext properties are removed.
|
||||
Non-evidence tasks never call the closure. Any repository-root `.git` filesystem entry is detected
|
||||
without following symbolic links, so a directory, worktree metadata file, or dangling symlink always
|
||||
selects the checkout branch. Both Git processes must start, exit zero, and return valid evidence
|
||||
before `CLEAN` or `DIRTY` can be emitted. `ATTESTED` is reserved for a truly absent `.git` entry in
|
||||
an explicitly Git-less build with an exact supplied revision; a Git execution failure must never
|
||||
fall back to it.
|
||||
|
||||
- [ ] **Step 6: Verify GREEN without `.git` and verify evidence mismatch failure**
|
||||
|
||||
Run the focused contract test, `./gradlew help` in the Git-less fixture with a 40-character
|
||||
`gitRevision`, and one Redis evidence task in the real checkout. The Git-less help invocation must
|
||||
pass; a Git-less Redis evidence task with a short revision must fail with the named message. Separate
|
||||
self-contained fixtures must cover a `.git` directory whose `rev-parse` fails, a `.git` worktree file
|
||||
whose `status` fails, and a dangling `.git` symlink whose Git invocation fails. Each fixture must
|
||||
assert the corresponding named fail-closed diagnostic instead of accepting a generic non-zero exit.
|
||||
|
||||
- [ ] **Step 7: Run actual Docker smoke when Docker is available**
|
||||
|
||||
Run both image builds with `--no-cache`. If Docker is unavailable, record the exact blocker and leave these commands as remaining risk; do not claim Docker success from string tests.
|
||||
|
||||
- [ ] **Step 8: Record diff evidence without committing**
|
||||
|
||||
Run `git diff --check` and `git status --short`.
|
||||
|
||||
### Task 4: Complete SpotBugs Auxiliary Classpaths and Remove the Gradle 10 Warning
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/build.gradle:208-360`
|
||||
- Modify: `src/build.gradle:1760-1795`
|
||||
- Test/verify: app-bootstrap redisComposition, inbound GraphQL main, inbound gRPC main SpotBugs tasks
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: every leaf's `SourceSetContainer` and the SpotBugs task named for each source set.
|
||||
- Produces: each SpotBugs task's `auxClassPaths` containing `sourceSet.runtimeClasspath - sourceSet.output` and a required XML report whose analysis errors/missing classes are checked after execution; `verifyApplicationCoreDependencyPurity` uses a configuration-time `Project` reference and declares its execution-time configuration traversal incompatible with the configuration cache.
|
||||
|
||||
- [ ] **Step 1: Capture the failing static-analysis evidence**
|
||||
|
||||
Run clean focused SpotBugs tasks and save output. Expected RED messages name Spring Session, `io.micrometer.context.ContextSnapshot`, and protobuf types as classes needed for analysis.
|
||||
|
||||
- [ ] **Step 2: Capture the Gradle 10 deprecation RED**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
cd src
|
||||
./gradlew verifyApplicationCoreDependencyPurity --warning-mode=fail --console=plain
|
||||
```
|
||||
|
||||
Expected: FAIL on execution-time `Task.project` access.
|
||||
|
||||
- [ ] **Step 3: Configure source-set-derived auxiliary classpaths**
|
||||
|
||||
After applying SpotBugs in each leaf, configure:
|
||||
|
||||
```groovy
|
||||
sourceSets.configureEach { sourceSet ->
|
||||
String taskName = "spotbugs${sourceSet.name.capitalize()}"
|
||||
tasks.named(taskName, com.github.spotbugs.snom.SpotBugsTask) {
|
||||
auxClassPaths.from(sourceSet.runtimeClasspath - sourceSet.output)
|
||||
def xmlAnalysisReport = reports.maybeCreate('xml')
|
||||
xmlAnalysisReport.required.set(true)
|
||||
doLast {
|
||||
List<String> analysisFailures =
|
||||
spotBugsAnalysisFailures(xmlAnalysisReport.outputLocation.get().asFile)
|
||||
if (!analysisFailures.isEmpty()) {
|
||||
throw new GradleException(
|
||||
"${path}: SpotBugs analysis incomplete:\n " +
|
||||
analysisFailures.join('\n '))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Do not add compile/runtime dependencies solely for SpotBugs. The XML parser fails on a missing or
|
||||
malformed report, malformed `Errors` counts, any `MissingClass`, and any analysis `Error`; ordinary
|
||||
`BugInstance` findings remain governed by the existing main/test severity policy. Wire an
|
||||
executable `verifySpotBugsAnalysisFailureContract` fixture into every leaf `check` so clean and
|
||||
advisory-bug-only reports pass while missing-class and analysis-error reports fail.
|
||||
|
||||
- [ ] **Step 4: Remove execution-time project access**
|
||||
|
||||
Resolve `Project applicationCoreProject = project(':application-core')` before registering
|
||||
`verifyApplicationCoreDependencyPurity`; capture that variable in `doLast` instead of calling
|
||||
`project(...)` from the task action. Because the action still traverses project configurations at
|
||||
execution time, declare
|
||||
`notCompatibleWithConfigurationCache('Inspects project configurations at execution time')` rather
|
||||
than making an unsupported compatibility claim.
|
||||
|
||||
- [ ] **Step 5: Verify GREEN**
|
||||
|
||||
Run `verifySpotBugsAnalysisFailureContract`, the three clean focused SpotBugs tasks, and
|
||||
`verifyApplicationCoreDependencyPurity --warning-mode=fail`. Expected: exit 0, XML
|
||||
`Errors errors="0" missingClasses="0"`, and no missing-analysis-class/deprecation output.
|
||||
|
||||
- [ ] **Step 6: Run release-hygiene aggregate verification**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
cd src
|
||||
./gradlew clean check :app-bootstrap:sampleOffTest verifyPublicPathSnapshot verifyDependencyLocks --no-daemon --console=plain --warning-mode=fail
|
||||
cd ..
|
||||
bash .github/scripts/verify-gate-matrix.sh
|
||||
bash .github/scripts/verify-gradle-wrapper.sh .
|
||||
```
|
||||
|
||||
Expected: every command exits 0; no skipped mandatory gate, missing SpotBugs class, or Gradle deprecation.
|
||||
|
||||
- [ ] **Step 7: Record final diff evidence without committing**
|
||||
|
||||
Run `git diff --check`, `git diff --stat`, and `git status --short`. Dispatch the complete diff for architecture/spec and code-quality review.
|
||||
|
||||
## Plan Self-Review
|
||||
|
||||
- Spec coverage: every release-hygiene design decision maps to Tasks 1-4.
|
||||
- Type consistency: both executable modules expose the same `stageDockerJar` task and output path; Redis evidence uses one `Map<String,String>` resolver contract.
|
||||
- Architecture: no production dependency edge changes are required.
|
||||
- Test discipline: each behavior has a named failing command or executable mutation fixture before implementation.
|
||||
- Commit policy: all generic commit steps are replaced with diff/status evidence.
|
||||
@@ -0,0 +1,73 @@
|
||||
# Client-Safe Error Boundary Implementation Plan
|
||||
|
||||
> **Execution:** Follow `superpowers:test-driven-development`; request an independent code review
|
||||
> before advancing to the next P1 batch.
|
||||
|
||||
**Goal:** Ensure public HTTP error envelopes contain only allowlisted messages and bounded safe
|
||||
metadata, never raw exceptions or request values.
|
||||
|
||||
**Architecture:** The inbound web adapter maps operational codes to fixed public messages. The
|
||||
sample consumer owns a parallel domain-code mapping. Exception diagnostics stay behind the
|
||||
transport boundary.
|
||||
|
||||
**Tech Stack:** Java 21, Spring Boot 4.0.0, JUnit 6/JUnit Jupiter, AssertJ, MockMvc.
|
||||
|
||||
## Constraints
|
||||
|
||||
- Preserve all completed P0 and verification-purity changes in the dirty worktree.
|
||||
- Preserve every error code/status/category/retryable value.
|
||||
- Preserve safe protocol details and required headers.
|
||||
- Do not leak request DTOs or transport types into application/domain.
|
||||
- Do not stage, commit, amend, or push.
|
||||
|
||||
### Task 1: Operational Handler RED Contracts
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/adapter/inbound/web/src/test/java/dev/caskeleton/adapter/inbound/web/error/GlobalExceptionHandlerTest.java`
|
||||
- Modify: `src/adapter/inbound/web/src/test/java/dev/caskeleton/adapter/inbound/web/error/TransportErrorHandlingTest.java`
|
||||
- Create: `src/adapter/inbound/web/src/test/java/dev/caskeleton/adapter/inbound/web/error/NoResourceFoundErrorHandlingTest.java`
|
||||
|
||||
- [x] Add secret-sentinel tests for mapping, illegal argument, adapter disabled, authentication,
|
||||
authorization, precondition, pagination, and cursor exceptions.
|
||||
- [x] Add validation tests proving rejected values, interpolated/default messages, and iterable
|
||||
keys/indices are absent while normalized fields plus allowlisted reason codes/fixed messages remain.
|
||||
- [x] Add transport tests proving raw request URLs and content-type values are not echoed.
|
||||
- [x] Add a real MVC resource-resolver test for a sentinel-bearing static-resource 404.
|
||||
- [x] Run the focused tests and record RED against the current raw-message implementation (30 tests, 9 expected failures).
|
||||
|
||||
### Task 2: Operational Allowlist Implementation
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/adapter/inbound/web/src/main/java/dev/caskeleton/adapter/inbound/web/error/ClientSafeErrorMessages.java`
|
||||
- Create: `src/adapter/inbound/web/src/main/java/dev/caskeleton/adapter/inbound/web/error/ClientSafeValidationDetails.java`
|
||||
- Modify: `src/adapter/inbound/web/src/main/java/dev/caskeleton/adapter/inbound/web/error/GlobalExceptionHandler.java`
|
||||
- Modify: `src/adapter/inbound/web/README.md`
|
||||
|
||||
- [x] Add code-specific fixed operational messages with a safe category fallback.
|
||||
- [x] Replace every public `ex.getMessage()`/rejected-value/raw-URL path.
|
||||
- [x] Discard validation message/value data, normalize field paths, strip iterable keys/indices, and
|
||||
emit only allowlisted reason codes with fixed messages.
|
||||
- [x] Route both `NoHandlerFoundException` and `NoResourceFoundException` through the same safe 404 envelope.
|
||||
- [x] Retain safe field/reason/expected-type/supported-method/media-type details and `Allow`.
|
||||
- [x] Run the operational/transport tests and confirm GREEN.
|
||||
|
||||
### Task 3: Sample Domain RED and Implementation
|
||||
|
||||
**Files:**
|
||||
- Create: `src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/adapter/inbound/web/error/PortfolioClientSafeErrorMessages.java`
|
||||
- Modify: `src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/adapter/inbound/web/error/DomainExceptionHandler.java`
|
||||
- Modify: `src/sample-portfolio/src/test/java/dev/caskeleton/sample/portfolio/adapter/inbound/web/error/DomainExceptionHandlerTest.java`
|
||||
|
||||
- [x] Add ID/title/reason sentinel tests and confirm RED (4 expected failures).
|
||||
- [x] Map every `PortfolioErrorCode` to fixed public text and use it from the advice.
|
||||
- [x] Confirm code/status/category remain unchanged and sentinels are absent.
|
||||
|
||||
### Task 4: Focused and Architecture Verification
|
||||
|
||||
- [x] Run `./gradlew :adapter:inbound:web:test --console=plain`.
|
||||
- [x] Run `./gradlew :sample-portfolio:test --console=plain`.
|
||||
- [x] Run focused Spotless/Checkstyle/SpotBugs tasks for both modules.
|
||||
- [x] Run `./gradlew verifyCleanArchitectureDependencies --console=plain`.
|
||||
- [x] Run `git diff --check` and request an independent read-only review.
|
||||
- [x] Apply the independent review findings and receive a no-Critical/no-Important code re-review;
|
||||
align this design/plan with the final validation and resource-404 contract.
|
||||
@@ -0,0 +1,80 @@
|
||||
# Conditional Inbound Transport Boundary Implementation Plan
|
||||
|
||||
> **Execution:** Apply TDD independently per transport, then run exact no-skip qualification and an
|
||||
> independent read-only review before beginning P2 cleanup.
|
||||
|
||||
**Goal:** Make GraphQL, gRPC, and WebSocket opt-in status truthful, fail closed on unsafe activation,
|
||||
and release-blocked by real protocol evidence without adding them to the default runtime.
|
||||
|
||||
### Task 1: Runtime Membership and Opt-In Composition
|
||||
|
||||
**Files:** `src/config/architecture/modules.json`, `src/settings.gradle`, `src/build.gradle`,
|
||||
`src/app-bootstrap/build.gradle`, app-bootstrap conditional transport tests
|
||||
|
||||
- [ ] Add and fail-closed validate exact `runtime_memberships` for all 19 leaves.
|
||||
- [ ] Compare registry membership to both composition roots' direct production project edges.
|
||||
- [ ] Add an isolated conditional-transport test classpath containing all three opt-in leaves.
|
||||
- [ ] Prove the default graphs omit them and the explicit qualification graph contains them.
|
||||
|
||||
### Task 2: gRPC Safe Activation and Wire Errors
|
||||
|
||||
**Files:** `src/adapter/inbound/grpc/**`
|
||||
|
||||
- [ ] Add RED tests for disabled bean/listener absence and safe property defaults/validation.
|
||||
- [ ] Add real Netty feature RPC tests for auth success/failure and reflection disabled.
|
||||
- [ ] Add RED tests for throw, `onError(ApiErrorCarrier)`, and raw status sentinel paths.
|
||||
- [ ] Implement loopback-only explicit insecure mode, required feature authentication policy, and
|
||||
`ServerCall.close` sanitization.
|
||||
- [ ] Update dependencies, locks, README, and CLAUDE truthfully.
|
||||
|
||||
### Task 3: GraphQL Real HTTP Boundary
|
||||
|
||||
**Files:** `src/adapter/inbound/graphql/**`
|
||||
|
||||
- [ ] Add random-port HTTP tests for auth, CORS, GraphiQL/introspection policy, and health.
|
||||
- [ ] Add carrier/unknown exception sentinels and assert absence from the complete JSON response.
|
||||
- [ ] Change production resolver/config only where the RED wire contract proves necessary.
|
||||
- [ ] Update dependencies, locks, README, and CLAUDE truthfully.
|
||||
|
||||
### Task 4: WebSocket Safe Activation and Wire Boundary
|
||||
|
||||
**Files:** `src/adapter/inbound/websocket/**`
|
||||
|
||||
- [ ] Add RED settings/disabled-context tests and real STOMP origin/auth/subscription tests.
|
||||
- [ ] Add RED broker-send and ERROR-frame sentinel tests.
|
||||
- [ ] Add RED no-projection/no-broadcast plus safe projection broadcast tests.
|
||||
- [ ] Implement disabled default, validated settings, inbound authorization, safe error handler, and
|
||||
explicit primitive projection allowlist.
|
||||
- [ ] Update dependencies, locks, README, and CLAUDE truthfully.
|
||||
|
||||
### Task 5: Exact No-Skip Release Gate
|
||||
|
||||
**Files:** `src/build.gradle`, `.github/workflows/ci-quality-gates.yml`,
|
||||
`.github/ci-gate-matrix.yml`, `.github/scripts/verify-gate-matrix.sh`, wrapper manifest contract
|
||||
|
||||
- [ ] Register exact per-transport Test lanes with no-match/no-discovery/zero-skip enforcement.
|
||||
- [ ] Register the aggregate `conditionalTransportQualification` task.
|
||||
- [ ] Invoke it explicitly from the release-blocking quality job and add the gate-matrix record.
|
||||
- [ ] Add semantic tests that fail if any required lane or workflow invocation disappears.
|
||||
|
||||
### Task 6: Verification and Review
|
||||
|
||||
- [ ] Run each leaf `check`, exact qualification, app-bootstrap composition contract, dependency
|
||||
locks, env keys, architecture, public path, wrapper validation, and `git diff --check`.
|
||||
- [ ] Run full `test`/`check` in proportion to the cross-cutting registry/build changes.
|
||||
- [ ] Request independent read-only review; resolve all Critical/Important findings.
|
||||
- [ ] Capture the batch in the LLM Wiki before final completion reporting.
|
||||
|
||||
### Explicit P2 Deferral
|
||||
|
||||
- GraphQL feature schema, field auth, cost/depth, persisted queries, DataLoader, subscriptions.
|
||||
- gRPC TLS/mTLS, external bind, proto compatibility, deadlines, streaming/backpressure.
|
||||
- WebSocket broker relay, multi-node delivery, resume/replay, backpressure, versioned feature catalog.
|
||||
- Transport dashboards, SLO alerts, and provider/ingress qualification.
|
||||
# Implementation status
|
||||
|
||||
- Completed on 2026-08-02.
|
||||
- Verified by `conditionalTransportQualification`: GraphQL 8, gRPC 15, WebSocket 5,
|
||||
composition 1; skipped 0.
|
||||
- Verified by the real CI gate-matrix validator and focused bypass regression tests.
|
||||
- Independent review result: READY, Critical 0 / Important 0 / Minor 0.
|
||||
@@ -0,0 +1,268 @@
|
||||
# P2 Verification Governance Refactoring Plan
|
||||
|
||||
## Batch 1 — strict owner-local qualification
|
||||
|
||||
- [x] Add TestKit RED cases for empty source sets, missing FQCNs, disabled-only tests, and one valid
|
||||
test.
|
||||
- [x] Add the shared strict qualification convention.
|
||||
- [x] Move conditional transport and Messaging task registration from root to owner projects.
|
||||
- [x] Adopt the convention for object-storage, Poster migration, and composition qualifications.
|
||||
- [x] Keep root tasks as absolute-path aggregators and verify all evidence XML.
|
||||
- [x] Run focused TestKit, every migrated qualification lane, locks, and independent review.
|
||||
|
||||
Evidence: eight TestKit cases passed fresh; conditional transport ran 8/15/5/1 tests and Messaging
|
||||
ran 15/6/4/29/28 tests with zero skips. All dependency locks passed. Object-storage and Poster
|
||||
required-class preflights passed; protected AWS and Docker-backed full lanes remain environment-
|
||||
qualified. Independent review closed with no remaining Critical, Important, or Minor findings.
|
||||
|
||||
## Batch 2 — tracked contract resources hard-fail
|
||||
|
||||
- [x] Add RED tests proving absent tracked files/directories fail instead of aborting.
|
||||
- [x] Add `RepositoryContractResources` and inject the canonical repository root.
|
||||
- [x] Replace stale tracked-resource assumptions in the contract corpus.
|
||||
- [x] Preserve assumptions only for genuinely optional external infrastructure.
|
||||
- [x] Run focused representative contracts, scan for stale skip language, and run app-bootstrap
|
||||
`check`.
|
||||
|
||||
Evidence (2026-08-02): the fail-closed repository resolver is covered by 11 boundary tests;
|
||||
Runbook coverage and lock-classification contracts passed with zero skips. Independent review found
|
||||
and closed both direct-link and directory-enumeration symlink escapes. A fresh
|
||||
`./gradlew :app-bootstrap:check --no-daemon --console=plain` passed (77 tasks; 18 executed, 59
|
||||
up-to-date), and the final Batch 2 review reported zero Critical, Important, or Minor findings.
|
||||
|
||||
## Batch 3 — real gate-matrix mutation tests
|
||||
|
||||
- [x] Add temporary-fixture tests that execute the shell validator itself.
|
||||
- [x] Make the validator accept a repository-root argument without changing default CI behavior.
|
||||
- [x] Delete the duplicated Java command parser.
|
||||
- [x] Cover deceptive names, suppression flags, missing/duplicate gates, and missing task wiring.
|
||||
- [x] Run the focused contract, real repository validator, and wrapper verifier.
|
||||
|
||||
Evidence (2026-08-02): the initial focused RED compiled and reported seven failing contracts against
|
||||
the old validator. Independent review found arbitrary project-qualified task matching, shorthand
|
||||
step parsing, generic `name:` registration, relocated-script guard evidence, unsafe custom refs,
|
||||
missing `check` wiring evidence, and process-tree cleanup gaps; each was closed with a regression
|
||||
test or bounded cleanup. A final regex-boundary audit also closed custom-task and plugin-ref ERE
|
||||
injection with literal-safe grammars and fixed-string plugin lookup. The final focused contract
|
||||
passed all 16 tests using bounded
|
||||
`ProcessBuilder` execution of the real shell script. `bash .github/scripts/verify-gate-matrix.sh`
|
||||
passed with 27 gates (26 verified and one explicitly delegated),
|
||||
`bash .github/scripts/verify-gradle-wrapper.sh .` passed, `bash -n` and
|
||||
`:app-bootstrap:spotlessJavaCheck` passed, and `git diff --check` reported no whitespace errors.
|
||||
|
||||
## Batch 4 — Redis manifest JSON Schema conformance
|
||||
|
||||
- [x] Add invalid-manifest RED fixtures for bounds, patterns, required fields, and extra fields.
|
||||
- [x] Validate the canonical schema and all manifests with Draft 2020-12 semantics.
|
||||
- [x] Retain Java-catalog equality checks for cross-resource invariants.
|
||||
- [x] Run the focused schema test, cache-redis `check`, and dependency-lock verification.
|
||||
|
||||
Evidence (2026-08-02): the initial focused RED compile failed on the deliberately missing
|
||||
`RedisProgramManifestSchemaValidator` (six `cannot find symbol` errors). NetworkNT 3.0.2 now
|
||||
validates the canonical schema against its bundled Draft 2020-12 meta-schema and validates the
|
||||
exact six closed manifests under strict parsing/configuration. Mutation coverage exercises
|
||||
additional properties, type, required, enum, minimum/maximum, pattern, duplicate JSON keys, and
|
||||
an independent cross-resource duplicate-program-id Java invariant. The first GREEN attempt exposed
|
||||
that the canonical ACL pattern rejected the existing `SCRIPT|LOAD` command form; the pattern was
|
||||
narrowly relaxed before independent review identified that it also admitted dangerous commands.
|
||||
A second RED run failed exactly two tests because the schema had no exact allowlist and accepted
|
||||
`FLUSHALL`, `CONFIG|SET`, and `MODULE|LOAD`. The six canonical manifests contain 265 ACL command
|
||||
occurrences and exactly 37 unique commands; `aclCommands.items` now uses that exact enum so adding
|
||||
a command requires an explicit schema change. Review coverage also rejects a trailing manifest
|
||||
JSON token and a duplicate schema key on the compile path, and pins invalid meta-schema diagnostics
|
||||
to `/type:type`. Final verification passed:
|
||||
`./gradlew :adapter:outbound:cache-redis:test --tests '*RedisProgramManifestContractTest' --console=plain`
|
||||
(12 tests), `./gradlew :adapter:outbound:cache-redis:test
|
||||
:adapter:outbound:cache-redis:spotlessJavaCheck --console=plain`,
|
||||
`./gradlew :adapter:outbound:cache-redis:verifyDependencyLocks
|
||||
:adapter:outbound:cache-redis:spotlessCheck --console=plain`, and
|
||||
`./gradlew :adapter:outbound:cache-redis:check --console=plain`. The owner lock gained only
|
||||
`com.networknt:json-schema-validator:3.0.2` and `com.ethlo.time:itu:1.14.0`; no
|
||||
`tools.jackson.dataformat:jackson-dataformat-yaml` entry is present. `git diff --check` passed.
|
||||
The configured owner `check` remained successful while its SpotBugs test report retained one
|
||||
pre-existing `DMI_RANDOM_USED_ONLY_ONCE` finding in `RedisPrimitiveRuntimeServiceTest`; the new
|
||||
schema validator and contract test introduced no SpotBugs finding.
|
||||
|
||||
## Batch 5 — registry and runbook governance
|
||||
|
||||
- [x] Enforce an exact catalog for every tracked registry, including object-storage readiness.
|
||||
- [ ] Resolve every stable `required_test` ID exactly once and reject dangling mappings.
|
||||
- [ ] Replace the Java runbook stub allowlist with owned, issue-linked, expiring debt data.
|
||||
- [ ] Clarify tracked registry ownership and private-wiki provenance.
|
||||
- [x] Run schema, object-storage readiness, runbook, app-bootstrap, and root checks. The checks
|
||||
exercise the mechanically enforceable catalog/containment rules; the three semantic migrations
|
||||
above remain explicitly blocked on project-owner evidence.
|
||||
|
||||
### Batch 5-A evidence — exact tracked registry catalog (2026-08-02)
|
||||
|
||||
The owner catalog now enumerates exactly eight regular, non-symlink direct children: seven
|
||||
universal contract registries plus the specialized object-storage readiness registry. The initial
|
||||
focused RED failed compilation on the deliberately absent `RegistryGovernanceCatalog` (13 symbol
|
||||
errors). A second exact-version mutation RED proved that numeric coercion admitted
|
||||
`schema_version: 1.5`; the implementation now requires the integer value `1`. Strict SnakeYAML
|
||||
safe construction disables duplicate keys and aliases, enforces exact root keys, a non-empty list
|
||||
of map rows, non-blank unique identities, the existing universal row policy, and the specialized
|
||||
owner delegation/provenance policy. Missing, unknown, non-regular, symlinked, malformed, duplicate,
|
||||
false-provenance, block-scalar spoofing, reordered-header, and fabricated-branch-header fixtures
|
||||
fail closed.
|
||||
|
||||
Gradle declares `docs/registries` as a relative-path-sensitive `:app-bootstrap:test` directory
|
||||
input. The object-storage owner declares its canonical readiness YAML as a relative-path-sensitive
|
||||
file input and passes its absolute path through `objectstorage.readiness.registry`; its leaf test no
|
||||
longer searches parent directories. The tracked specialized registry header is exactly four
|
||||
ordered leading comment lines containing only the factual repository and semantic owner Gradle
|
||||
paths and test FQCNs.
|
||||
|
||||
Fresh verification passed:
|
||||
|
||||
- `./gradlew :app-bootstrap:test --tests
|
||||
dev.caskeleton.bootstrap.contract.ContractRegistrySchemaGovernanceTest --console=plain`
|
||||
- `./gradlew :adapter:outbound:objectstorage:test --tests
|
||||
dev.caskeleton.adapter.outbound.objectstorage.readiness.ObjectStorageReadinessRegistryTest
|
||||
--console=plain`
|
||||
- `./gradlew :app-bootstrap:test --console=plain` (38 tasks; 2 executed)
|
||||
- `./gradlew :app-bootstrap:check --console=plain` (77 tasks; 21 executed)
|
||||
- `./gradlew :app-bootstrap:spotlessJavaCheck
|
||||
:adapter:outbound:objectstorage:spotlessJavaCheck --console=plain`
|
||||
- `git diff --check`, an exact direct-child regular-file audit, the owner-path `jq` audit, and
|
||||
`yq eval 'true' docs/registries/*.yaml` (eight parsed documents)
|
||||
|
||||
### Batch 5-C partial containment evidence — legacy runbook stub debt (2026-08-02)
|
||||
|
||||
This is bounded containment, not completion of the owned, issue-linked, expiring debt-ledger item
|
||||
above. The Java set is now named `LEGACY_STUB_DEBT`, contains exactly the 43 current
|
||||
`status: stub` runbooks, and is checked bidirectionally against canonical tracked runbook files.
|
||||
The stale `migration-failed.md` entry was removed because that runbook is already active. Active,
|
||||
missing, template, and newly introduced stub drift now fail the same exact-set contract. Messages
|
||||
and the runbook template forbid adding new legacy allowlist entries and direct maintainers to
|
||||
complete the runbook or adopt the future governed ledger.
|
||||
|
||||
The focused RED failed only because `migration-failed.md` was an unexpected legacy-debt element.
|
||||
After the containment change, the focused Runbook contract passed with 6 tests, zero failures, and
|
||||
zero skips. Fresh verification also passed `:app-bootstrap:spotlessJavaCheck` and
|
||||
`:app-bootstrap:check` (77 tasks; 18 executed, 59 up-to-date). Owner, issue, start/sunset,
|
||||
expiry enforcement, and the private-wiki provenance migration remain deliberately incomplete and
|
||||
the corresponding Batch 5 checkboxes remain open.
|
||||
|
||||
### Batch 5-B/C unresolved semantic migrations audit (2026-08-02)
|
||||
|
||||
These items are intentionally not marked complete. The seven universal registries contain 324
|
||||
non-reference `required_test` occurrences and 216 unique IDs. There is no tracked selector
|
||||
catalog, no Gradle declaration containing those IDs, and no ID that can currently be proven to
|
||||
resolve to one exact module/task/class/method selector. Exact Java test-source literals cover only
|
||||
17 IDs (45 occurrences, 40 in comments/Javadocs); 199 IDs have no exact source literal. Creating
|
||||
216 selectors from namespaces or historical branch labels would manufacture execution evidence,
|
||||
so the exact-linkage gate requires semantic owner confirmation or new tests before it can be
|
||||
enabled.
|
||||
|
||||
The runbook corpus contains 43 stub documents, all with response owner `oncall` but no accountable
|
||||
debt owner, real issue, approved expiry, or bounded debt window. The seven legacy registries contain
|
||||
30 distinct `owner_branch` labels, none resolving to a current local/remote Git ref, while their
|
||||
private-wiki paths are absent from a fresh clone. The repository files are now protected as the
|
||||
tracked artifacts, but current owner IDs, historical-label migration, CODEOWNERS identities,
|
||||
runbook expiry dates, the `INTERNAL_ERROR` reverse-link decision, and the four umbrella-runbook
|
||||
retention decisions require real project-owner input. Placeholder owners, issues, selectors, and
|
||||
sunsets were not added to make the checks pass.
|
||||
|
||||
## Batch 6 — bounded P2 cleanup
|
||||
|
||||
- [x] Extend link-check triggers and scan scope to module README/CLAUDE documents.
|
||||
- [x] Make Poster migration gate labels version-neutral while preserving externally stable job IDs.
|
||||
- [x] Replace fixed HTTP timeout sleeps with deterministic latch-controlled handlers.
|
||||
- [x] Separate sample-off compile evidence from its minimal runtime proof if exact required tests can
|
||||
be established without weakening coverage.
|
||||
- [x] Run focused docs, CI, HTTP client, sample-off, and wrapper checks.
|
||||
|
||||
Batch 6 link/Poster evidence: test-first changes made the two focused app-bootstrap contracts fail
|
||||
only for the absent module documentation scope and the legacy Poster V7 internal gate ID. The same
|
||||
contracts then passed with exact pull/push/lychee scope, all 27 gate IDs, and the stable external
|
||||
`poster-image-v7-migration` workflow job plus `posterImageMigrationTest` task mapping. The full
|
||||
`DeveloperExperienceContractTest` and `ConditionalTransportQualificationContractTest` classes
|
||||
passed, `posterImageMigrationTest` produced 4 tests with zero skips, and both the 27-entry gate
|
||||
validator and Gradle wrapper verifier passed. The complete sorted six-workflow SHA-256 lock was
|
||||
refreshed after review; app-bootstrap Java and sample-portfolio Spotless checks also passed. An
|
||||
independent Batch 6 link/Poster read-only review found no Critical, Important, or Minor issues.
|
||||
|
||||
Batch 6 HTTP evidence: the focused synchronization contract first failed on exactly five fixed
|
||||
sleeps across `OutboundHttpClientTest` (one), `OutboundHttpClientDeadlineTest` (one), and
|
||||
`OutboundCallExecutorTest` (three). The HTTP handlers now signal `requestStarted`, await a bounded
|
||||
`releaseResponse` latch, and are released in the caller's `finally` after the timeout result and
|
||||
classification assertions. Executor workers now block on a bounded latch interruption point, with
|
||||
the existing started/interrupted evidence and caller cleanup preserved. The four focused classes
|
||||
passed 25 tests with zero failures, errors, or skips. A 3-second read-timeout mutation failed when
|
||||
the handler's 1-second HTTP 204 fallback completed successfully, proving that the test cannot pass
|
||||
via the separate 5-second logical deadline. The full owner `test` passed, and
|
||||
`:adapter:outbound:httpclient:check` passed 29 tasks (16 executed, 13 up-to-date), including
|
||||
Spotless, Checkstyle, SpotBugs, architecture dependencies, and environment-key verification. No
|
||||
production source changed. Independent re-review found no remaining Critical, Important, or Minor
|
||||
issues and found no cleanup leak or deadlock race.
|
||||
|
||||
Batch 6 sample-off evidence: the focused build contract first failed because the dedicated source
|
||||
directory, compile lifecycle task, strict registration, and required FQCN did not exist. The
|
||||
`sampleOffTest` source set now compiles all 204 ordinary test sources plus the dedicated contract
|
||||
without `sample-portfolio`, while `sampleOffCompile` exposes that complete compile proof separately.
|
||||
The externally stable `sampleOffTest` task is registered through the shared strict qualification
|
||||
convention and executes only `SampleOffClasspathContractTest`; fresh XML reported exactly 1 test,
|
||||
0 skipped, 0 failures, and 0 errors. The existing eight strict-convention functional contracts
|
||||
passed, including missing-class, no-discovery, skip, and stale-evidence fail-closed cases. The
|
||||
focused build contract, `sampleOffCompile`, gate-matrix validator, wrapper verifier, dependency-lock
|
||||
verification, Spotless, and the full `:app-bootstrap:check` also passed; the full check completed 78
|
||||
tasks (23 executed, 55 up-to-date). This is focused/owner evidence; the repository-wide Batch 6
|
||||
aggregate is recorded below.
|
||||
|
||||
Batch 6 repository evidence (2026-08-02): the real gate-matrix validator passed all 27 entries
|
||||
(26 locally verified and the protected AWS lane explicitly delegated-pending), the Gradle-wrapper
|
||||
contract passed, `bash -n .github/scripts/verify-gate-matrix.sh` passed, all eight tracked registry
|
||||
YAML documents parsed, the Redis Draft 2020-12 schema parsed as JSON, and `git diff --check`
|
||||
reported no whitespace errors. The first repository `check` exposed a 503 in the first
|
||||
`JwtJwksSecurityFilterIntegrationTest` request while static-analysis workers were running. The
|
||||
single test passed in isolation, identifying a test-fixture scheduling race rather than a JWT
|
||||
classification mismatch. The embedded OIDC server now owns a dedicated single daemon executor and
|
||||
shuts it down in `close()`; the full eight-test security-boundary lane plus Checkstyle and Spotless
|
||||
passed, and a fresh repository `check` subsequently passed with the same boundary lane included.
|
||||
|
||||
## Final verification and capture
|
||||
|
||||
- [x] Run full Gradle tests/checks and all repository validators.
|
||||
- [x] Request an independent P2 code review, resolve actionable findings, and record semantic
|
||||
blockers separately.
|
||||
- [x] Update the LLM Wiki branch note and any honest derived raw documents.
|
||||
|
||||
Fresh aggregate evidence (2026-08-02):
|
||||
|
||||
- `./gradlew test --no-daemon --console=plain` — successful in 4m 24s (86 tasks).
|
||||
- `./gradlew check --no-daemon --console=plain` — first run failed only on the OIDC test-fixture
|
||||
race above; after the bounded fixture correction, successful in 4m 35s (260 tasks).
|
||||
- Final post-review `./gradlew check --no-daemon --console=plain` — successful in 10m 33s
|
||||
(260 tasks; 76 executed, 184 up-to-date). It regenerated the SampleRemoval result after the
|
||||
source edit: 5 tests, zero skipped/failures/errors.
|
||||
- `./gradlew verifyCleanArchitectureDependencies verifyRuntimeModuleMembership
|
||||
verifyDependencyLocks verifyPublicPathSnapshot verifyEnvKeys --no-daemon --console=plain` —
|
||||
successful (23 tasks); all 19 leaf locks passed and two runtime compositions matched the registry.
|
||||
- Real gate-matrix, wrapper, shell syntax, Redis JSON, registry YAML, and diff validators — all
|
||||
successful; the protected AWS qualification remains explicitly delegated to its environment.
|
||||
- Final `verifyDependencyLocks` rerun — successful in 24s with all 19 leaf tasks executed. The
|
||||
tracked-file assumption audit now reports only four Docker/Testcontainers integration
|
||||
assumptions; no registry or repository-contract assumption remains.
|
||||
|
||||
LLM Wiki capture evidence (2026-08-02): `raw/branch-notes/main.md` records the integrated P1/P2
|
||||
implementation, decisions, validation commands, failures, evidence grades, and unresolved semantic
|
||||
migrations. It links bidirectionally to one resolved error note, one interview-prep note, and one
|
||||
blog-topic note. The vault's targeted structure lint passed all three derived documents. The branch
|
||||
note passed its content, frontmatter, required-section, and wikilink checks but retained one explicit
|
||||
`NAMING_VIOLATION`: repository policy requires `<branch-name>.md` (`main.md`) while the vault naming
|
||||
rule permits only `feature|fix|chore|experiment-` branch-note prefixes. Neither policy was silently
|
||||
weakened; the exact conflict is the recorded capture-validation blocker.
|
||||
|
||||
Independent aggregate review evidence (2026-08-02): the first pass reported zero critical,
|
||||
three important, and two minor findings. Wiki capture closed the capture-pending finding; the two
|
||||
remaining important items were reclassified as the three project-semantic blockers already kept
|
||||
open in Batch 5. The two minor code findings were corrected with an exact test-fixture-only
|
||||
GraphQL SpotBugs exclusion and registry-derived scanning of all 18 production leaves in
|
||||
`SampleRemovalSmokeContractTest`. A follow-up audit also found and removed the last tracked-file
|
||||
assumption/upward-directory search in `PortfolioErrorCodeRegistryMappingTest`, replacing it with a
|
||||
canonical repository-root property, relative Gradle input, and missing-root/symlink-escape
|
||||
fail-closed checks. The re-review found no new code defect; its only completion-evidence concern
|
||||
was a stale SampleRemoval XML, addressed by the final repository `check` after these corrections.
|
||||
The reviewer retained only the Wiki naming-policy disclosure and this Batch 5 checkbox wording as
|
||||
minor documentation findings; both are now explicit here and in the branch note.
|
||||
@@ -0,0 +1,72 @@
|
||||
# Redis Session HTTP Boundary Implementation Plan
|
||||
|
||||
> **Execution:** Follow test-driven development and request an independent read-only review before
|
||||
> advancing to the remaining P1 work.
|
||||
|
||||
**Goal:** Prove browser-session security persists and fails closed across the real Spring Session ↔
|
||||
Redis composition, without silent skips.
|
||||
|
||||
**Architecture:** The app-bootstrap composition test reuses its existing Redis test source set and
|
||||
dependencies. It assembles inbound-web and cache-redis without adding a forbidden leaf-to-leaf edge.
|
||||
|
||||
**Tech Stack:** Java 21, Spring Boot 4.0.0, Spring Security 7, Spring Session 4, Testcontainers 2,
|
||||
Redis 7.4 digest-pinned image, MockMvc, Gradle 9.
|
||||
|
||||
### Task 1: Explicit Docker No-Skip Gate
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/app-bootstrap/build.gradle`
|
||||
|
||||
- [x] Exclude `redis-session-http` from ordinary `redisCompositionTest`.
|
||||
- [x] Register `redisSessionHttpIntegrationTest` over the same source output/classpath with tag
|
||||
inclusion, no-discovery failure, no-skip root-suite guard, UTC, rerun, and image-registry property.
|
||||
- [x] Keep the Docker task outside ordinary `check`; reuse Spring Session 4.0.0 and lock only the
|
||||
added `redisCompositionTestCompileClasspath` configuration.
|
||||
|
||||
### Task 2: Real Session HTTP RED Contract
|
||||
|
||||
**Files:**
|
||||
- Create: `src/app-bootstrap/src/redisCompositionTest/java/dev/caskeleton/adapter/outbound/cache/redis/RedisSessionHttpBoundaryIntegrationTest.java`
|
||||
|
||||
- [x] Load and validate the approved digest-pinned Redis image; explicitly start the container.
|
||||
- [x] Generate ephemeral TLS/ACL/password/HMAC material and assemble canonical SESSION-role
|
||||
configuration with full hostname verification and explicit trust.
|
||||
- [x] Cross CSRF, login, Spring Session filter, primitive snapshot, and hardened cookie creation.
|
||||
- [x] Close context A and prove context B restores the authenticated principal from Redis.
|
||||
- [x] Prove logout/tombstone rejects the old cookie and a stale repository save.
|
||||
- [x] Stop Redis during lookup and prove fail-closed controller behavior with fixed diagnostics.
|
||||
- [x] Record and resolve RED composition mismatches: response-commit session creation and framework
|
||||
request-cache serialization.
|
||||
|
||||
### Task 3: CI Release Gate
|
||||
|
||||
**Files:**
|
||||
- Modify: `.github/workflows/ci-quality-gates.yml`
|
||||
|
||||
- [x] Add `:app-bootstrap:redisSessionHttpIntegrationTest` to the existing `redis-standalone` job.
|
||||
- [x] Keep the existing required gate identity and matrix dependency unchanged.
|
||||
|
||||
### Task 4: Verification and Review
|
||||
|
||||
- [x] Run the explicit HTTP task and existing app-bootstrap Redis composition task.
|
||||
- [x] Run the selected cache-redis session capability lane, dependency locks, env keys, architecture,
|
||||
public-path snapshot, static analysis, and `git diff --check`.
|
||||
- [x] Request an independent read-only review and resolve all Critical/Important findings.
|
||||
|
||||
### Verification Evidence
|
||||
|
||||
- `:app-bootstrap:redisSessionHttpIntegrationTest`: 1 test, 0 skipped, GREEN.
|
||||
- `:adapter:outbound:cache-redis:redisSessionCapabilityTest`: GREEN with sanitized evidence.
|
||||
- `:adapter:inbound:web:check`: unit/contract/static analysis and 13 no-skip JWT/CORS boundary
|
||||
tests GREEN.
|
||||
- `:app-bootstrap:check :app-bootstrap:redisCompositionTest`: 640 bootstrap tests (6 pre-existing
|
||||
conditional Docker skips in the ordinary suite, not used as this gate's evidence), TestKit
|
||||
contracts, 14 Redis composition tests, Checkstyle, SpotBugs, and Spotless GREEN.
|
||||
- `verifyDependencyLocks verifyEnvKeys verifyCleanArchitectureDependencies
|
||||
verifyPublicPathSnapshot`: GREEN for all 19 registered leaves.
|
||||
- Review RED: final context reconciliation could retain the authentication saved at response commit;
|
||||
host TLS/ACL material permissions were too broad; the CI task lacked a semantic workflow assertion.
|
||||
- Review fixes: authoritative final empty/replacement context tests went RED then GREEN, async start
|
||||
defers commit-hook persistence, host material is `0700`/`0600` and copied selectively into the
|
||||
fixture, and the blocking Redis job is now asserted directly.
|
||||
- Independent re-review: Critical 0, Important 0, Minor 0; batch READY.
|
||||
@@ -0,0 +1,79 @@
|
||||
# Verification Purity Refactoring Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Make stale-JAR and public-path verification strictly read-only while preserving explicit cleanup/update workflows.
|
||||
|
||||
**Architecture:** Extract only these two root Gradle concerns into applied scripts so the production tasks can be exercised by isolated Gradle TestKit fixtures. Verification tasks only observe and fail; `clean*` and `update*` tasks are the sole writers.
|
||||
|
||||
**Tech Stack:** Java 21, Gradle 9.0.0 Groovy DSL, Gradle TestKit, JUnit 5, AssertJ.
|
||||
|
||||
## Global Constraints
|
||||
|
||||
- Preserve all existing P0 changes in the dirty worktree.
|
||||
- Preserve the 19-leaf registry and every production project dependency edge.
|
||||
- Normal archive tasks and every `verify*` task must be read-only.
|
||||
- `updatePublicPathSnapshot` requires `-PapprovePublicPathChange`.
|
||||
- Agents do not stage, commit, amend, or push.
|
||||
|
||||
---
|
||||
|
||||
### Task 1: Add Functional RED Contracts
|
||||
|
||||
**Files:**
|
||||
- Create: `src/app-bootstrap/src/functionalTest/java/dev/caskeleton/bootstrap/contract/BuildVerificationPurityContractTest.java`
|
||||
- Modify: `src/app-bootstrap/build.gradle`
|
||||
- Modify: `src/app-bootstrap/gradle.lockfile`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: production scripts at `src/gradle/archive-hygiene.gradle` and `src/gradle/public-path-snapshot.gradle`.
|
||||
- Produces: functional tests that execute real Gradle tasks and assert filesystem side effects.
|
||||
|
||||
- [x] Add an isolated `functionalTest` source set/task and its `functionalTestImplementation gradleTestKit()` dependency so Gradle's SLF4J provider cannot pollute ordinary tests.
|
||||
- [x] Add a nested temporary archive fixture with root + `family:module` projects. Apply the production archive script, pre-create a stale traceable JAR and a nonmatching JAR, run `:family:module:jar`, `verifyNoStaleTraceableJars`, and `cleanStaleTraceableJars`, and assert exact preservation/deletion plus the full task-path diagnostic.
|
||||
- [x] Add a temporary public-path fixture. Apply the production public-path script and assert missing/drifted snapshots are not written, the verifier rejects `-PapprovePublicPathChange`, and only the approved updater writes canonical content.
|
||||
- [x] Confirm the contracts RED before the two production scripts exist. The first RED run used the ordinary test source set; after it exposed Gradle TestKit's SLF4J provider collision, move the contract and TestKit dependency to isolated `functionalTest` configurations and add their strict lock state.
|
||||
|
||||
### Task 2: Separate Archive Verification from Cleanup
|
||||
|
||||
**Files:**
|
||||
- Create: `src/gradle/archive-hygiene.gradle`
|
||||
- Modify: `src/build.gradle`
|
||||
|
||||
**Interfaces:**
|
||||
- Produces: root tasks `verifyNoStaleTraceableJars` and `cleanStaleTraceableJars` with no dependency between them.
|
||||
|
||||
- [x] Move traceable archive matching/discovery and both root tasks into the applied script.
|
||||
- [x] Remove the stale-deleting `doFirst` from every `Jar` task while retaining manifest metadata.
|
||||
- [x] Apply the script before leaf `check` dependencies are configured; task actions discover leaf JAR tasks at execution time.
|
||||
- [x] Explicitly declare both archive tasks configuration-cache incompatible because their actions inspect subproject task models.
|
||||
- [x] Run the focused functional test and confirm archive cases are GREEN.
|
||||
|
||||
### Task 3: Separate Public-Path Verification from Update
|
||||
|
||||
**Files:**
|
||||
- Create: `src/gradle/public-path-snapshot.gradle`
|
||||
- Modify: `src/build.gradle`
|
||||
- Modify: `src/README.md`
|
||||
- Modify: `docs/security/public-paths-snapshot.txt`
|
||||
|
||||
**Interfaces:**
|
||||
- Produces: read-only `verifyPublicPathSnapshot` and explicitly mutating `updatePublicPathSnapshot`.
|
||||
|
||||
- [x] Centralize canonical snapshot rendering in the script.
|
||||
- [x] Make verification fail on missing env, missing snapshot, drift, and use of the approval property without any writes.
|
||||
- [x] Make update require `-PapprovePublicPathChange`, create the parent directory, and write canonical content.
|
||||
- [x] Replace documentation and snapshot instructions with `updatePublicPathSnapshot -PapprovePublicPathChange`.
|
||||
- [x] Run the focused functional test and confirm all public-path cases are GREEN.
|
||||
|
||||
### Task 4: Focused and Architecture Verification
|
||||
|
||||
**Files:** none beyond Tasks 1-3.
|
||||
|
||||
- [x] Run `./gradlew :app-bootstrap:functionalTest --tests '*BuildVerificationPurityContractTest' --console=plain`.
|
||||
- [x] Run `./gradlew :app-bootstrap:test --console=plain`; 640 ordinary tests pass after TestKit isolation (6 skipped), alongside the 9 functional contracts.
|
||||
- [x] Run `./gradlew :app-bootstrap:verifyDependencyLocks --console=plain`.
|
||||
- [x] Run `./gradlew :app-bootstrap:spotlessJavaCheck :app-bootstrap:checkstyleFunctionalTest :app-bootstrap:spotbugsFunctionalTest --console=plain`.
|
||||
- [x] Run `./gradlew verifyNoStaleTraceableJars verifyPublicPathSnapshot --console=plain` and confirm both are read-only and pass on the current baseline.
|
||||
- [x] Run `./gradlew verifyCleanArchitectureDependencies --console=plain`.
|
||||
- [x] Run `git diff --check` and record `git status --short` without staging or committing.
|
||||
@@ -0,0 +1,387 @@
|
||||
# Warning-Zero Build Refactoring Implementation Plan
|
||||
|
||||
> **For Codex:** REQUIRED SUB-SKILLS: use `superpowers:subagent-driven-development` for the
|
||||
> independent owner-leaf batches, `superpowers:test-driven-development` for behavior changes,
|
||||
> `superpowers:systematic-debugging` for any failure, and
|
||||
> `superpowers:verification-before-completion` before reporting success.
|
||||
|
||||
**Goal:** Remove the audited compiler/static-analysis/test-output warning debt, preserve the approved
|
||||
legacy compatibility boundaries, and make the blocking build fail on any future warning.
|
||||
|
||||
**Architecture:** Fix behavior in the owning leaf, preserve identity/framework/compatibility seams
|
||||
with the narrowest justified suppressions, migrate deprecated provider APIs in their outbound leaf,
|
||||
then enable root Gradle/CI gates only after all focused tasks are clean. No dependency edge or runtime
|
||||
membership changes are permitted. The 19-leaf registry remains the dependency SSOT.
|
||||
|
||||
**Tech Stack:** Java 21, Spring Boot 4.0.0, Gradle multi-project build, JUnit 5, AssertJ, Mockito,
|
||||
Error Prone, Checkstyle, SpotBugs, Jackson 3.0.2, Lettuce 6.8.1, AWS SDK v2, Testcontainers 2.
|
||||
|
||||
**Approved design:**
|
||||
`docs/superpowers/specs/2026-08-02-warning-zero-build-design.md`
|
||||
|
||||
**Repository constraints:** The worktree already contains user/P0/P1/P2 changes. Preserve them,
|
||||
never reset or rewrite unrelated files, and do not stage, commit, amend, or push. Agent tasks must
|
||||
edit only their assigned files and report overlaps before proceeding.
|
||||
|
||||
## Task 1: Freeze warning evidence and add behavior regressions
|
||||
|
||||
**Owner leaves:** `adapter-inbound-web`, `adapter-outbound-notification`, `sample-portfolio`,
|
||||
`app-bootstrap`
|
||||
|
||||
**Files:**
|
||||
|
||||
- Add: `src/adapter/inbound/web/src/test/java/dev/caskeleton/adapter/inbound/web/auth/JwtToAuthenticatedPrincipalConverterTest.java`
|
||||
- Modify: `src/adapter/inbound/web/src/test/java/dev/caskeleton/adapter/inbound/web/conditional/ETagsTest.java`
|
||||
- Modify: `src/adapter/outbound/notification/src/test/java/dev/caskeleton/adapter/outbound/notification/core/RoutingNotifierTest.java`
|
||||
- Modify: `src/sample-portfolio/src/test/java/dev/caskeleton/sample/portfolio/adapter/outbound/repostats/RepoStatsAclMapperTest.java`
|
||||
- Modify: `src/app-bootstrap/src/test/java/dev/caskeleton/bootstrap/async/AsyncGracefulShutdownBehaviorTest.java`
|
||||
|
||||
**Steps:**
|
||||
|
||||
1. Add Turkish-default-locale regressions for JWT role uppercasing, notification route-key
|
||||
lowercasing, and repository ACL lowercasing. Snapshot `Locale.getDefault()`, set
|
||||
`Locale.forLanguageTag("tr-TR")`, and restore it in `finally`.
|
||||
2. Add RED ETag cases for `"opaque,tag"`, weak `W/"opaque,tag"` inside a mixed list, malformed
|
||||
unclosed quotes, wildcard, blank, stale, and ordinary multiple values.
|
||||
3. Add a RED async case proving an exception raised in the submitted action reaches the test through
|
||||
`Future.get()`.
|
||||
4. Run the exact focused tests. Confirm the new locale/ETag cases fail for the intended reason; the
|
||||
async change uses the existing `FutureReturnValueIgnored` compile diagnostic as its RED contract:
|
||||
|
||||
```bash
|
||||
./gradlew :adapter:inbound:web:test --tests '*JwtToAuthenticatedPrincipalConverterTest' --tests '*ETag*' --console=plain
|
||||
./gradlew :adapter:outbound:notification:test --tests '*RoutingNotifier*' --console=plain
|
||||
./gradlew :sample-portfolio:test --tests '*RepoStatsAclMapper*' --console=plain
|
||||
./gradlew :app-bootstrap:test --tests '*AsyncGracefulShutdownBehaviorTest' --console=plain
|
||||
```
|
||||
|
||||
5. Do not change production code in this task; retain the behavior-test failures and compile warning
|
||||
as the TDD/static-analysis baseline.
|
||||
|
||||
## Task 2: Correct locale, ETag, async, cleanup, and host-default behavior
|
||||
|
||||
**Owner leaves:** `adapter-inbound-web`, `adapter-outbound-notification`, `sample-portfolio`,
|
||||
`app-bootstrap`, `application-core`, `shared-contract`, `adapter-outbound-fileserver`,
|
||||
`adapter-outbound-httpclient`, `adapter-outbound-identifier`
|
||||
|
||||
**Production files:**
|
||||
|
||||
- Modify: `src/adapter/inbound/web/src/main/java/dev/caskeleton/adapter/inbound/web/auth/JwtToAuthenticatedPrincipalConverter.java`
|
||||
- Modify: `src/adapter/inbound/web/src/main/java/dev/caskeleton/adapter/inbound/web/conditional/ETags.java`
|
||||
- Modify: `src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/core/RoutingNotifier.java`
|
||||
- Modify: `src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/adapter/outbound/repostats/RepoStatsAclMapper.java`
|
||||
|
||||
**Test/mechanical files:**
|
||||
|
||||
- Modify the nine audited implicit-charset sites in `CursorCodecTest`,
|
||||
`RedisTrustMaterialProviderTest`, `OutboundHttpClientTest`,
|
||||
`HmacUserPrincipalPseudonymizerTest`, `StreamingResponseBodyAllowedFixture`, and
|
||||
`IdempotencyExecutorTest`.
|
||||
- Modify the remaining audited test-only locale sites in `JwtDecoderConfigTest`,
|
||||
`OutboundHttpClientTest`, `WorkLogReservedIntegrationEventMapperJsonTest`, `WorkLogIdTest`, and
|
||||
`TraceParentTest`.
|
||||
- Modify: `src/sample-portfolio/src/test/java/dev/caskeleton/sample/portfolio/domain/worklog/WorkLogTest.java`
|
||||
- Modify the four outbox cleanup classes under
|
||||
`src/app-bootstrap/src/test/java/dev/caskeleton/bootstrap/integration/outbox/`.
|
||||
- Modify: `src/adapter/outbound/fileserver/src/test/java/dev/caskeleton/adapter/outbound/fileserver/FilesystemCsvExportAdapterTest.java`
|
||||
|
||||
**Steps:**
|
||||
|
||||
1. Use `Locale.ROOT` at the three production identifier sites and at audited test comparisons.
|
||||
2. Replace `ETags` delimiter splitting with a quote-aware scanner. Split only on commas outside
|
||||
quoted opaque tags; malformed quoting yields no match. Keep wildcard and weak-tag semantics.
|
||||
3. Retain and observe the async `Future<?>`; unwrap `ExecutionException` only as required by the
|
||||
test's existing assertion contract.
|
||||
4. Replace empty cleanup catches with propagation or `IllegalStateException`/`UncheckedIOException`
|
||||
preserving the original cause.
|
||||
5. Replace implicit charset calls with `StandardCharsets.UTF_8`; replace `LocalDate.now()` test data
|
||||
with the fixed intended date or an explicit UTC clock.
|
||||
6. Convert byte-identical readability literals to text blocks and verify the exact expected strings.
|
||||
7. Run the focused tests from Task 1 and the affected owner test suites:
|
||||
|
||||
```bash
|
||||
./gradlew :application-core:test :shared-contract:test :adapter:inbound:web:test \
|
||||
:adapter:outbound:notification:test :adapter:outbound:fileserver:test \
|
||||
:adapter:outbound:httpclient:test :adapter:outbound:identifier:test \
|
||||
:sample-portfolio:test :app-bootstrap:test --console=plain
|
||||
```
|
||||
|
||||
## Task 3: Preserve Redis invariants and migrate Lettuce calls
|
||||
|
||||
**Owner leaf:** `adapter-outbound-cache-redis`
|
||||
|
||||
**Files:**
|
||||
|
||||
- Modify: `src/adapter/outbound/cache-redis/src/main/java/dev/caskeleton/adapter/outbound/cache/redis/RedisPrimitiveInvocation.java`
|
||||
- Modify: `src/adapter/outbound/cache-redis/src/main/java/dev/caskeleton/adapter/outbound/cache/redis/RedisTopologyCommandRuntime.java`
|
||||
- Modify: `src/adapter/outbound/cache-redis/src/main/java/dev/caskeleton/adapter/outbound/cache/redis/RedisVersionedSession.java`
|
||||
- Modify: `src/adapter/outbound/cache-redis/src/main/java/dev/caskeleton/adapter/outbound/cache/redis/VersionedRedisSessionStore.java`
|
||||
- Add: `src/adapter/outbound/cache-redis/src/test/java/dev/caskeleton/adapter/outbound/cache/redis/RedisPrimitiveInvocationTest.java`
|
||||
- Add: `src/adapter/outbound/cache-redis/src/test/java/dev/caskeleton/adapter/outbound/cache/redis/VersionedRedisSessionStoreTest.java`
|
||||
- Modify: `src/adapter/outbound/cache-redis/src/test/java/dev/caskeleton/adapter/outbound/cache/redis/LettuceRedisRuntimeServiceTest.java`
|
||||
- Modify: `src/adapter/outbound/cache-redis/src/test/java/dev/caskeleton/adapter/outbound/cache/redis/RedisVersionedSessionRepositoryTest.java`
|
||||
- Modify: `src/adapter/outbound/cache-redis/src/test/java/dev/caskeleton/adapter/outbound/cache/redis/RedisPrimitiveRuntimeServiceTest.java`
|
||||
|
||||
**Steps:**
|
||||
|
||||
1. Add characterization regressions proving a value-equal descriptor from a different catalog is
|
||||
rejected and the four session array records copy constructor inputs and accessor outputs. These
|
||||
should pass before implementation because they justify preserving the invariants; the compiler
|
||||
warnings are the RED executable contract for the suppression/migration work.
|
||||
2. Keep descriptor reference equality and add constructor-only
|
||||
`@SuppressWarnings("ReferenceEquality")` with an invariant rationale.
|
||||
3. Qualify every ambiguous nested `ExpectedKind` reference with its enclosing record.
|
||||
4. Keep Spring Session's `<T> T getAttribute(String)` signature and add method-only
|
||||
`TypeParameterUnusedInFormals` suppression.
|
||||
5. Preserve defensive copying for the four `VersionedRedisSessionStore` array records; apply exact
|
||||
`ArrayRecordComponent` suppressions to those records and the private test fake only.
|
||||
6. Convert canonical finite score strings to `BigDecimal`, build inclusive Lettuce `Range` values,
|
||||
and use typed `zcount` and `zrangebyscoreWithScores(..., Limit.create(...))` overloads. Extend the
|
||||
runtime proxy test to prove both overloads and their offset/count arguments.
|
||||
7. Replace one-shot `new SecureRandom()` with one static final instance.
|
||||
8. Run:
|
||||
|
||||
```bash
|
||||
./gradlew :adapter:outbound:cache-redis:test --console=plain
|
||||
./gradlew :adapter:outbound:cache-redis:compileJava \
|
||||
:adapter:outbound:cache-redis:compileTestJava --rerun-tasks --console=plain
|
||||
./gradlew :adapter:outbound:cache-redis:spotbugsTest --rerun-tasks --console=plain
|
||||
```
|
||||
|
||||
## Task 4: Preserve HTTP retry and notification ciphertext invariants
|
||||
|
||||
**Owner leaves:** `adapter-outbound-httpclient`, `adapter-outbound-persistence-jpa`
|
||||
|
||||
**Files:**
|
||||
|
||||
- Modify: `src/adapter/outbound/httpclient/src/main/java/dev/caskeleton/adapter/outbound/httpclient/OutboundRetryPolicy.java`
|
||||
- Modify: `src/adapter/outbound/httpclient/src/test/groovy/dev/caskeleton/adapter/outbound/httpclient/OutboundRetryPolicySpec.groovy`
|
||||
- Modify: `src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/notification/crypto/NotificationCiphertext.java`
|
||||
- Modify: `src/adapter/outbound/persistence-jpa/src/test/java/dev/caskeleton/adapter/outbound/persistence/notification/crypto/NotificationPayloadCryptoTest.java`
|
||||
|
||||
**Steps:**
|
||||
|
||||
1. Add a characterization test using two `OutboundRetryPolicy` instances on one thread: policy A
|
||||
context must not be visible to policy B, and `endCall()` must clear the owning context. It should
|
||||
pass before implementation and justifies preserving the instance field; the compile warning is
|
||||
the RED contract.
|
||||
2. Keep the instance `ThreadLocal`; add field-only `ThreadLocalUsage` suppression with the isolation
|
||||
reason.
|
||||
3. Add/strengthen tests proving `NotificationCiphertext` clones nonce/ciphertext inputs and
|
||||
accessors, compares arrays by content, hashes consistently, and never exposes bytes in
|
||||
`toString()`.
|
||||
4. Keep the record API and add exact record-level `ArrayRecordComponent` suppression.
|
||||
5. Run:
|
||||
|
||||
```bash
|
||||
./gradlew :adapter:outbound:httpclient:test --console=plain
|
||||
./gradlew :adapter:outbound:persistence-jpa:test --console=plain
|
||||
```
|
||||
|
||||
## Task 5: Migrate Jackson 3 messaging APIs
|
||||
|
||||
**Owner leaf:** `adapter-outbound-messaging`
|
||||
|
||||
**Files:**
|
||||
|
||||
- Modify: `src/adapter/outbound/messaging/src/main/java/dev/caskeleton/adapter/outbound/messaging/schema/LocalJsonSchemaRegistry.java`
|
||||
- Modify: `src/adapter/outbound/messaging/src/main/java/dev/caskeleton/adapter/outbound/messaging/envelope/DeterministicEnvelopeWriter.java`
|
||||
- Modify: `src/adapter/outbound/messaging/src/test/java/dev/caskeleton/adapter/outbound/messaging/envelope/LocalJsonSchemaRegistryTest.java`
|
||||
- Modify: `src/adapter/outbound/messaging/src/test/java/dev/caskeleton/adapter/outbound/messaging/envelope/JsonSchemaIntegrationEventEncoderTest.java`
|
||||
|
||||
**Steps:**
|
||||
|
||||
1. Extend existing tests to freeze text-node validation and canonical envelope bytes.
|
||||
2. Replace `isTextual()`/`textValue()` with `isString()`/`stringValue()`.
|
||||
3. Replace `createGenerator(output)` with
|
||||
`createGenerator(ObjectWriteContext.empty(), output, JsonEncoding.UTF8)`.
|
||||
4. Run:
|
||||
|
||||
```bash
|
||||
./gradlew :adapter:outbound:messaging:test --console=plain
|
||||
./gradlew :adapter:outbound:messaging:compileJava --rerun-tasks --console=plain
|
||||
```
|
||||
|
||||
## Task 6: Preserve legacy object storage and migrate provider APIs
|
||||
|
||||
**Owner leaves:** `application-core`, `adapter-outbound-objectstorage`, `sample-portfolio`,
|
||||
`app-bootstrap` architecture tests
|
||||
|
||||
**Files:**
|
||||
|
||||
- Modify: `src/application-core/src/main/java/dev/caskeleton/application/storage/ObjectStoragePort.java`
|
||||
- Modify the six Java files under
|
||||
`src/application-core/src/main/java/dev/caskeleton/application/storage/migration/`.
|
||||
- Modify: `src/adapter/outbound/objectstorage/src/main/java/dev/caskeleton/adapter/outbound/objectstorage/FilesystemObjectStorageAdapter.java`
|
||||
- Modify: `src/adapter/outbound/objectstorage/src/main/java/dev/caskeleton/adapter/outbound/objectstorage/S3ObjectStorageAdapter.java`
|
||||
- Modify: `src/adapter/outbound/objectstorage/src/main/java/dev/caskeleton/adapter/outbound/objectstorage/maintenance/LegacyObjectInspector.java`
|
||||
- Modify: `src/adapter/outbound/objectstorage/src/main/java/dev/caskeleton/adapter/outbound/objectstorage/maintenance/LegacyObjectAdoptionService.java`
|
||||
- Modify: `src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/application/poster/UploadPosterImageUseCase.java`
|
||||
- Modify: `src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/application/poster/migration/AdoptLegacyPosterImageUseCase.java`
|
||||
- Modify: `src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/bootstrap/objectstorage/PosterImageApiConfig.java`
|
||||
- Modify: `src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/adapter/inbound/web/controller/LegacyPosterImageController.java`
|
||||
- Modify: `src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/adapter/inbound/web/mapper/PosterWebMapper.java`
|
||||
- Modify: `src/application-core/src/test/java/dev/caskeleton/application/objectstorage/ObjectStorageArchitectureContractTest.java`
|
||||
- Modify: `src/adapter/outbound/objectstorage/src/main/java/dev/caskeleton/adapter/outbound/objectstorage/s3/S3AsyncClientFactory.java`
|
||||
- Modify: `src/adapter/outbound/objectstorage/src/objectStorageMinioFaultTest/java/dev/caskeleton/adapter/outbound/objectstorage/qualification/MinioManagedObjectFaultTest.java`
|
||||
- Modify: `src/adapter/outbound/objectstorage/build.gradle`
|
||||
- Modify: `src/adapter/outbound/objectstorage/gradle.lockfile` only if the toxiproxy dependency graph changes.
|
||||
- Modify audited URL, Mockito varargs, range parser, text-block, and legacy characterization tests.
|
||||
|
||||
**Steps:**
|
||||
|
||||
1. Add/retain lifecycle tests: `ObjectStoragePort`, `StoredObject`, and adapter-owned
|
||||
`ObjectStorageSettings` remain `forRemoval=true`; migration types remain deprecated but are no
|
||||
longer `forRemoval`.
|
||||
2. Change the six migration mechanism types plus `AdoptLegacyPosterImageUseCase` to plain
|
||||
`@Deprecated`. Add only exact `deprecation` suppressions at adoption implementation/configuration
|
||||
consumers.
|
||||
3. Add only the exact `removal` suppressions named by the design to legacy implementations,
|
||||
controller/mapper/wiring, characterization classes, and single receipt methods.
|
||||
4. Replace AWS `RetryPolicy`/old equal-jitter API with `StandardRetryStrategy`, half-jitter
|
||||
exponential backoff, exact max attempts, and `retryStrategy(...)`. Assert normal/throttling
|
||||
configuration in `S3AsyncClientFactoryTest`.
|
||||
5. Keep the existing `org.testcontainers:testcontainers-toxiproxy` dependency, switch to its
|
||||
Testcontainers 2 package, and use `ToxiproxyClient`/`Proxy` against an explicitly exposed proxy
|
||||
port. Preserve cut/restore MinIO semantics; update the leaf lock only if resolution actually
|
||||
changes.
|
||||
6. Replace `new URL(String)` with `URI.create(...).toURL()`.
|
||||
7. Replace Mockito's two-value varargs `thenReturn` with two chained single-value stubs.
|
||||
8. Replace test-only range splitting with an asserted single-hyphen boundary; keep fingerprint
|
||||
literal bytes identical when converting to a text block.
|
||||
9. Run:
|
||||
|
||||
```bash
|
||||
./gradlew :application-core:test :adapter:outbound:objectstorage:test \
|
||||
:sample-portfolio:test --console=plain
|
||||
./gradlew :adapter:outbound:objectstorage:check --console=plain
|
||||
./gradlew :app-bootstrap:test --tests '*CleanArchitectureTest' --console=plain
|
||||
./gradlew verifyDependencyLocks --console=plain
|
||||
```
|
||||
|
||||
10. If Docker is available, run the MinIO fault source-set task. If unavailable, record the exact
|
||||
environmental blocker; never suppress its deprecation to claim success.
|
||||
|
||||
## Task 7: Remove remaining mechanical Error Prone warnings
|
||||
|
||||
**Owner leaves:** `application-core`, `app-bootstrap`, `sample-portfolio`, and the exact test leaves
|
||||
from the audit inventory
|
||||
|
||||
**Files:**
|
||||
|
||||
- Modify: `IdempotencyExecutor.java`, `IdempotencySettings.java`,
|
||||
`SampleIdempotencySettings.java`, and matching tests.
|
||||
- Modify: `TracingSampleRateResolver.java` and `TestTaxonomyArchitectureTest.java`.
|
||||
- Modify: `CleanArchitectureTest.java`, `ManagementActuatorSecurityContractTest.java`,
|
||||
`ProblemDetailDisabledConfigTest.java`, and the serialization violation fixture.
|
||||
- Modify: `CreateWorkLogOutboxTest.java`, `WorkLogUseCasesTest.java`, and the remaining exact sample
|
||||
test warning locations.
|
||||
|
||||
**Steps:**
|
||||
|
||||
1. Replace five `Duration.ofHours(72)` sites with `Duration.ofDays(3)`.
|
||||
2. Add the missing Javadoc summary and render annotation names as `{@code @WebMvcTest}`.
|
||||
3. Add all 16 missing `@Override` annotations.
|
||||
4. Replace Boolean wrapper comparison with the direct literal/assertion form.
|
||||
5. Preserve the forbidden `new BigDecimal(double/float)` bytecode and add method-only
|
||||
`BigDecimalLiteralDouble` suppressions with fixture rationale.
|
||||
6. Replace the three test-only one-argument splits without changing each grammar:
|
||||
limit-bearing CSV handling, equivalent mapping-path scanning, and exact byte-range parsing.
|
||||
7. Run affected owner tests and rerun all compile tasks with Error Prone:
|
||||
|
||||
```bash
|
||||
./gradlew :application-core:test :app-bootstrap:test :sample-portfolio:test --console=plain
|
||||
./gradlew compileJava compileTestJava --rerun-tasks --console=plain
|
||||
```
|
||||
|
||||
## Task 8: Capture Redis lab expected failures and configure clean test JVMs
|
||||
|
||||
**Files:**
|
||||
|
||||
- Modify: `infra/redis-lab/test/redis-lab-contract.sh`
|
||||
- Add: `src/gradle/test-jvm-agents.gradle`
|
||||
- Modify: `src/build.gradle`
|
||||
|
||||
**Steps:**
|
||||
|
||||
1. Change `assert_fails` to capture stdout/stderr per invocation, require non-zero status, assert the
|
||||
exact expected diagnostic with no extra lines, and print capture only on mismatch.
|
||||
2. Run `bash -n infra/redis-lab/test/redis-lab-contract.sh`, then run the real Redis lab Gradle/shell
|
||||
contract and verify successful output contains no leaked `redis-lab:` child diagnostics.
|
||||
3. Add a dedicated `mockitoAgent` configuration per Java test project and a relocatable
|
||||
`CommandLineArgumentProvider` in `src/gradle/test-jvm-agents.gradle`. Require exactly one
|
||||
`mockito-core` jar and emit `-javaagent:<absolute jar>` plus test-only `-Xshare:off`.
|
||||
4. Apply the script once from the root build and wire every ordinary/custom `Test` task without
|
||||
changing production JVM arguments.
|
||||
5. Run representative Mockito-heavy app-bootstrap, Redis, object-storage, and messaging tests and
|
||||
verify no self-attachment/CDS warning is printed.
|
||||
|
||||
## Task 9: Enable warning-zero blocking gates
|
||||
|
||||
**Files:**
|
||||
|
||||
- Modify: `src/build.gradle`
|
||||
- Modify: `src/app-bootstrap/build.gradle`
|
||||
- Modify: `.github/workflows/ci-quality-gates.yml`
|
||||
|
||||
**Steps:**
|
||||
|
||||
1. First run every `JavaCompile` task with `-Xlint:deprecation` and `-Xlint:unchecked`; resolve every
|
||||
remaining diagnostic at the exact source owner.
|
||||
2. Add `-Werror`, `-Xlint:deprecation`, and `-Xlint:unchecked` to every leaf `JavaCompile` task while
|
||||
retaining Error Prone.
|
||||
3. Remove root `checkstyleTest` and `spotbugsTest` `ignoreFailures=true`.
|
||||
4. Remove app-bootstrap `sampleOffTest`, `functionalTest`, and `conditionalTransportTest`
|
||||
Checkstyle/SpotBugs ignore overrides. Keep only `quarantineTest` non-blocking.
|
||||
5. Add `--warning-mode=fail` to the blocking `quality-gates` Gradle invocation.
|
||||
6. Run:
|
||||
|
||||
```bash
|
||||
./gradlew checkstyleTest spotbugsTest --rerun-tasks --console=plain
|
||||
./gradlew check --warning-mode=fail --no-daemon --console=plain
|
||||
```
|
||||
|
||||
## Task 10: Fresh repository verification, review, and Wiki capture
|
||||
|
||||
**Files:**
|
||||
|
||||
- Modify: `docs/superpowers/plans/2026-08-02-warning-zero-build-refactoring.md` only if execution
|
||||
evidence exposes a plan correction.
|
||||
- Modify external Wiki capture:
|
||||
`/home/donghyeon/workspace/ai-tool/llm-wiki-private/raw/branch-notes/main.md`
|
||||
and `raw/errors/build-success-warning-debt-2026-08-02.md`.
|
||||
|
||||
**Steps:**
|
||||
|
||||
1. Run owner-focused tests for every changed leaf.
|
||||
2. Run repository verification 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
|
||||
```
|
||||
|
||||
3. Verify the gate matrix, wrapper, shell syntax, XML findings/skips, and diff:
|
||||
|
||||
```bash
|
||||
bash .github/scripts/verify-gate-matrix.sh
|
||||
bash .github/scripts/verify-gradle-wrapper.sh .
|
||||
bash -n infra/redis-lab/test/redis-lab-contract.sh
|
||||
git diff --check
|
||||
```
|
||||
|
||||
4. Scan the fresh build log for `warning:`, deprecated/unchecked `Note:`, SpotBugs non-zero output,
|
||||
OpenJDK/CDS warnings, Mockito self-attachment, and leaked expected-negative Redis diagnostics.
|
||||
5. Confirm the skipped-test XML inventory is exactly the five approved optional-adapter contract
|
||||
cases and no qualification source set skipped.
|
||||
6. Dispatch independent code review over behavior fixes, legacy/provider migrations, and
|
||||
Gradle/test-noise gates. Apply only evidence-backed findings and rerun affected/full gates.
|
||||
7. Update the mandatory Wiki branch/error notes with changed files, commands, results, suppression
|
||||
inventory, blocked environment-only qualifications, and evidence grade. Run per-file Wiki lint;
|
||||
retain the known `main.md` naming-policy conflict without weakening either policy.
|
||||
8. Report success only if the clean build is exit zero and the final log is warning/noise clean.
|
||||
@@ -0,0 +1,56 @@
|
||||
# Web Security Boundary Implementation Plan
|
||||
|
||||
> **Execution:** Follow test-driven development and request an independent read-only review before
|
||||
> advancing to Redis session/CSRF.
|
||||
|
||||
**Goal:** Make JWT/JWKS and CORS filter-boundary behavior hermetic, release-blocking, and impossible
|
||||
to skip silently.
|
||||
|
||||
**Architecture:** Tests remain in inbound-web, use only existing dependencies, and cross the real
|
||||
Spring Security filter chain. A tagged Gradle task isolates them from the ordinary unit suite.
|
||||
|
||||
**Tech Stack:** Java 21, Spring Boot 4.0.0, Spring Security 7, Nimbus JOSE JWT, JDK HttpServer,
|
||||
MockMvc, Gradle 9.
|
||||
|
||||
### Task 1: Dedicated No-Skip Test Gate
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/adapter/inbound/web/build.gradle`
|
||||
|
||||
- [x] Register `webSecurityBoundaryTest` over `sourceSets.test` with tag inclusion, no-discovery
|
||||
failure, no up-to-date reuse, UTC, and a root-suite skipped-count guard.
|
||||
- [x] Exclude `security-boundary` from ordinary `test` and require the dedicated task from `check`.
|
||||
- [x] Confirm 13 tagged tests are discovered with zero skips and no dependency/lock entry is added.
|
||||
|
||||
### Task 2: JWT/JWKS RED Contracts
|
||||
|
||||
**Files:**
|
||||
- Create: `src/adapter/inbound/web/src/test/java/dev/caskeleton/adapter/inbound/web/auth/JwtJwksSecurityFilterIntegrationTest.java`
|
||||
|
||||
- [x] Add a loopback OIDC discovery/JWKS server with request counters and deterministic 503 mode.
|
||||
- [x] Add RS256 token generation using ephemeral keys and conspicuous secret sentinels.
|
||||
- [x] Prove lazy startup and valid bearer-to-principal conversion.
|
||||
- [x] Prove exact expiry, issuer, audience, signature, unknown-kid, and JWKS-outage envelopes/headers.
|
||||
- [x] Prove same-context recovery after a first-request JWKS 503 and prove mismatched discovery
|
||||
metadata reaches the safe 500 `INTERNAL_AUTH_MISCONFIGURATION` filter boundary.
|
||||
- [x] Run the dedicated task and record RED: unknown kid was classified as signature failure and a
|
||||
first-request JWKS 503 escaped as `JwtDecoderInitializationException`/`AuthenticationServiceException`.
|
||||
|
||||
### Task 3: CORS RED Contracts
|
||||
|
||||
**Files:**
|
||||
- Create: `src/adapter/inbound/web/src/test/java/dev/caskeleton/adapter/inbound/web/auth/CorsSecurityFilterIntegrationTest.java`
|
||||
|
||||
- [x] Prove approved credentialed preflight bypasses bearer authentication and emits exact headers.
|
||||
- [x] Prove denied origin, disabled CORS, wildcard-without-credentials, and approved actual-origin behavior.
|
||||
- [x] Assert bounded `Vary` behavior and no reflection of an unapproved sentinel origin.
|
||||
- [x] Run the dedicated task: all five CORS filter-boundary contracts passed without production changes.
|
||||
|
||||
### Task 4: Minimal Production Fixes and Verification
|
||||
|
||||
- [x] If RED exposes a production mismatch, change only the owning classifier/security configuration
|
||||
and keep stable error-code/header contracts intact.
|
||||
- [x] Run `webSecurityBoundaryTest`, ordinary inbound-web `test`, module static analysis, `check`,
|
||||
dependency-lock verification, architecture verification, and `git diff --check`.
|
||||
- [x] Request an independent read-only review; add the requested same-context recovery and non-I/O
|
||||
initialization-failure contracts, and bind the loopback server to an explicit IPv4 address.
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,838 @@
|
||||
# Redis Wrapper and Typed API — repository adaptation and delivery status
|
||||
|
||||
- **Design:** `docs/superpowers/specs/2026-08-07-redis-wrapper-typed-api-design.md`
|
||||
- **Plan:** `docs/superpowers/plans/2026-08-07-redis-wrapper-typed-api-implementation-plan.md`
|
||||
- **Status date:** 2026-08-07
|
||||
- **All 27 tasks delivered.** Sections 13–24 record what each one decided and what the topology
|
||||
lanes found; `docs/redis/support-matrix.md` records which test produced which evidence.
|
||||
|
||||
---
|
||||
|
||||
## 1. Why the structure differs from the plan
|
||||
|
||||
The design and plan were written without the target repository attached, so they assume a
|
||||
`backend-skeleton/` root with twelve standalone Gradle projects under `modules/redis/`, Kotlin DSL
|
||||
build files, and the `io.backend.skeleton.redis` package root. The package README anticipates exactly
|
||||
this and instructs the implementer to keep the structural contract while conforming to whatever
|
||||
stronger rules the real repository already enforces.
|
||||
|
||||
This repository has three such rules, and all of them outrank the plan's file layout:
|
||||
|
||||
1. `src/config/architecture/modules.json` is a fail-closed registry of **exactly 19 leaf modules**,
|
||||
re-validated by `src/settings.gradle` on every configuration. Adding twelve Gradle projects would
|
||||
violate HARD-STOP condition 5 in `AGENTS.md`.
|
||||
2. The build is Groovy DSL with `dependencyLocking(STRICT)`, so the plan's `libs.versions.toml`
|
||||
entries and its Spring Data Redis 4.1 / Lettuce 7.6 pins cannot be introduced without regenerating
|
||||
lock state. The repository is on Spring Boot 4.0.0 with **Lettuce 6.8.1**.
|
||||
3. The package root is `dev.caskeleton`, not `io.backend.skeleton`.
|
||||
|
||||
The SDK therefore lives inside the already-registered `adapter:outbound:cache-redis` leaf, and each
|
||||
designed module is a package. What the separate Gradle projects would have enforced —
|
||||
dependency direction and driver containment — is enforced instead by
|
||||
`RedisSdkModuleBoundaryTest`, which reads the source tree and fails on a forbidden import.
|
||||
|
||||
### Module mapping
|
||||
|
||||
| Design module | Package under `dev.caskeleton.adapter.outbound.cache.redis.sdk` |
|
||||
| --- | --- |
|
||||
| `redis-core-api` | `api`, `api.key`, `api.codec`, `api.command`, `api.error`, `api.operations`, `api.reactive` |
|
||||
| `redis-core-lettuce` | `lettuce.codec`, `lettuce.command`, `lettuce.connection`, `lettuce.observability` |
|
||||
| `redis-spring-boot-starter` | `config` |
|
||||
| `redis-cluster` | `cluster` |
|
||||
| `redis-programmability` | `programmability` |
|
||||
| `redis-raw-gateway` | `raw` |
|
||||
| `redis-admin-plane` | `admin` |
|
||||
| `extensions/*` | `extensions.json`, `extensions.search`, `extensions.timeseries`, `extensions.probabilistic` |
|
||||
| `redis-testkit` | `src/test` and the existing `redisTest` source set |
|
||||
|
||||
### Other adaptations, and the reason for each
|
||||
|
||||
| Plan says | Repository does | Why |
|
||||
| --- | --- | --- |
|
||||
| `backend.redis.*` properties | `ca-skeleton.capabilities.redis-sdk.*` | Matches the existing capability property namespace and avoids colliding with `app.cache.redis`. |
|
||||
| `RedisEnvelope` is a record with a `byte[]` component | Value class with the same accessors | ErrorProne `ArrayRecordComponent` is a blocking check in this build. |
|
||||
| Jackson-based YAML policy loader | Explicit strict reader for a closed YAML subset | No Jackson or SnakeYAML on the main compile classpath, and a general YAML engine would accept anchors, merges, and duplicate keys inside a security policy file. |
|
||||
| `VersionedJsonCodec` maps objects reflectively | Frames a versioned JSON envelope around a caller-supplied `RedisPayloadCodec` | Same guarantee — schema id, version, size ceiling, hard failure on an unknown version — without an object mapper the module cannot depend on. |
|
||||
| Each task ends with `git commit` | No commits | `AGENTS.md` commit policy is `human-only`. |
|
||||
| Gradle tasks `redis72Test` … `cluster82Test` | Not registered | They belong to Task 8's testkit half and Task 26; both need Docker-backed Testcontainers, which Milestone A does not reach. |
|
||||
|
||||
---
|
||||
|
||||
## 2. Task status
|
||||
|
||||
| Task | Title | Status |
|
||||
| --- | --- | --- |
|
||||
| 1 | Module graph and shared quality rules | **Done** as a package graph plus `RedisSdkModuleBoundaryTest` |
|
||||
| 2 | Command policy catalog and metadata diff | **Done** |
|
||||
| 3 | Version, topology, risk, permit, budget models | **Done** |
|
||||
| 4 | Key namespace and slot-safe typed keys | **Done** |
|
||||
| 5 | Codec registry and versioned envelope | **Done** |
|
||||
| 6 | Stable error model and ambiguous execution | **Done** |
|
||||
| 7 | Sync and reactive public API with parity test | **Done** |
|
||||
| 8 | Properties, capability probe, connection isolation, permit authority | **Done** except the Testcontainers topology environments and their Gradle tasks |
|
||||
| 9 | Policy-aware executor and observability | **Done** |
|
||||
| 10 | String and Key/TTL operations, blocking and reactive | **Done** against the in-memory gateway; no real-server evidence |
|
||||
| 11 | Hash operations and the 7.4 field-TTL version gate | **Done** against the in-memory gateway; no real-server evidence |
|
||||
| 12 | Set and Sorted Set operations, blocking and reactive | **Done** against the in-memory gateway; no real-server evidence |
|
||||
| 13 | List operations and the bounded blocking lane | **Done** against the in-memory gateway; no real-server evidence |
|
||||
| 14 | Bitmap, bitfield, HyperLogLog, and geospatial operations | **Done** against the in-memory gateway; no real-server evidence |
|
||||
| 15 | Batch and pipeline | **Done** against the in-memory gateway; no real-server evidence |
|
||||
| 16 | Stream | **Done** against the in-memory gateway, including the Redis 8.2 deletion capability; `XNACK` (8.8) deferred, see §13 |
|
||||
| 17 | Pub/Sub and sharded Pub/Sub | **Done** against the in-memory bus; no real-server evidence |
|
||||
| 18–19 | Sentinel failover certainty, Cluster slot/redirect/topology | **Done** as pure logic with unit evidence; the fault-injection lane is Task 26 |
|
||||
| 21 | Registered scripts and functions | **Done** against the in-memory gateway; no real-server evidence |
|
||||
| 20 | Transactions | **Done**, with the fixture reworked to defer inside a MULTI window, see §24 |
|
||||
| 22 | Approved raw gateway | **Done** against the in-memory gateway; no real-server evidence |
|
||||
| 23 | Isolated admin plane | **Done** against the in-memory gateway; no real-server evidence |
|
||||
| 24–25 | JSON, Search, Time Series, Probabilistic extensions | **Done** against the in-memory gateway; no real-module evidence |
|
||||
| 26–27 | Topology/fault/ACL/performance harness, CI matrix and docs gates | **Done** — all three lanes have produced evidence on 7.4, see §21–§23 |
|
||||
|
||||
`RedisSdkModuleBoundaryTest.NOT_YET_IMPLEMENTED_MODULES` is the machine-checked version of the
|
||||
"not started" rows: the test fails if a listed package appears without the list being updated, and
|
||||
fails if an unlisted one is missing.
|
||||
|
||||
---
|
||||
|
||||
## 3. What Milestone A actually guarantees
|
||||
|
||||
- Every command the SDK will ever run is classified in
|
||||
`src/main/resources/redis-sdk/redis-command-policy.yml`. An unclassified command is refused by
|
||||
`RedisCommandCatalog`, so a Redis upgrade cannot make a new command reachable by default.
|
||||
- `KEYS`, `FLUSHALL`, `FLUSHDB`, `SHUTDOWN`, `DEBUG`, `EVAL`, `CONFIG SET`, and the deprecated
|
||||
command names are `BLOCKED` with ACL account `NONE`.
|
||||
- R2 commands cannot execute without both an issued permit and an `OperationBudget`, and a permit the
|
||||
caller implemented itself fails provenance verification.
|
||||
- Sync and reactive typed API surfaces are mechanically proven to be in parity.
|
||||
- Metric and trace tags are a closed low-cardinality set with no key, field, member, or value in it.
|
||||
- A write that timed out is reported as `RedisAmbiguousExecutionException` with `retryable=false`,
|
||||
and `RedisFailureMetadata` rejects the retryable-and-ambiguous combination at construction.
|
||||
|
||||
## 4. What Milestone A does not guarantee
|
||||
|
||||
- No command has been executed against a real Redis server by this work. Every test is a unit or
|
||||
contract test over fakes; the contract suites the plan defines for Tasks 10–17 do not exist yet.
|
||||
- The typed operation interfaces have no implementation, so `RedisOperations` cannot be wired into a
|
||||
Spring context yet. `RedisSdkSettings` is bound but no bean registration reads it.
|
||||
- Cluster slot calculation is a caller-supplied function; the CRC16 implementation is Task 19.
|
||||
## 5. Cleanup of everything the design does not specify
|
||||
|
||||
The leaf previously carried five Redis capabilities that this design does not describe — semantic
|
||||
cache, session, request-replay idempotency, soft lease, and edge rate limit — together with their
|
||||
evidence and readiness governance. All of it is removed, so the Redis surface is now exactly the
|
||||
SDK.
|
||||
|
||||
| Removed | Scale |
|
||||
| --- | --- |
|
||||
| `cache-redis` non-SDK sources, tests, Lua programs, and the `redisTest` evidence source set | 188 main + 105 test + 18 evidence Java files, 52 resources |
|
||||
| `cache-redis/build.gradle` | 626 lines → 22; ~50 evidence/readiness lanes gone |
|
||||
| `app-bootstrap` Redis wiring, health contributor, material providers, `redisCompositionTest` source set | 15 files plus its Gradle tasks and configurations |
|
||||
| `application-core/src/redisPolicyContractTest` | 1 file plus its source set |
|
||||
| Root `build.gradle` Redis readiness/evidence/CI-matrix governance | 1,420 lines |
|
||||
| `config/redis/`, `gradle/redis-test-images.properties`, `infra/redis-lab/`, `.github/workflows/redis-production-readiness.yml` | removed |
|
||||
| `ci-quality-gates.yml` / `ci-gate-matrix.yml` | `redis-standalone` job retargeted to `redis-sdk` |
|
||||
|
||||
Kept deliberately: `shared-contract`'s `EdgeRateLimitPort` and its provider-neutral contract test.
|
||||
It is a rate-limit port, not a Redis type, and the design's exclusion list covers business policy
|
||||
rather than application ports.
|
||||
|
||||
Verified after the cleanup: `./gradlew test`, `verifyCleanArchitectureDependencies`,
|
||||
`verifyEnvKeys`, `verifyDependencyLocks` all pass; `verify-gate-matrix.sh` reports 27 gates OK.
|
||||
Dependency locks were regenerated for every module.
|
||||
|
||||
## 6. Task 10 — decisions a reviewer should check
|
||||
|
||||
The string and key/TTL operations landed in `sdk.lettuce.operations`, which is the package form of
|
||||
the plan's `redis-core-lettuce/.../lettuce/operations`. Five things differ from a literal reading of
|
||||
the plan, each for a stated reason.
|
||||
|
||||
| Decision | Why |
|
||||
| --- | --- |
|
||||
| A narrow `RedisCommandGateway` seam sits between the typed operations and Lettuce; `LettuceRedisCommandGateway` is the only class that touches the driver. | The plan's contract suites run on Testcontainers, which this environment has no lane for. The seam lets the whole policy path — catalog, permit provenance, budget, admission order, decode — be proven deterministically, and it keeps driver containment real rather than asserted. It is not a substitute for the real-server evidence Task 26 owns. |
|
||||
| Where design section 10 gives an R2 method only a permit (`multiGet`, `delete`, `unlink`, `rename`, `scan`) or only a budget (`append`, `getRange`, `setRange`), the SDK fills the missing half. | `CommandPolicyGuard` requires both for every R2 command. The caller-supplied half always wins; the other comes from `RedisOperationLimits` or a permit the SDK itself holds. Without this, half the designed R2 surface could not be admitted at all. |
|
||||
| Increment-with-initial-TTL runs a registered Lua script, and the SDK loads that script itself inside the guarded `EVALSHA` invocation. | Redis 7.2–8.2 has no `INCR` variant carrying an expiry, and both two-command sequences leak a permanent counter on a crash. The `SCRIPT LOAD` that resolves the digest is therefore *not* separately admitted by the guard — it travels under the `EVALSHA` admission with the same `registered-script` permit and script budget. Proper script registration is Task 20/21's `programmability` module; this is the narrowest thing that makes the operation correct in the meantime. |
|
||||
| No `INCREX` version-gated path. | No shipped Redis version has the command, so it is in neither the policy catalog nor `RedisCapability`. Adding a gate for a command that does not exist would be untestable. |
|
||||
| `expire`/`expireAt` report `ABSENT` only when the condition was `ALWAYS`. | Redis answers `0` both for a missing key and for an unmet condition. With `ALWAYS` the only possible cause is a missing key; with any other condition the SDK reports `CONDITION_NOT_MET` rather than guessing. A non-positive TTL is refused outright instead of silently deleting the key. |
|
||||
|
||||
Coverage: 30 new tests (`RedisValueOperationsContractTest`, `RedisKeyOperationsContractTest`) over
|
||||
permit provenance, budget ceilings, atomic counter creation, script reload after `NOSCRIPT`,
|
||||
namespace-bounded paging, and blocking/reactive agreement. `lettuce/operations` is registered in
|
||||
`RedisSdkModuleBoundaryTest.DESIGNED_MODULES`.
|
||||
|
||||
## 7. Task 11 — the field-TTL version gate
|
||||
|
||||
The gate design section 10.2 asks for is applied in two independent places, because either one alone
|
||||
is weaker than it looks.
|
||||
|
||||
- `LettuceRedisHashFieldExpirationOperations.ifSupported(...)` returns empty below Redis 7.4, so a
|
||||
composition root has nothing to inject and a caller cannot hold the API at all. This is the
|
||||
"bean is absent on 7.2" property the plan's Redis 7.2 test asserts.
|
||||
- `HEXPIRE`, `HPEXPIRE`, `HPERSIST`, `HTTL`, and `HPTTL` carry `minimum-version: "7.4"` in the policy
|
||||
catalog, so `CommandPolicyGuard` refuses them on an older server even for a hand-built instance.
|
||||
`guardRefusesFieldExpiryOnAnOlderServer` proves that second layer by forcing an instance into
|
||||
existence against a 7.2 server and watching the guard reject it.
|
||||
|
||||
`entries` is R2 with a caller-supplied permit *and* budget, exactly as designed — it is the one hash
|
||||
method the design gives both, so nothing is filled in for it. `HSCAN` gets the Task 10 treatment: an
|
||||
SDK `cursor-scan` permit and a budget derived from the requested page, because
|
||||
`scan(HashKey, ScanRequest)` carries neither. `HGETALL` and `HSCAN` replies are measured against the
|
||||
budget before decoding, so an oversized hash is refused rather than materialised.
|
||||
|
||||
One Lettuce accommodation is worth knowing about: its only batched `HSET` takes a `Map`, which for a
|
||||
`byte[]`-keyed connection means identity hashing. The seam therefore passes two positional lists and
|
||||
`LettuceRedisCommandGateway.hashPutAll` is the single place that builds the map — never reading from
|
||||
it, only iterating — with the ErrorProne check suppressed there and nowhere else.
|
||||
|
||||
## 8. Task 12 — the range commands are encoded, not borrowed
|
||||
|
||||
Design section 10.5 requires `rangeByScore`, `rangeByLex`, and a descending `rangeByRank`. Lettuce
|
||||
6.8 has no typed `ZRANGE ... BYSCORE / BYLEX / REV`; its only typed paths are the deprecated
|
||||
`ZRANGEBYSCORE`, `ZREVRANGEBYSCORE`, `ZRANGEBYLEX`, `ZREVRANGEBYLEX`, and `ZREVRANGE`.
|
||||
|
||||
Those five stay `BLOCKED`, exactly like `SETNX`, `GETSET`, `HMSET`, `RPOPLPUSH`, and `GEORADIUS`.
|
||||
`LettuceRedisCommandGateway` encodes the modern command itself — `ZRANGE key min max
|
||||
BYSCORE|BYLEX [REV] LIMIT offset count [WITHSCORES]` — through Lettuce's typed `dispatch` with a
|
||||
fixed `CommandType.ZRANGE`, a fixed output, and arguments built from the already-rendered key. Every
|
||||
range read therefore declares `ZRANGE` to the guard and sends `ZRANGE` on the wire, so the ACL
|
||||
account and the catalog drift gate stay aligned with reality.
|
||||
|
||||
This is not the forbidden raw-command surface: there is no method anywhere that accepts a command
|
||||
name, and the encoding lives in the one class that is already allowed to know the driver.
|
||||
|
||||
Everything else in Task 12 follows Task 10's rules. `SMEMBERS` has no method at all — the API offers
|
||||
`scan` or permit-and-budget set algebra, and a test asserts no whole-set reader exists.
|
||||
`SRANDMEMBER`, `SSCAN`, and `ZSCAN` get an SDK permit plus a derived budget because their signatures
|
||||
carry neither; `SMOVE` takes the caller's multi-key permit; `SDIFF`/`SINTER`/`SUNION` and every range
|
||||
read take both from the caller, and the reply is measured against the budget before it is decoded.
|
||||
|
||||
## 9. Task 13 — the blocking lane
|
||||
|
||||
`LettuceRedisBlockingListOperations` takes its own `RedisCommandGateway`, which the composition root
|
||||
binds to a connection borrowed from `RedisConnectionKind.BLOCKING`. That parameter is the structural
|
||||
form of design section 10.3's "separate bean, dedicated pool": a command that occupies its
|
||||
connection until the server answers cannot be issued down the lane ordinary traffic shares, and the
|
||||
type system is what stops it rather than a convention.
|
||||
|
||||
An unbounded wait is impossible on three independent levels: the request always declares its block,
|
||||
`ListOperationRequests` refuses a non-positive one before building anything, and
|
||||
`CommandPolicyGuard` refuses a block above the configured ceiling and sets the client timeout to the
|
||||
block plus `TimeoutProfile.BLOCKING_MARGIN`. All three are asserted.
|
||||
|
||||
`BLMOVE` needs both authorisations and the design gives the caller only one, so the caller's
|
||||
multi-key permit is verified in the operations layer while the SDK supplies the `blocking-pop`
|
||||
permit the guard demands. Crossing two keys and occupying a connection are separate decisions and
|
||||
the caller must still hold the first.
|
||||
|
||||
## 10. Task 14 — the ceiling that matters
|
||||
|
||||
A single `SETBIT` at an arbitrary offset allocates the whole prefix, so an unchecked offset is a
|
||||
memory-exhaustion primitive rather than a write. `RedisOperationLimits.maxBitmapOffset` bounds every
|
||||
bit offset — `GETBIT`, `SETBIT`, and each `BITFIELD` subcommand — before a command is built, and a
|
||||
negative offset is refused outright.
|
||||
|
||||
`BITOP`, `PFCOUNT`, `PFMERGE`, and `GEOSEARCHSTORE` take the caller's multi-key permit; `BITCOUNT`,
|
||||
`BITPOS`, `BITFIELD`, and `GEOSEARCH` take the caller's budget with an SDK permit. A geo search is
|
||||
bounded three ways — its own `count`, the collection ceiling, and the caller's budget measured
|
||||
against the reply before decoding.
|
||||
|
||||
## 11. Task 15 — the two decisions the design left open
|
||||
|
||||
`RedisBatch` exposes only `size()`, `keys()`, and `requestBytes()`, so it is opaque: nothing in the
|
||||
public contract lets a caller put commands into one. The SDK therefore owns both the concrete batch
|
||||
and the only way to fill it, and two questions had to be answered.
|
||||
|
||||
**What the builder covers.** `LettuceRedisBatch.Builder` covers the string, key, and hash surfaces
|
||||
rather than mirroring all ~80 typed methods. Those are what pipelining is actually used for, each
|
||||
extra method is one delegating line onto the existing request factories, and widening it later is
|
||||
mechanical rather than a redesign. A batch built anywhere else is refused.
|
||||
|
||||
**Whether R2 commands may be batched.** They may, carrying their own permit and budget exactly as
|
||||
they do alone. `BatchOptions` has no permit field, so the alternative was an R1-only batch — which
|
||||
would have blocked the case where saving a round trip matters most. Both ceilings apply and the
|
||||
smaller wins: the guard refuses an item that broke its own budget before the batch ceiling is even
|
||||
checked.
|
||||
|
||||
Four properties are enforced rather than documented. Every item is admitted **before** any command
|
||||
is sent, so one refused item cancels the batch instead of leaving it half-applied. Input index is
|
||||
result index, failure or not. Items fail independently — `hasPartialFailure` is the caller's signal,
|
||||
not an exception. And there is no retry path in the class at all, so a failed write is never
|
||||
re-sent.
|
||||
|
||||
## 12. Task 17 — a subscription is not a command
|
||||
|
||||
Publishing goes through the guard like anything else. Subscribing does not: it has no reply to bound
|
||||
and no timeout to apply, it occupies its connection for as long as it lives, and it therefore has
|
||||
its own seam — `RedisPubSubGateway`, bound to a connection borrowed from
|
||||
`RedisConnectionKind.PUBSUB`. A long-lived listener can never sit on the lane ordinary commands use.
|
||||
|
||||
What the guard would have checked is checked in `PubSubOperationRequests` instead: every channel and
|
||||
pattern must belong to the process namespace, an empty subscription is refused, and a pattern
|
||||
subscription demands the `pattern-subscribe` permit because the server decides how much a pattern
|
||||
matches.
|
||||
|
||||
Lifecycle is the part that leaks if it is only documented. The blocking API returns an
|
||||
`AutoCloseable` `Subscription`; the reactive API returns a `Flux` whose cancellation closes the
|
||||
driver handle. Both are asserted against a bus that reports how many subscriptions are still open,
|
||||
so an abandoned subscriber releasing its connection is a test, not a claim.
|
||||
|
||||
Sharded Pub/Sub is gated exactly like per-field expiry: `ifSupported` yields nothing below Redis
|
||||
7.0, and `SPUBLISH` carries the same minimum in the catalog so the guard refuses it independently.
|
||||
|
||||
### Still outstanding
|
||||
|
||||
`application.yml`, `.env`, and `docs/registries/env-keys.yaml` still carry the property blocks of
|
||||
the five removed capabilities. They bind nothing and the build is green with them present, but they
|
||||
are dead configuration and should go in the same sweep that removes the corresponding capability
|
||||
sections.
|
||||
|
||||
## 13. Task 16 — a stream entry, a payload field, and one command that had to be encoded
|
||||
|
||||
**One payload field.** `StreamKey<V>` carries exactly one payload codec and `StreamRecord<V>`
|
||||
exactly one value, so the SDK writes exactly one field, named `payload` in
|
||||
`StreamOperationRequests` and nowhere else. An entry that comes back with any other shape is
|
||||
refused rather than half-decoded: a foreign producer's record is an anomaly the caller has to see,
|
||||
not something to silently truncate into a `StreamRecord`.
|
||||
|
||||
**`XREAD` forced a catalog distinction.** `BLPOP` has no non-blocking form, so a request that omits
|
||||
its block is a defect. `XREAD` does have one — the same command name is an ordinary bounded read
|
||||
without `BLOCK`. The catalog previously modelled only "blocking", which would have meant either
|
||||
rejecting every non-blocking stream read or excusing the stream reads from the rule that nothing
|
||||
waits forever. Both were wrong, so `optional-block` was added to the policy schema and
|
||||
`RedisCommandPolicy.requiresServerBlock()` now separates the two. `XREAD` and `XREADGROUP` are the
|
||||
only commands that carry it. The blocking bean still takes a non-nullable `Duration`, and the guard
|
||||
still refuses a non-positive block or one over the configured ceiling.
|
||||
|
||||
**A group read has exactly two legal offsets.** `NewForGroup` and `PendingForConsumer` are accepted;
|
||||
`After` and `Latest` are refused. Reading a group from an arbitrary identifier would hand a consumer
|
||||
entries the group already distributed elsewhere without moving the pending list — a duplicate
|
||||
delivery the caller did not ask for. The mirror rule holds for the group-free read, which refuses
|
||||
the two group offsets.
|
||||
|
||||
**`XAUTOCLAIM` is encoded, not borrowed.** Lettuce's typed `xautoclaim` returns `ClaimedMessages`,
|
||||
which drops the third reply element: the identifiers that were pending but no longer exist in the
|
||||
stream. `ClaimResult.deletedIds` is part of the SDK contract precisely because a consumer that
|
||||
cannot see that list keeps sweeping the same tombstones forever. The command is therefore built in
|
||||
`LettuceRedisCommandGateway` with `NestedMultiOutput`, the same precedent set by the sorted-set
|
||||
ranges in §8 — the command declared to the guard is still the command on the wire, and no method
|
||||
accepts a command name.
|
||||
|
||||
**Permits and budgets.** `XTRIM` runs under `bounded-collection-write`, the ranges under
|
||||
`bounded-collection-read`, both reads under the new `stream-read` policy, and `XPENDING`/`XAUTOCLAIM`
|
||||
under `stream-recovery`. None of the design's stream signatures carry a caller permit, so all four
|
||||
are SDK permits; the caller-supplied bound is the mandatory `count`, which becomes both the guard's
|
||||
budget and the ceiling checked against `maxCollectionElements`. There is no "read the whole stream"
|
||||
call that can be written against this API.
|
||||
|
||||
**Redis 8.2 deletion landed; 8.8 `XNACK` did not.** `XACKDEL`/`XDELEX` are behind
|
||||
`LettuceRedisStreamDeletionOperations.ifSupported(...)`, gated exactly like hash field expiry — the
|
||||
capability probe decides whether a bean exists, and the catalog's 8.2 minimum refuses a hand-built
|
||||
one. `XNACK` is deliberately not implemented: the pinned Lettuce 6.8.2 has no typed form for it, and
|
||||
unlike `XAUTOCLAIM` its wire format cannot be verified against a driver or a released server, so
|
||||
encoding it by hand would be inventing a protocol rather than adapting one. The capability, the
|
||||
catalog entry, and the 8.8 minimum stay in place; the bean is the only missing piece and should be
|
||||
added when the command is available in the driver or in a released server.
|
||||
|
||||
## 14. Tasks 18–19 — the parts that do not need a cluster to be true
|
||||
|
||||
Both tasks are specified against real Sentinel and Cluster environments, which this repository does
|
||||
not yet have a lane for. What landed is the half that is decidable without one, and it is the half
|
||||
the rest of the SDK depends on.
|
||||
|
||||
**The slot calculator is a pre-flight check, not a redirect handler.** `RedisSlotCalculator`
|
||||
computes CRC-16/XMODEM over the hash tag exactly as Redis does, so `CommandPolicyGuard` can refuse a
|
||||
cross-slot multi-key command before it is written. A server-side `CROSSSLOT` would arrive after the
|
||||
request left the process, which is precisely the outcome the guard exists to prevent. The seam was
|
||||
already there — the guard has always taken a `ToIntFunction<String>` — so this task filled it rather
|
||||
than changing the pipeline. The published slots for `foo`, `bar`, and `hello` are asserted, so a
|
||||
regression in the checksum shows up as a wrong number rather than as a cluster that quietly
|
||||
mis-routes.
|
||||
|
||||
**An empty tag is not a tag.** `{}` hashes the whole key, matching Redis, and that is tested,
|
||||
because the alternative — hashing an empty string — would collapse every such key onto one slot.
|
||||
|
||||
**A cluster scan is not a snapshot, and `ClusterScanCursor` refuses to pretend otherwise.** A sweep
|
||||
is complete only when every primary has *answered* with a zero cursor; a primary that was never
|
||||
asked counts as unfinished. Reporting completion after skipping a shard would let a caller conclude
|
||||
a key does not exist when a whole shard was never looked at.
|
||||
|
||||
**Redirect counting separates two different incidents.** A trickle of `MOVED` means the client's
|
||||
topology is stale; `ASK` and `TRYAGAIN` mean a resharding is in progress. The driver follows both
|
||||
transparently, so neither is visible to a caller — `ClusterTopologyObserver` is what makes them
|
||||
visible to an operator, and it accepts slot numbers and node identifiers only, never a key.
|
||||
|
||||
**`ExecutionCertainty` is the failover decision made explicit.** "The server refused it" and "the
|
||||
connection died after the command was written" look identical to a caller and have opposite
|
||||
consequences. `SentinelFailoverObserver.classify` returns `SAFE_TO_RETRY_FAILURE` only when the
|
||||
command provably never reached the server; anything written and unanswered is `AMBIGUOUS_FAILURE`,
|
||||
and `allowsAutomaticRetry` then defers to the command policy's `retry-safe` flag. A non-idempotent
|
||||
write is therefore never resent by the pipeline, and each one is counted so an operator knows how
|
||||
many need reconciling.
|
||||
|
||||
**The reconnect queue is bounded on purpose.** An unbounded queue turns a thirty-second promotion
|
||||
into a thirty-second backlog that lands at once on a freshly promoted primary. Refusals past the
|
||||
bound are counted so the bound can be tuned from evidence rather than guessed.
|
||||
|
||||
**What is still owed:** the fault-injection evidence. Nothing here proves how Lettuce actually
|
||||
behaves during a promotion or a resharding — that is a real-topology lane and belongs to Task 26.
|
||||
These types are the classification and accounting that lane will assert against.
|
||||
|
||||
## 15. Task 21 — scripts are a deployment artefact, and Task 20 is blocked on the fixture
|
||||
|
||||
**Nothing accepts a script body at call time.** `EVAL` is blocked in the command policy, so the only
|
||||
reachable path is `EVALSHA` of a digest that `RedisScriptRegistry` obtained from a `SCRIPT LOAD` of
|
||||
a reviewed `RegisteredRedisScript`. A script assembled from request data has the blast radius of the
|
||||
whole keyspace; making registration a deployment step is what turns "we only run reviewed scripts"
|
||||
from a convention into a structural property.
|
||||
|
||||
**Keys are declared, and that is what makes them checkable.** Every key goes into the request's key
|
||||
list, so a script is namespace-checked and same-slot-checked exactly like any other multi-key
|
||||
command. `RedisArgument` is a distinct type from a key for the same reason: a key smuggled through
|
||||
`ARGV` would bypass both checks, and having the two be different types is what makes that a compile
|
||||
problem rather than a review problem.
|
||||
|
||||
**A registered script returns one bulk reply.** That is a contract, not a limitation of
|
||||
`RedisResultDecoder`. A nested Lua table forces the SDK to guess how deep the reply is and how each
|
||||
level is typed, which is the ambiguity a typed API exists to remove. Encode the result and decode it
|
||||
in the decoder.
|
||||
|
||||
**`NOSCRIPT` is the one automatic retry in the SDK.** The server rejects the call before running
|
||||
anything, so reloading and re-issuing once repeats nothing. It is not a retry of an ambiguous write,
|
||||
and no other failure is retried on this path.
|
||||
|
||||
**Functions are callable, not loadable.** `FUNCTION LOAD` is `ADMIN_ONLY` in the catalog and belongs
|
||||
to the admin plane, so `RedisFunctionOperations` has no method that introduces server-side code.
|
||||
`RegisteredRedisFunction` carries the library's semantic version because a library replaced under
|
||||
the same name changes behaviour with no signal at the call site. A function declared read-only is
|
||||
issued as `FCALL_RO`, which lets the server refuse a wrong declaration — worth more than the replica
|
||||
routing it also buys.
|
||||
|
||||
**Task 20 is deliberately not half-done.** `WATCH`/`MULTI`/`EXEC` is implementable against Lettuce —
|
||||
after `MULTI` the command futures complete when `EXEC` runs — but proving it needs a fixture that
|
||||
models that deferral. The current `InMemoryRedisCommandGateway` completes every future eagerly, so a
|
||||
transaction written against it would apply its writes *before* the `WATCH` conflict was detected: the
|
||||
fixture would report a correct-looking conflict while the effects had already landed. A fake that
|
||||
lies about atomicity is worse than no fake, so the transaction work is deferred until the fixture
|
||||
grows a deferral model (or the real-server lane from Task 26 exists), rather than being landed
|
||||
against a fixture that cannot falsify it.
|
||||
|
||||
## 16. Task 22 — the escape hatch, and why it is not an escape
|
||||
|
||||
The raw gateway exists because a few commands have no typed form worth building, not because
|
||||
arbitrary command execution is acceptable. Everything about its shape follows from that.
|
||||
|
||||
**Two independent gates, neither decided at request time.** A command must be classified
|
||||
`RAW_ONLY` in `redis-command-policy.yml` — the organization's decision about which commands may ever
|
||||
leave through this door — *and* the deployment must have registered an `ApprovedRawCommand` for it
|
||||
in `RawCommandApprovals`. Neither alone is enough. The approval carries the argument, request, and
|
||||
reply ceilings and the timeout, so widening what may be sent is a deployment change, not a call-site
|
||||
one.
|
||||
|
||||
**The token is bound to its registry.** `RawCommandApprovals.issue` is the only source, and
|
||||
`verify` refuses a token from a different registry instance, a token issued for another policy, and
|
||||
an approval that is not byte-for-byte the registered one. That last check is the one that matters:
|
||||
without it a caller could present a widened copy of a real approval and keep the real policy id.
|
||||
|
||||
**Keys are parsed back, not taken on trust.** Arguments reach the gateway as opaque bytes, so the
|
||||
catalog's key specification locates the key positions and `RedisOperationContext.parseKey` — the
|
||||
same strict parse `SCAN` uses — turns each one back into a `QualifiedRedisKey`. A key outside the
|
||||
bound namespace or one that does not follow the key grammar is refused before anything is sent. A
|
||||
`movable` key specification cannot be checked without asking the server with `COMMAND
|
||||
GETKEYSANDFLAGS`, so it is refused at registration time; `SORT` and `SORT_RO` are therefore
|
||||
classified `RAW_ONLY` but not approvable until that lookup exists.
|
||||
|
||||
**Every RAW_ONLY command now names a permit policy.** The guard's rule is that an R2 command always
|
||||
states the policy that authorised it. The raw path used to be the one place that rule did not hold,
|
||||
so `raw-command` was added to the three `RAW_ONLY` entries and the gateway presents the SDK permit
|
||||
for it. The approval registry still decides *which* commands a deployment may send; the permit is
|
||||
what keeps the guard's invariant true on this path too.
|
||||
|
||||
**Everything else was already built.** Reachability, minimum version, risk refusal, and the timeout
|
||||
profile come from the catalog; namespace and same-slot from the guard; the audit record from the
|
||||
executor's observation, which carries the command family and latency and never a key or a value.
|
||||
The one new seam method, `sendApprovedRaw`, takes a `CommandId` rather than a string — by the time
|
||||
it is reached the identity has already been validated, classified, and matched to an approval.
|
||||
|
||||
## 17. Task 23 — the admin plane is defined by what it cannot do
|
||||
|
||||
Design section 14.2 lists what the admin plane must never reach. None of it is enforced by
|
||||
`RedisAdminOperations` omitting a method — omission is not enforcement, because the next person to
|
||||
add one would not notice. `FLUSHDB`, `FLUSHALL`, `SHUTDOWN`, `DEBUG`, `CONFIG SET`, `CONFIG REWRITE`,
|
||||
`CLIENT KILL`, `ACL SETUSER`, `ACL DELUSER`, `SLOWLOG RESET`, `LATENCY RESET`, `SCRIPT FLUSH`,
|
||||
`FUNCTION FLUSH`, and `MODULE UNLOAD` are all `BLOCKED` in the catalog, which means no path in the
|
||||
SDK can send them, and a test asserts that list rather than trusting it.
|
||||
|
||||
**Every diagnostic is checked against the catalog before it is built.** Not classified
|
||||
`ADMIN_ONLY`, or not read-only, and it is refused. That check is what stops a future addition to
|
||||
this class from quietly becoming a write.
|
||||
|
||||
**Replies are projected, not forwarded.** A slow log entry carries the command family and drops the
|
||||
arguments; a client entry carries id, age, idle, and last command and drops the peer address and the
|
||||
connection name. Both are read by an operator and end up in dashboards and tickets, and the dropped
|
||||
fields are exactly the caller and tenant identity that must not travel that way. The command family
|
||||
is enough to find a call site; an address is not needed to find a leaking pool.
|
||||
|
||||
**A key is still a key.** `MEMORY USAGE` takes a `QualifiedRedisKey` and goes through the guard, so
|
||||
an admin diagnostic cannot read a key outside the bound namespace. An absent key reports {@code -1},
|
||||
not zero, because "this key uses no memory" and "this key does not exist" are different answers.
|
||||
|
||||
**Separation is structural, not documentary.** The plane takes its own gateway, bound to the admin
|
||||
account's own connection, the same way the blocking operations take theirs. What that cannot enforce
|
||||
is that the deployment actually configured a separate ACL account — which is precisely why the
|
||||
dangerous commands are blocked catalog-wide rather than left to the credentials to prevent.
|
||||
|
||||
## 18. Tasks 24–25 — four extensions, one seam, and the checks the guard cannot do
|
||||
|
||||
All four extension families share `ExtensionCommandRunner`, so every extension command declares its
|
||||
key and is namespace- and slot-checked exactly like a classic one. Sharing the runner is also what
|
||||
stops them drifting apart on the parts that matter.
|
||||
|
||||
**The probe is the authority, the version is a pre-filter.** A managed Redis 8 with no module loaded
|
||||
reports the version and not the commands, so each bean is created through `ifSupported(...)` and a
|
||||
deployment without the module simply has no instance. Catalog minimums are the second gate, not the
|
||||
first.
|
||||
|
||||
**Bounds are in the types, not in a caller's discipline.** A `JsonPath` is validated against a
|
||||
narrow grammar — roots, members, indices, recursive descent — so a path assembled from request data
|
||||
cannot become `$` and replace a whole document. A `TimeSeriesSample` series is created with a
|
||||
retention or not at all; unlike a stream there is no per-append trim to fall back on. A `SearchQuery`
|
||||
carries its offset, page size, and timeout, so "read the whole index" cannot be written. Every
|
||||
probabilistic structure is reserved with an explicit error rate and capacity, because one created
|
||||
implicitly by its first write gets server defaults and saturates into answering "probably present"
|
||||
for everything.
|
||||
|
||||
**The interfaces say the answers are approximate.** `probablyContains`, `estimateCount`,
|
||||
`estimateQuantile` — a false-positive rate does not become a correctness bug because someone read a
|
||||
method called `contains`.
|
||||
|
||||
**Search is the one place the guard cannot help.** An `FT` command addresses an index, and an index
|
||||
is not a key, so there is no key on the request to namespace-check. The index name is therefore a
|
||||
validated type rendered with the process's namespace prefix by the operations class, and the key
|
||||
prefix an index covers is rendered the same way. An index can only be created over — and queried
|
||||
against — documents this process owns, and that rule lives in one method rather than in a review
|
||||
checklist. `FT.DROPINDEX` is `BLOCKED` for the whole SDK: dropping an index is a destructive
|
||||
operational action, and an accidental one is indistinguishable from a search that suddenly returns
|
||||
nothing.
|
||||
|
||||
**What is still owed:** evidence against real modules. Nothing here proves how RedisJSON, the query
|
||||
engine, Time Series, or the probabilistic structures actually reply — the fixture answers with what
|
||||
the design says they answer. That is Task 26's lane.
|
||||
|
||||
## 19. The "dead capability property blocks" item was wrong
|
||||
|
||||
Earlier notes in this delivery listed `app-bootstrap/src/main/resources/application.yml`, `src/.env`,
|
||||
and `docs/registries/env-keys.yaml` as carrying dead property blocks for five removed capabilities
|
||||
(cache, session, idempotency, lease, rate-limit), to be deleted together because `verifyEnvKeys` is
|
||||
fail-closed.
|
||||
|
||||
That is not true for at least three of them. `ca-skeleton.capabilities.rate-limit.provider`,
|
||||
`.idempotency.provider`, and `.lease.provider` are read at startup by
|
||||
`dev.caskeleton.bootstrap.runtime.SecretSourceValidator`, which refuses to start when a provider is
|
||||
selected without its HMAC secret, and `SecretSourceValidatorTest` covers all three. Deleting those
|
||||
blocks would remove a live startup check and break the test.
|
||||
|
||||
`app.rate-limit.*` is a separate, also live tree bound by `EdgeRateLimitTransportSettings` in
|
||||
`adapter:inbound:web`; it is not the same property as the capability selector above and the two must
|
||||
not be conflated.
|
||||
|
||||
The `ca-skeleton.capabilities.cache.canonical.*` and `ca-skeleton.security.redis-session.*` blocks
|
||||
have no binder that a source search finds, so they may genuinely be residue — but "no binder found"
|
||||
is not the same as "unused", and removing keys from a fail-closed three-file invariant on that basis
|
||||
is not a change worth making without auditing each key's consumers. No cleanup was performed.
|
||||
|
||||
## 20. Tasks 26–27 — the harness landed, the evidence did not
|
||||
|
||||
I previously described these two as blocked on a real server. That was wrong and worth correcting:
|
||||
the *evidence* needs servers, but the harness, the ACL accounts, the docs gates, and the CI wiring
|
||||
are all files, and they are now in the repository.
|
||||
|
||||
**What landed.**
|
||||
|
||||
- `infra/redis-sdk/{standalone,sentinel,cluster}/compose.yml` — three lanes, version-parameterised so
|
||||
one file serves every row of the support matrix. Sentinel runs three sentinels because a
|
||||
two-sentinel quorum cannot survive losing one, and a failover test that cannot lose a sentinel is
|
||||
not testing failover. Cluster runs six nodes so a promotion can be forced without losing a shard,
|
||||
and waits for slot assignment before tests start.
|
||||
- `infra/redis-sdk/acl/*.acl` — one account per `CommandAccess` level, each deliberately narrower
|
||||
than the SDK's own rules. The account is the last boundary and a permit never widens it, so a
|
||||
mistake in the SDK is still refused by the server.
|
||||
- `redisTopologyTest`, a Gradle lane tagged `redis-topology` and excluded from the default unit task.
|
||||
It **fails closed**: selecting it without host, port, and mode is a `GradleException`, and
|
||||
`RedisTopologyEndpoint` refuses to default to `localhost:6379`. A topology test that silently
|
||||
passes because it never connected is worse than not having one.
|
||||
- `docs/redis/support-matrix.md`, which `RedisSupportMatrixTest` parses. A package or a capability
|
||||
that is not listed fails the build, so stating the support level is part of shipping a module
|
||||
rather than a follow-up someone remembers. The certified-version table says "lane declared, not
|
||||
run" for all three topologies, and the test asserts that string — a certified version cannot be
|
||||
claimed from a lane that has never produced evidence.
|
||||
- `docs/redis/command-policy.md`, `operations.md`, `upgrade-guide.md`. The upgrade guide states why
|
||||
each check exists, not just that it is required: an unclassified command is refused, but a command
|
||||
whose risk changed upstream and is still classified R1 here is not; a rollback that leaves a
|
||||
process holding stale script digests produces `NOSCRIPT` on every scripted call.
|
||||
- `.github/workflows/redis-sdk-topology.yml`, manual-dispatch only, plus two new entries in
|
||||
`.github/ci-gate-matrix.yml` — the support matrix as a release-blocking contract test, and the
|
||||
topology evidence as explicitly `delegated-pending`. The gate count moved from 27 to 29.
|
||||
|
||||
**What did not land: the evidence.** No assertion in `RedisTopologyContractTest` yet exercises a
|
||||
promotion, a resharding, an ACL denial, or the guardrail datasets from the plan (1 MiB string,
|
||||
hundred-thousand-element collections, a million-entry trimmed stream, a five-hundred-command
|
||||
pipeline). Writing those assertions against a lane that has never been started would produce tests
|
||||
whose first run is also their first review, so the lane is fail-closed and the support matrix says
|
||||
plainly that nothing is certified. That is the honest state, and the harness is what makes closing
|
||||
it a bounded piece of work rather than a project.
|
||||
|
||||
## 21. The standalone lane ran, and it found five defects
|
||||
|
||||
The lane in `infra/redis-sdk/standalone` was started against Redis 7.4 and
|
||||
`RedisTopologyContractTest` now asserts, for every account in `infra/redis-sdk/acl`, that the
|
||||
`CommandAccess` level grants exactly what the command policy catalog says it may issue. Seven tests
|
||||
pass. Getting there required fixing five things that reading the files would never have surfaced:
|
||||
|
||||
1. **The ACL files did not load at all.** A Redis `aclfile` accepts nothing but complete `user`
|
||||
lines — no comments, no line continuations — and the server refused to start. The rationale moved
|
||||
to `infra/redis-sdk/acl/README.md`, and the four accounts are concatenated into
|
||||
`all-accounts.acl` because Redis takes one `aclfile`.
|
||||
2. **The advanced account granted `SMEMBERS` and `SORT`.** Both are `RAW_ONLY`, so they belong to the
|
||||
raw gateway account alone. This is the defect worth caring about: the ACL account is the last
|
||||
enforcement boundary and a permit never widens it, so an account wider than the catalog silently
|
||||
removes the second control the whole raw-gateway design rests on.
|
||||
3. **The ordinary account granted `SORT_RO`,** for the same reason.
|
||||
4. **The ordinary account could not run `PUBLISH`, `SUBSCRIBE`, or `PING`,** all classified `TYPED`.
|
||||
5. **The ordinary account could not run `MULTI`, `EXEC`, `UNWATCH`, or `DISCARD`,** also `TYPED`.
|
||||
|
||||
6. **The admin account was missing twelve read-only diagnostics** the catalog exposes: the `OBJECT`,
|
||||
`PUBSUB`, and `XINFO` subcommands, `FUNCTION LIST`/`STATS`, and `CLUSTER KEYSLOT`. Closing this
|
||||
also forced a decision: `FUNCTION LOAD` is `ADMIN_ONLY` but not read-only, and granting it to an
|
||||
account named `admin-readonly` would make the name a lie. Loading a library is a deployment
|
||||
action with its own credentials, so the assertion covers read-only `ADMIN_ONLY` commands only.
|
||||
|
||||
There was also a defect in the test itself, which is worth recording because it is the failure mode
|
||||
this kind of test usually dies of: `ACL DRYRUN` checks arity *before* permission, so probing a
|
||||
command with the wrong number of arguments answers "wrong number of arguments" for an account that
|
||||
would have been refused anyway. Reading that as a grant makes the test pass while the account is
|
||||
wrong. The probe now walks argument counts until the server actually answers the permission
|
||||
question. A second one followed it: a command the server does not carry answers "not found", and
|
||||
skipping that without checking the catalog's minimum version is how a real ACL gap hides behind a
|
||||
module that happens not to be installed. An absent command is now only tolerated when the catalog
|
||||
already says the server is too old for it.
|
||||
|
||||
`docs/redis/support-matrix.md` records standalone 7.4 as "ACL contract verified"; Sentinel and
|
||||
Cluster remain "lane declared, not run", and `RedisSupportMatrixTest` still asserts that string.
|
||||
|
||||
**Still owed on this task:** the guardrail datasets (1 MiB string, hundred-thousand-element
|
||||
collections, a million-entry trimmed stream, a five-hundred-command pipeline) and the fault
|
||||
injection — promotion on the Sentinel lane, resharding on the Cluster lane. Those are the assertions
|
||||
`ExecutionCertainty` and `RedisSlotCalculator` were built to be checked against.
|
||||
|
||||
## 22. The guardrail run found the first real SDK defect
|
||||
|
||||
`LiveRedisGuardrailTest` is the first thing that puts `LettuceRedisCommandGateway` under the SDK's
|
||||
own contracts against a live server. Everything before it ran against
|
||||
`InMemoryRedisCommandGateway`, which is a deterministic stand-in and answers what the design says it
|
||||
should — so an encoding or budgeting mistake could not show up there by construction.
|
||||
|
||||
It found one immediately, and it is a good example of the class of bug a fake cannot catch:
|
||||
|
||||
**The cursor-scan reply budget was sized to the requested `COUNT`.** Redis treats `COUNT` as a hint,
|
||||
not a limit: it walks whole hash buckets and listpack entries and returns what it found. A real
|
||||
`HSCAN` asked for 500 came back with 501, and the SDK rejected a perfectly correct reply — a refusal
|
||||
the caller can neither act on nor avoid. `RedisOperationContext.scanBudget` now accepts the
|
||||
configured scan ceiling plus a fixed overshoot allowance, which is still a bound: a server returning
|
||||
an order of magnitude more than it was asked for is refused. All four scan sites (key, hash, set,
|
||||
sorted set) use it.
|
||||
|
||||
The rest of the datasets passed unchanged: the 1 MiB value ceiling holds and one byte over never
|
||||
leaves the process; a hundred-thousand-field hash refuses `HGETALL` and is only reachable by cursor;
|
||||
a stream trimmed to 1,000 stays trimmed while twenty thousand entries are appended; a
|
||||
five-hundred-command batch reports every item positionally.
|
||||
|
||||
**Still owed:** Sentinel promotion and Cluster resharding. Those need their own lanes started, and
|
||||
they are where `ExecutionCertainty` and `RedisSlotCalculator` finally get checked against reality.
|
||||
|
||||
## 23. The Sentinel and Cluster lanes ran, and the worst defect was not in the code
|
||||
|
||||
Both remaining lanes now produce evidence. `docs/redis/support-matrix.md` records which test
|
||||
produced which, and `RedisSupportMatrixTest` no longer asserts the literal string
|
||||
`"lane declared, not run"` — that gate worked only until the lanes ran, and a gate that has to be
|
||||
deleted the moment it binds was never a gate. It now requires every evidence claim to name a test
|
||||
class that exists in the source tree, which is a rule that survives the lanes running.
|
||||
|
||||
### The harness had to be fixed before it could produce anything
|
||||
|
||||
Neither compose file could have worked. Both published no ports, and more importantly both would
|
||||
have advertised container-internal addresses: Sentinel answers `get-master-addr-by-name` with the
|
||||
address it monitors and the client dials that itself, and a cluster client reads `CLUSTER SHARDS`
|
||||
and connects to every node it names. On a bridge network a host client resolves a topology it cannot
|
||||
reach. Both lanes now use host networking with fixed ports, which is the only arrangement where the
|
||||
address the topology advertises is the address the client can use.
|
||||
|
||||
Three smaller harness defects went with it: the endpoint record assumed the declared address was a
|
||||
data node (on the Sentinel lane it is a sentinel, so ACL assertions were being asked of the
|
||||
sentinel's own accounts); the CI workflow passed `6379` for all three lanes; and `redisTopologyTest`
|
||||
was cacheable, so Gradle reported a previous run's verdict as the current one against a lane that
|
||||
had since been restarted and promoted. Lane selection is now derived from the declared mode
|
||||
(`redis-topology & lane-<mode>`) so a promotion test is never selected on a standalone lane and
|
||||
never silently skipped either.
|
||||
|
||||
### The finding: a superseded primary keeps acknowledging writes
|
||||
|
||||
This is the most serious thing this delivery has surfaced, and none of it is in the SDK's code.
|
||||
|
||||
Sentinel promoted the replica at `05:56:12.503` and did not demote the old primary until
|
||||
`05:56:23.529`. For those eleven seconds the client stayed connected to a primary that had already
|
||||
been replaced, wrote, and was told `+OK` **2,086 times**. Every one of those writes was discarded
|
||||
when the old primary resynced from the new one — the server's own log says so:
|
||||
`Partial resynchronization not accepted: Requested offset for second ID was 9897663, but I can reply
|
||||
up to 9731839`. Exactly **one** command failed in the whole run.
|
||||
|
||||
There is no client-side signal for this. The server answered, so the driver recorded a success, the
|
||||
SDK recorded `CONFIRMED_SUCCESS`, and the caller was told the write landed. A second run made the
|
||||
point harder: sixteen thousand attempts, **zero** exceptions, 2,086 acknowledged writes gone.
|
||||
|
||||
`SentinelFailoverObserver` counts *ambiguous* writes and its documentation called those "the ones an
|
||||
operator has to reconcile". That was wrong by three orders of magnitude — the writes that actually
|
||||
needed reconciling were the confirmed ones, and no counter on the client can be made to include
|
||||
them. The class now says so instead of implying it measures something it cannot.
|
||||
|
||||
What closes the window is server-side. Re-running the identical promotion with
|
||||
`min-replicas-to-write 1` and `min-replicas-max-lag 1` cut acknowledged-and-discarded writes from
|
||||
**2,086 to 1**: the orphaned primary refused 2,020 writes with `NOREPLICAS`, which the SDK already
|
||||
translates to a definite, non-ambiguous failure. Both settings are in the lane, and
|
||||
`acknowledgedWriteLossIsBounded` ties the tolerated loss to the configured lag window rather than to
|
||||
a magic number.
|
||||
|
||||
### The assertion immediately caught a second version of the same mistake
|
||||
|
||||
The first run with the setting passed. The second failed, with 2,099 lost writes — because the
|
||||
setting had been written into the `primary` service only. These two nodes swap roles on every
|
||||
failover, so a guardrail applied to whichever one happens to start as primary stops applying the
|
||||
moment the lane does the thing it exists to do. Both data nodes now take their whole configuration
|
||||
from one definition, which makes the asymmetry impossible to reintroduce. Three consecutive
|
||||
promotions in both directions since: 0, 0, and 1 acknowledged write lost.
|
||||
|
||||
### One real translator defect
|
||||
|
||||
The promotion closed the channel under an in-flight `RPUSH` and Lettuce raised a bare
|
||||
`RedisException`, which matched no branch of `LettuceExceptionTranslator` and fell through to a
|
||||
generic failure reported with `ambiguous=false` — that is, as a write that *definitely did not run*.
|
||||
Nothing about an unrecognised failure supports that claim, and a caller who believes it retries a
|
||||
non-idempotent write. The fallback now treats an unclassified write failure as ambiguous, which is
|
||||
the safe direction, and two unit tests pin both branches.
|
||||
|
||||
### Cluster: the arithmetic holds
|
||||
|
||||
`LiveRedisClusterTest` checked `RedisSlotCalculator` against `CLUSTER KEYSLOT` over a corpus built
|
||||
from the brace rules a hand-written implementation gets wrong — `{}`, `a{}b`, `foo{}{bar}`,
|
||||
`foo{{bar}}zap`, `foo{bar}{zap}`, `{`, `}`, `}{`, an unclosed brace, the empty key, and non-ASCII
|
||||
keys. No disagreements, and the result was reproduced independently against the server outside the
|
||||
test. The rendered-key invariant holds too: the slot the SDK computes from a tag alone equals the
|
||||
slot the server computes from the whole rendered key, which is what makes the two-step design sound.
|
||||
|
||||
Cross-slot refusal was checked in both directions, because a guard stricter than the cluster costs
|
||||
availability for nothing and a looser one sends requests that cannot succeed; the pair the guard
|
||||
refuses is the pair the server answers `CROSSSLOT` for. Redirects were observed rather than assumed:
|
||||
a `MOVED` names the slot the client computed, and a slot put into a real `MIGRATING`/`IMPORTING`
|
||||
state answers `ASK` for an absent key and `TRYAGAIN` for a multi-key request that straddles the
|
||||
migration. The lane restores the slot to `STABLE`, so a run leaves the cluster as it found it.
|
||||
|
||||
Nothing in `sdk.cluster` needed changing. That is worth recording as an outcome, not treated as the
|
||||
test having nothing to say: the calculator is the one piece of this SDK that silently degrades into
|
||||
wrong refusals and wrong admissions if it is off by one, and it is now checked rather than assumed.
|
||||
|
||||
### Where this leaves the task
|
||||
|
||||
| | |
|
||||
| --- | --- |
|
||||
| Unit | 288 tests, 0 failures |
|
||||
| Standalone lane | 14 tests, 0 failures |
|
||||
| Sentinel lane | 8 tests, 0 failures, three promotions in both directions |
|
||||
| Cluster lane | 14 tests, 0 failures |
|
||||
| `check` + architecture/env/public-path | green for `adapter:outbound:cache-redis` |
|
||||
| `verify-gate-matrix.sh` | 29 gates, 27 verified, 2 delegated-pending, OK |
|
||||
|
||||
Defects found and fixed across the whole evidence effort: six in the ACL accounts, two in the ACL
|
||||
test itself, one in the scan budget, four in the topology harness, one in the Sentinel lane's
|
||||
configuration, one in the exception translator, and one documentation claim that was wrong by three
|
||||
orders of magnitude.
|
||||
|
||||
`:app-bootstrap:test --tests '*CleanArchitectureTest'` passes. It briefly did not, on
|
||||
`NO_UUID_RANDOM_IN_CONTROLLER` in `application.fileserver.cleanup.CleanupItem` — untracked
|
||||
in-progress work from a different feature that was being edited while this evidence ran. The
|
||||
identifier factories have since moved to `CleanupRequest` and no direct `UUID.randomUUID` or
|
||||
`UuidCreator` call remains in `application-core`, so the rule is satisfied by the current sources
|
||||
rather than waived.
|
||||
|
||||
**Task 20 is the only implementation task left.**
|
||||
|
||||
## 24. Task 20 — the fixture had to learn to defer before the contract meant anything
|
||||
|
||||
Task 20 was deferred back at section 15 for a reason that turned out to be the whole task: the
|
||||
in-memory fixture executes every command the moment it is called, so a transaction written against
|
||||
it would have passed while proving the opposite of what it claimed. The writes would already have
|
||||
happened before the commit, and a watch conflict would have had nothing left to discard.
|
||||
|
||||
The controller chose the full option — every command available inside the window, and the fixture
|
||||
reworked to match — over a narrow hand-picked subset.
|
||||
|
||||
### Deferral is one property, not a hundred and eleven
|
||||
|
||||
`RedisCommandGateway` has 111 methods and every one of them returns a `CompletionStage`. That is not
|
||||
incidental: deferral is a property of the *connection*, so it can be implemented once rather than
|
||||
per command.
|
||||
|
||||
On the production side it costs nothing at all. Lettuce already defers everything issued after
|
||||
`MULTI` and completes those futures from the `EXEC` reply, so `LettuceRedisCommandGateway` needed no
|
||||
change to any existing method — only the five new seam methods (`watch`, `unwatch`,
|
||||
`beginTransaction`, `commitTransaction`, `discardTransaction`). `commitTransaction` returns a
|
||||
boolean rather than a list of results, because the per-command stages resolve themselves and the
|
||||
only thing `EXEC` alone can say is whether it ran.
|
||||
|
||||
On the test side, `DeferringRedisCommandGateway` is a `java.lang.reflect.Proxy` that records an
|
||||
invocation, hands back an unfinished future, and replays it against the fixture at commit — which is
|
||||
exactly when Redis runs it. The 1,996-line fixture was not edited for it. The consequence that
|
||||
matters: a command added to the seam later cannot forget to be transactional.
|
||||
|
||||
The one part that does need the data is the watch check, so that lives in the fixture. It hashes the
|
||||
watched key's current contents rather than incrementing a counter at each of the sixteen mutation
|
||||
sites — a counter is something a seventeenth mutation can silently fail to update, and a hash is not.
|
||||
|
||||
### What the contract refuses to let a caller do
|
||||
|
||||
`QueuedReply.value()` throws before the commit. The alternative — returning `null` or a zero for a
|
||||
command the server has only answered `+QUEUED` to — is the trap the type exists to remove.
|
||||
|
||||
`TransactionResult` reports exactly two outcomes, "executed" and "a watched key changed so nothing
|
||||
ran", and neither is a rollback. Redis has none: a command that fails at runtime inside `EXEC` does
|
||||
not undo the ones around it, and the proxy reproduces that faithfully by failing one future and
|
||||
leaving the rest alone.
|
||||
|
||||
`RedisTransactionQueue` is write-only, which is a contract rather than an unfinished surface. A read
|
||||
inside the window cannot be branched on — its reply does not exist until every command has already
|
||||
been chosen — so accepting one would only offer a way to write code that looks conditional and is
|
||||
not. Reads a transaction depends on belong before it, under `WATCH`.
|
||||
|
||||
Queued commands go through `QueueingRedisCommandExecutor`, which is `SyncRedisCommandExecutor` with
|
||||
the wait removed and *nothing else* changed. The same `CommandPolicyGuard` admits them, so namespace,
|
||||
slot, permit, and budget rules hold identically: a transaction is not a way around the guard, and a
|
||||
test asserts that a foreign-namespace key is refused inside a window exactly as it is outside one.
|
||||
|
||||
### Three defects the tests found
|
||||
|
||||
1. **A callback returning nothing crashed the transaction.** `Optional.of` on a null body result
|
||||
threw an NPE after a perfectly successful commit. A transaction with no interesting return value
|
||||
is entirely normal, so the result now carries an empty value for it and the invariant only forbids
|
||||
a value on a transaction that did not execute.
|
||||
2. **`RedisTransactionQueue.delete` could never succeed.** `DEL` is R2 in the catalog because it
|
||||
accepts any number of keys, so it needs a permit and a budget even when a transaction queues
|
||||
exactly one. The queue presents the SDK's own permit rather than making every caller thread one
|
||||
through for a single-key delete.
|
||||
3. **The first conflict test was contending with itself.** It wrote the watched key through the same
|
||||
gateway — that is, from inside the very window it was supposed to be contending with — so the
|
||||
write was queued rather than applied and the transaction timed out instead of conflicting. A
|
||||
competing writer has to come from another connection, and the test now has one. This is the kind
|
||||
of mistake that would have produced a green test if the fixture had not been deferring.
|
||||
|
||||
| | |
|
||||
| --- | --- |
|
||||
| Unit | 296 tests, 0 failures |
|
||||
| `check` | green for `adapter:outbound:cache-redis` |
|
||||
|
||||
**Every implementation task in the plan is now done.**
|
||||
@@ -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 1–2 |
|
||||
| `SameNameButDifferent` | 9 | Qualify the two Redis nested enum types in section 2 |
|
||||
| `DefaultCharset` | 9 | Explicit UTF-8 test data in sections 1–2 |
|
||||
| `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 1–2 |
|
||||
| `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
Reference in New Issue
Block a user