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.**
|
||||
Reference in New Issue
Block a user