feat: add production capability foundations

This commit is contained in:
donghyeon-ka
2026-07-31 23:50:44 +09:00
parent b3add0162d
commit 567422f2e5
757 changed files with 132385 additions and 2146 deletions
@@ -0,0 +1,882 @@
# Fileserver R2 Control Plane and Provider Selection Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use
> `superpowers:subagent-driven-development` to implement this plan task-by-task. Steps use checkbox
> (`- [ ]`) syntax for tracking. Repository policy is `human-only`: do not stage, commit, amend, or
> push.
**Goal:** Add an explicit provider-neutral Fileserver R2 control plane and qualify
`local-persistent` as the first provider without making local filesystem the production default.
**Architecture:** `application-core` keeps the existing `FilePublicationPort` and gains only one
provider-neutral achieved-durability value. The fileserver leaf compiles `app.fileserver`
destination/provider settings into an exact registry, routes requests through one port bean, and
coordinates versioned operation, manifest, and reference records. A strict
`local-persistent` provider attests its root before use and advances the durable publication state
machine in forced, recoverable steps.
**Tech Stack:** Java 21, Spring Boot 4 configuration properties/autoconfiguration, JDK NIO/POSIX,
JUnit 5, AssertJ, ApplicationContextRunner, Gradle quality gates.
---
### Task 1: Add the provider-neutral achieved durability
**Files:**
- Modify:
`src/application-core/src/main/java/dev/caskeleton/application/filepublication/FilePublishReceipt.java`
- Modify:
`src/application-core/src/test/java/dev/caskeleton/application/filepublication/FilePublicationContractTest.java`
- [x] **Step 1: Write the failing contract test**
Add a test that constructs a receipt with the new achieved value and proves no provider or path type
is introduced:
```java
@Test
void receiptCanReportFileAndDirectorySyncWithoutExposingAProviderType() {
FilePublishReceipt receipt =
receiptWith(DurabilityGuarantee.FILE_AND_DIRECTORY_SYNC);
assertThat(receipt.durabilityGuarantee())
.isEqualTo(DurabilityGuarantee.FILE_AND_DIRECTORY_SYNC);
assertThat(FilePublishReceipt.class.getDeclaredFields())
.allSatisfy(field -> assertThat(field.getType().getName())
.doesNotContain("java.nio.file", "fileserver", "sftp"));
}
```
- [x] **Step 2: Verify RED**
Run:
```bash
cd src
./gradlew :application-core:test --tests '*FilePublicationContractTest' --console=plain
```
Expected: compilation failure because `FILE_AND_DIRECTORY_SYNC` does not exist.
- [x] **Step 3: Implement the minimum contract change**
Add only this enum member:
```java
public enum DurabilityGuarantee {
PROCESS_LOCAL_SYNC,
FILE_AND_DIRECTORY_SYNC,
PROVIDER_ACK_ONLY
}
```
- [x] **Step 4: Verify GREEN**
Run the command from Step 2. Expected: PASS.
---
### Task 2: Compile exact destination/provider settings with no local fallback
**Files:**
- Create:
`src/adapter/outbound/fileserver/src/main/java/dev/caskeleton/adapter/outbound/fileserver/FileserverR2Settings.java`
- Create:
`src/adapter/outbound/fileserver/src/main/java/dev/caskeleton/adapter/outbound/fileserver/CompiledFileDestination.java`
- Create:
`src/adapter/outbound/fileserver/src/main/java/dev/caskeleton/adapter/outbound/fileserver/FileserverBindingCompiler.java`
- Test:
`src/adapter/outbound/fileserver/src/test/java/dev/caskeleton/adapter/outbound/fileserver/FileserverBindingCompilerTest.java`
- [x] **Step 1: Write failing exact-binding tests**
Cover:
```java
@Test
void enabledSettingsRequireAnExplicitDestinationAndProvider() {
assertThatThrownBy(() -> FileserverBindingCompiler.compile(enabled(Map.of(), Map.of())))
.isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("destination");
}
@Test
void rejectsUnknownOrUnimplementedProviderTypes() {
assertThatThrownBy(() -> compile("shared-mounted"))
.isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("local-persistent");
}
@Test
void compilesOnlyAnExactLocalPersistentBinding() {
Map<FileDestinationId, CompiledFileDestination> result =
FileserverBindingCompiler.compile(validSettings());
assertThat(result).containsOnlyKeys(new FileDestinationId("local-export"));
assertThat(result.get(new FileDestinationId("local-export")).providerId())
.isEqualTo("local-primary");
}
```
Also reject blank IDs, unknown `provider-ref`, duplicate normalized IDs, non-absolute root, enabled
`auto-create`, unsupported publication/durability values, and non-positive row/byte bounds.
- [x] **Step 2: Verify RED**
Run:
```bash
cd src
./gradlew :adapter:outbound:fileserver:test \
--tests '*FileserverBindingCompilerTest' --console=plain
```
Expected: compilation failure because the settings/compiler do not exist.
- [x] **Step 3: Implement typed settings**
Use one public configuration-properties record:
```java
@ConfigurationProperties(prefix = "app.fileserver")
public record FileserverR2Settings(
boolean enabled,
Map<String, DestinationSettings> destinations,
Map<String, ProviderSettings> providers) {
public record DestinationSettings(
String providerRef,
String requiredPublication,
String requiredDurability,
long maximumRows,
long maximumEncodedBytes) {}
public record ProviderSettings(
String type,
String rootDirectory,
boolean autoCreate,
boolean strictPathSecurity,
String expectedFileStoreName,
String expectedFileStoreType,
String mountSentinelName,
String mountSentinelSha256,
String expectedOwner,
String maximumRootMode) {}
}
```
The compiler accepts exactly:
```text
type=local-persistent
required-publication=unique-atomic-create
required-durability=file-and-directory-sync
auto-create=false
strict-path-security=true
```
`CompiledFileDestination` contains validated application destination ID, provider ID, absolute
root, limits, root attestation inputs, and no Spring type.
- [x] **Step 4: Verify GREEN**
Run the command from Step 2. Expected: PASS.
---
### Task 3: Attest a pre-provisioned persistent root
**Files:**
- Create:
`src/adapter/outbound/fileserver/src/main/java/dev/caskeleton/adapter/outbound/fileserver/LocalPersistentRootEvidence.java`
- Create:
`src/adapter/outbound/fileserver/src/main/java/dev/caskeleton/adapter/outbound/fileserver/LocalPersistentRootAttestor.java`
- Test:
`src/adapter/outbound/fileserver/src/test/java/dev/caskeleton/adapter/outbound/fileserver/LocalPersistentRootAttestorTest.java`
- [x] **Step 1: Write failing attestation tests**
Create a real POSIX temporary root and sentinel. Test successful evidence and each fail-closed
condition:
```java
@Test
void attestsOwnerModeStoreSentinelSecureDirectoryAndSyncPrimitives() {
CompiledFileDestination destination = destinationFor(attestedRoot());
LocalPersistentRootEvidence evidence =
new LocalPersistentRootAttestor().attest(destination);
assertThat(evidence.root()).isEqualTo(root.toRealPath());
assertThat(evidence.secureDirectoryStream()).isTrue();
assertThat(evidence.directorySync()).isTrue();
assertThat(evidence.exclusiveHardLink()).isTrue();
}
```
Separate tests reject:
- relative or missing root;
- symlink root/ancestor;
- owner mismatch;
- group/world-writable root;
- FileStore name/type mismatch;
- missing, symlinked, non-regular, or digest-mismatched sentinel;
- staging/data/control on a different FileStore;
- unavailable `SecureDirectoryStream`, hard-link, or directory-force probe.
Probe collaborators may be package-private injectable functions so negative paths do not depend on
the host filesystem lacking a feature.
- [x] **Step 2: Verify RED**
Run:
```bash
cd src
./gradlew :adapter:outbound:fileserver:test \
--tests '*LocalPersistentRootAttestorTest' --console=plain
```
Expected: compilation failure because attestation types do not exist.
- [x] **Step 3: Implement strict attestation**
The attestor must:
```text
reject before creating anything when root/sentinel/owner/mode/store mismatch
capture root real path, file key, FileStore name/type, sentinel digest
create private .ca-fileserver, data, staging, operations, manifests, references, probe directories
set newly-created directories to 0700
force each created parent directory
open a SecureDirectoryStream on root
run unique exclusive-create + force + hard-link + directory-force probe
delete probe artifacts and force the probe directory
return immutable evidence used for pre/post identity checks
```
Do not silently downgrade to R1.
- [x] **Step 4: Verify GREEN**
Run the command from Step 2. Expected: PASS on the supported Linux/POSIX lane.
---
### Task 4: Add strict reference, journal-v2, manifest, and reference records
**Files:**
- Create:
`src/adapter/outbound/fileserver/src/main/java/dev/caskeleton/adapter/outbound/fileserver/R2PublishedReferenceCodec.java`
- Create:
`src/adapter/outbound/fileserver/src/main/java/dev/caskeleton/adapter/outbound/fileserver/DurablePublicationRecord.java`
- Create:
`src/adapter/outbound/fileserver/src/main/java/dev/caskeleton/adapter/outbound/fileserver/PrivateFileManifest.java`
- Create:
`src/adapter/outbound/fileserver/src/main/java/dev/caskeleton/adapter/outbound/fileserver/PublishedReferenceRecord.java`
- Create:
`src/adapter/outbound/fileserver/src/main/java/dev/caskeleton/adapter/outbound/fileserver/FileserverControlRecordCodec.java`
- Test:
`src/adapter/outbound/fileserver/src/test/java/dev/caskeleton/adapter/outbound/fileserver/FileserverControlRecordCodecTest.java`
- [x] **Step 1: Write failing codec tests**
Test:
```java
@Test
void referenceRoundTripRejectsForgeryUnknownRouteAndTruncation() {
PublishedFileReference reference = codec.encode("routea1", fixedFileId());
assertThat(codec.decode(reference, Set.of("routea1")).fileId()).isEqualTo(fixedFileId());
assertThatThrownBy(() -> codec.decode(tamper(reference), Set.of("routea1")))
.isInstanceOf(IllegalArgumentException.class);
assertThatThrownBy(() -> codec.decode(reference, Set.of("routeb2")))
.isInstanceOf(IllegalArgumentException.class);
}
```
For all three records prove:
- canonical encode/decode round trip;
- maximum encoded length;
- exact schema version;
- state and revision invariants;
- single-segment internal locators;
- lowercase SHA-256 fields;
- no absolute path, raw row/cell, credential, URI, or control character;
- newer schema and duplicate/unknown fields fail closed.
- [x] **Step 2: Verify RED**
Run:
```bash
cd src
./gradlew :adapter:outbound:fileserver:test \
--tests '*FileserverControlRecordCodecTest' --console=plain
```
Expected: compilation failure because R2 records/codecs do not exist.
- [x] **Step 3: Implement bounded canonical records**
Use a strict flat canonical JSON codec owned by this leaf. The record state is:
```java
enum State {
WRITING,
SEALED,
DATA_PUBLISHED,
MANIFEST_PUBLISHED,
REFERENCE_PUBLISHED,
PUBLISHED,
QUARANTINED
}
```
`R2PublishedReferenceCodec` uses:
```text
fsr1.<route-token>.<32-lower-hex-file-id>.<first-12-hex-of-sha256(prefix)>
```
The check digits detect corruption only and are not authentication.
- [x] **Step 4: Verify GREEN**
Run the command from Step 2. Expected: PASS.
---
### Task 5: Persist forced control records and operation locks
**Files:**
- Create:
`src/adapter/outbound/fileserver/src/main/java/dev/caskeleton/adapter/outbound/fileserver/LocalPersistentControlPlane.java`
- Test:
`src/adapter/outbound/fileserver/src/test/java/dev/caskeleton/adapter/outbound/fileserver/LocalPersistentControlPlaneTest.java`
- [x] **Step 1: Write failing control-plane tests**
Test direct lookup and forced revision handling:
```java
@Test
void storesAndDirectlyLoadsOperationManifestAndReferenceRecords() {
controlPlane.storeOperation(writingRecord());
controlPlane.storeManifest(manifest());
controlPlane.storeReference(referenceRecord());
assertThat(controlPlane.findOperation(OPERATION_ID)).contains(writingRecord());
assertThat(controlPlane.findManifest(FILE_ID)).contains(manifest());
assertThat(controlPlane.findReference(FILE_ID)).contains(referenceRecord());
}
```
Also prove:
- lower/equal incompatible state revision is rejected;
- request fingerprint mismatch is conflict;
- temp file is force-written before atomic replace;
- target parent is forced after replace;
- shard creation forces its parent;
- symlink shard/record is rejected with `NOFOLLOW_LINKS`;
- reads, temporary creation, stat, and delete use attested directory-relative names through
`SecureDirectoryStream`; operations without a portable secure hard-link/flagged atomic-replace
overload remain limited to the private-owner root and require pre/post identity checks;
- same operation is serialized by JVM stripe plus OS `FileLock`;
- record corruption is never treated as absent.
Use a package-private fault-point callback to observe/throw at:
```text
TEMP_FORCED
RECORD_REPLACED
PARENT_FORCED
```
- [x] **Step 2: Verify RED**
Run:
```bash
cd src
./gradlew :adapter:outbound:fileserver:test \
--tests '*LocalPersistentControlPlaneTest' --console=plain
```
Expected: compilation failure because the control plane does not exist.
- [x] **Step 3: Implement durable storage**
All writes follow:
```text
CREATE_NEW sibling temp
write all bytes
FileChannel.force(true)
ATOMIC_MOVE + REPLACE_EXISTING for the control record only
force parent directory
read-back and verify identity/revision/digest
```
Payload publication must never use overwrite-capable move. Control record replacement is safe only
under the operation lock and monotonically increasing `stateRevision`.
- [x] **Step 4: Verify GREEN**
Run the command from Step 2. Expected: PASS.
---
### Task 6: Implement the local-persistent R2 provider and deterministic recovery
**Files:**
- Modify:
`src/adapter/outbound/fileserver/src/main/java/dev/caskeleton/adapter/outbound/fileserver/CompiledFileDestination.java`
- Modify:
`src/adapter/outbound/fileserver/src/main/java/dev/caskeleton/adapter/outbound/fileserver/FileserverBindingCompiler.java`
- Modify:
`src/adapter/outbound/fileserver/src/main/java/dev/caskeleton/adapter/outbound/fileserver/LocalPersistentControlPlane.java`
- Modify:
`src/adapter/outbound/fileserver/src/main/java/dev/caskeleton/adapter/outbound/fileserver/LocalPublicationJournalCodec.java`
- Create:
`src/adapter/outbound/fileserver/src/main/java/dev/caskeleton/adapter/outbound/fileserver/FilePublicationProvider.java`
- Create:
`src/adapter/outbound/fileserver/src/main/java/dev/caskeleton/adapter/outbound/fileserver/LocalPersistentPublicationProvider.java`
- Create:
`src/adapter/outbound/fileserver/src/main/java/dev/caskeleton/adapter/outbound/fileserver/LocalPersistentPayloadOperations.java`
- Create:
`src/adapter/outbound/fileserver/src/main/java/dev/caskeleton/adapter/outbound/fileserver/FilePublicationCanonicalDigests.java`
- Create:
`src/adapter/outbound/fileserver/src/main/java/dev/caskeleton/adapter/outbound/fileserver/LocalPersistentRecoveryVerifier.java`
- Test:
`src/adapter/outbound/fileserver/src/test/java/dev/caskeleton/adapter/outbound/fileserver/FileserverBindingCompilerTest.java`
- Test:
`src/adapter/outbound/fileserver/src/test/java/dev/caskeleton/adapter/outbound/fileserver/LocalPersistentControlPlaneTest.java`
- Test:
`src/adapter/outbound/fileserver/src/test/java/dev/caskeleton/adapter/outbound/fileserver/LocalPersistentPayloadOperationsTest.java`
- Test:
`src/adapter/outbound/fileserver/src/test/java/dev/caskeleton/adapter/outbound/fileserver/LocalPersistentPublicationProviderTest.java`
- Test:
`src/adapter/outbound/fileserver/src/test/java/dev/caskeleton/adapter/outbound/fileserver/LocalPersistentPublicationRecoveryTest.java`
- [x] **Step 1: Write failing publication-order tests**
First add failing compiler/control-plane assertions for:
```text
deterministic route token = "r" + first 31 lowercase hex of canonical policy digest
same startup allowlist route-token collision -> startup failure
length-prefixed effective policy/schema/format digest stability
same secure operation lookup -> typed canonical v1 or v2
v1 is read-only; malformed UTF-8/non-canonical/newer schema is indeterminate, never absent
control fault context identifies record kind, identity,
applicable operation state/revision, and force boundary
```
Then use a deterministic file ID/clock and a fault recorder. Prove exact order:
```text
J_WRITING
STAGE_FORCED
J_SEALED
DATA_LINKED
DATA_DIRECTORY_FORCED
J_DATA_PUBLISHED
MANIFEST_FORCED
J_MANIFEST_PUBLISHED
REFERENCE_FORCED
J_REFERENCE_PUBLISHED
J_PUBLISHED
```
Verify the receipt has an opaque `fsr1` reference,
`UNIQUE_ATOMIC_CREATE`, and `FILE_AND_DIRECTORY_SYNC`.
Also test producer once, streaming bounds, formula mitigation, target collision no overwrite,
root-identity change indeterminate, and manifest/reference locator non-disclosure. The stored
`internalLocator` is the generated filename only; its data shard is derived from the first two
hex characters of `fileId`.
- [x] **Step 2: Verify RED**
Run:
```bash
cd src
./gradlew :adapter:outbound:fileserver:test \
--tests '*FileserverBindingCompilerTest' \
--tests '*LocalPersistentControlPlaneTest' \
--tests '*LocalPersistentPublicationProviderTest' --console=plain
```
Expected: compilation/test failure because the compiled identity, typed compatibility lookup,
contextual fault seam, payload operations, and provider do not exist.
- [x] **Step 3: Implement prerequisites and minimal R2 publication**
Compile one restart-stable destination identity without adding a config key:
```text
effectivePolicyDigest = SHA-256(length-prefixed canonical descriptor fields)
routeToken = "r" + first 31 lowercase hex of effectivePolicyDigest
```
The canonical descriptor includes destination/provider IDs, limits, required guarantees, and the
format/encoder revision. The schema and format policy use the same length-prefixed digest helper.
Reject route-token collisions across the compiled startup allowlist. Keep digest/token derivation
on the production SHA-256 path only. Exercise the otherwise impractical collision branch through
the same package-private pure route-registry check used by production, using two different test
digests whose first 31 hex characters collide; expose no digest/token runtime override.
Extend `LocalPersistentControlPlane` with one secure relative typed operation lookup. It returns
schema-v1 only through strict UTF-8 plus canonical v1 re-encode byte equality and never writes v1;
schema-v2 remains the only write format. Enrich its package-private fault callback with record kind,
identity, operation state/revision, and force boundary so Task 8 can stop at an exact record force.
The provider:
```text
validates destination and request before producer invocation
acquires operation lock
loads operation by direct ID
allocates fileId/name before WRITING
streams with existing StreamingCsvEncoder
forces stage and stores SEALED
exclusive hard-links data and forces data directory
publishes private manifest
publishes reference index
stores terminal receipt snapshot
returns only after terminal journal parent force/read-back
```
`LocalPersistentPayloadOperations` owns restrictive staging/data shard creation, secure relative
stage create/write/force, stable no-follow artifact inspection/digest, exact stage deletion,
exclusive no-replace hard-link, standalone recovery-time data-shard directory force, and
attested-root-relative R1 artifact inspection. Absolute hard-link/directory-force calls are allowed
only inside the attested private-owner boundary with file/root/directory identity checks. An
existing matching data artifact discovered from `SEALED` must have its shard directory forced
again before the journal may advance; it is never republished through a collision path. A
root-level R1 artifact is restored only after bounded SDS-relative no-follow inspection matches the
terminal R1 journal.
Before and after the hard-link commit, compare root real path, file key, FileStore, and sentinel
digest to `LocalPersistentRootEvidence`.
- [x] **Step 4: Write failing recovery matrix tests**
For every non-terminal state construct matching/missing artifacts and retry with a producer that
throws if called. Expected:
```text
SEALED + stage -> resume data publish
SEALED + matching data -> resume manifest
DATA_PUBLISHED -> resume manifest
MANIFEST_PUBLISHED -> resume reference
REFERENCE_PUBLISHED -> finish terminal journal
PUBLISHED + all matching -> restore exact receipt
non-terminal data/manifest/reference mismatch -> QUARANTINED / integrity failure
PUBLISHED artifact/metadata/receipt mismatch -> preserve all terminal evidence; integrity / indeterminate
required artifact missing -> fail-closed indeterminate / quarantine, never success
fingerprint mismatch -> CONFLICT
root identity mismatch -> PUBLISH_INDETERMINATE
WRITING producer/stage failure -> exact cleanup + unsealed QUARANTINED
retry with existing WRITING -> producer is not invoked; indeterminate / quarantine
retry of unsealed QUARANTINED -> producer is not invoked
```
`LocalPersistentRecoveryVerifier` must cross-check the operation, incoming request, stable data
digest, canonical manifest/reference digests, all locators/counts/timestamps, and guarantees.
Because operation schema v2 does not carry a standalone format-policy snapshot, it must require an
exact current compiled effective-policy revision/digest match before using the current
format-policy digest; it must fail closed instead of guessing across an encoder-policy change.
Current configured byte/row limits apply to a new attempt. Recovery inspection is bounded by the
already frozen operation byte size (with overflow-safe equality), so a later lower configuration
limit does not reinterpret a sealed artifact. If both stage and data exist, their stable file keys
must match before exact stage deletion; equal bytes alone are insufficient.
Restore a terminal receipt only when it equals the full receipt reconstructed from the verified
manifest/reference; checking only operation ID/count/SHA is insufficient. Reuse a verified
immutable manifest/reference `publishedAt` after a crash instead of generating a conflicting time.
`QUARANTINED` journal transitions are limited to non-terminal operations. A mismatch discovered
from `PUBLISHED` must not replace the terminal journal or delete/overwrite data, manifest, or
reference records; return typed integrity/indeterminate and preserve all terminal evidence. A
separate immutable quarantine incident record is outside this increment.
- [x] **Step 5: Write failing R1 compatibility tests**
Pre-provision an existing R1 root so it passes every R2 root attestation condition, then configure
that same root as the R2 destination. Place a valid journal schema-v1 terminal record at the shared
hashed operation path and a matching root-level R1 artifact.
The R2 reader may restore its original `PROCESS_LOCAL_SYNC` receipt, but must not create an R2
manifest/reference, change its guarantee, or rewrite the record as schema v2. Newer/corrupt R1
records remain indeterminate. Also prove malformed UTF-8 and a decodable but non-canonical v1
encoding fail, and that simultaneous R1/R2 bean activation is not required for migration.
- [x] **Step 6: Verify compatibility RED, then implement read-only compatibility**
Run:
```bash
cd src
./gradlew :adapter:outbound:fileserver:test \
--tests '*LocalPersistentPublicationRecoveryTest' --console=plain
```
Expected before implementation: the R1 restoration assertion fails. Reuse the existing schema-v1
model/codec behind an added strict UTF-8 and canonical re-encode equality guard, only as a read-only
compatibility reader; do not add schema-v1 write paths or an unconfigured second root.
- [x] **Step 7: Verify recovery RED, then implement recovery**
Run:
```bash
cd src
./gradlew :adapter:outbound:fileserver:test \
--tests '*LocalPersistentPublicationRecoveryTest' --console=plain
```
Expected before recovery implementation: failures at each resume assertion. Implement only the
matrix and verifier rules above. When producer or staging fails after `J_WRITING`, preserve the
original exception, attach cleanup/control failures as suppressed, exact-delete the partial stage,
and store unsealed `QUARANTINED` evidence so retry cannot replay the producer. A retry that finds
`WRITING` after a process crash also must not invoke the producer. Then rerun. Expected: PASS.
- [x] **Step 8: Verify provider GREEN**
Run:
```bash
cd src
./gradlew :adapter:outbound:fileserver:test \
--tests '*LocalPersistentPublicationProviderTest' \
--tests '*LocalPersistentPublicationRecoveryTest' --console=plain
```
Expected: PASS.
---
### Task 7: Add one routing port bean and reject ambiguous R1/R2 activation
**Files:**
- Create:
`src/adapter/outbound/fileserver/src/main/java/dev/caskeleton/adapter/outbound/fileserver/RoutingFilePublicationAdapter.java`
- Create:
`src/adapter/outbound/fileserver/src/main/java/dev/caskeleton/adapter/outbound/fileserver/FileserverR2Config.java`
- Create:
`src/adapter/outbound/fileserver/src/main/java/dev/caskeleton/adapter/outbound/fileserver/FileserverActivationValidator.java`
- Test:
`src/adapter/outbound/fileserver/src/test/java/dev/caskeleton/adapter/outbound/fileserver/FileserverR2ConfigTest.java`
- Modify:
`src/adapter/outbound/fileserver/src/main/java/dev/caskeleton/adapter/outbound/fileserver/FileExportConfig.java`
- Rename:
`src/adapter/outbound/fileserver/src/main/java/dev/caskeleton/adapter/outbound/fileserver/FileExportProperties.java`
to
`src/adapter/outbound/fileserver/src/main/java/dev/caskeleton/adapter/outbound/fileserver/FileExportSettings.java`
- Modify:
`src/adapter/outbound/fileserver/src/test/java/dev/caskeleton/adapter/outbound/fileserver/FilePublicationConfigTest.java`
- Modify:
`src/app-bootstrap/build.gradle`
- Modify:
`src/config/architecture/modules.json`
- Modify:
`src/app-bootstrap/src/test/java/dev/caskeleton/bootstrap/architecture/OptionalAdapterBeanGatingTest.java`
- Modify:
`src/app-bootstrap/src/test/java/dev/caskeleton/bootstrap/architecture/DisabledAdapterArchitectureTest.java`
- [x] **Step 1: Write failing composition/routing tests**
Prove:
```java
@Test
void disabledR2CreatesNoPortOrFilesystemSideEffect() {}
@Test
void enabledR2CreatesExactlyOneRoutingPortForExplicitBindings() {}
@Test
void requestForUnknownDestinationFailsBeforeProducerInvocation() {}
@Test
void enablingLegacyR1AndR2TogetherFailsStartup() {}
@Test
void configuredButUnimplementedSharedOrSftpProviderFailsStartup() {}
```
- [x] **Step 2: Verify RED**
Run:
```bash
cd src
./gradlew :adapter:outbound:fileserver:test \
--tests '*FileserverR2ConfigTest' --console=plain
```
Expected: compilation/test failure because R2 composition does not exist.
Execution note: the production composition skeleton had already been introduced before the
delegated test task returned, so a standalone RED Gradle run was no longer reproducible without
reverting work. The tests still exposed the missing method-level conditional gate through the
bootstrap architecture check; that failure was observed and fixed before GREEN.
- [x] **Step 3: Implement exact routing composition**
`RoutingFilePublicationAdapter` contains an immutable
`Map<FileDestinationId, FilePublicationProvider>` and delegates only after exact lookup.
`FileserverR2Config`:
- is conditional on `app.fileserver.enabled=true`;
- enables `FileserverR2Settings`;
- compiles and attests every configured binding at startup;
- creates one provider instance per provider ID;
- creates exactly one `FilePublicationPort`;
- rejects `ca-skeleton.fileserver.enabled=true` in the same environment before either R1 root
creation or R2 attestation, independently of Spring bean creation order;
- rejects different provider IDs that resolve to the same normalized root;
- never creates directories/connections when disabled.
The same package-private activation validator runs first in both R1 bean factories and the R2
routing factory; conditional precedence is not an acceptable substitute for an ambiguity failure.
Use strict configuration-properties binding (`ignoreUnknownFields = false`). Wire the fileserver
leaf into `app-bootstrap` through the architecture registry and Gradle dependency in this task so
the runtime composition is real, while keeping all local provider/control types private to the
leaf. Rename the legacy configuration-properties type to the repository-required `*Settings`
suffix before exposing this leaf to bootstrap naming checks.
- [x] **Step 4: Verify GREEN**
Run the command from Step 2. Expected: PASS.
---
### Task 8: Add process-crash qualification, docs, and full gates
**Files:**
- Create:
`src/adapter/outbound/fileserver/src/test/java/dev/caskeleton/adapter/outbound/fileserver/FileserverCrashScenarioMain.java`
- Create:
`src/adapter/outbound/fileserver/src/test/java/dev/caskeleton/adapter/outbound/fileserver/LocalPersistentCrashRecoveryTest.java`
- Modify: `src/adapter/outbound/fileserver/README.md`
- Modify: `src/adapter/outbound/fileserver/CLAUDE.md`
- Modify:
`docs/superpowers/specs/2026-07-26-fileserver-production-capability-design.md`
- Modify:
`docs/superpowers/specs/2026-07-28-fileserver-r2-control-plane-provider-selection-design.md`
- Modify:
`docs/superpowers/plans/2026-07-28-fileserver-r2-control-plane-provider-selection.md`
- Modify: `docs/registries/env-keys.yaml`
- [x] **Step 1: Write the failing forked-process crash test**
Launch a new JVM with the test runtime classpath. The helper receives a fault point and calls
`Runtime.getRuntime().halt(91)` immediately after that point. Cover:
```text
J_WRITING
STAGE_FORCED
J_SEALED
DATA_LINKED
DATA_DIRECTORY_FORCED
MANIFEST_FORCED
MANIFEST_DIRECTORY_FORCED
REFERENCE_FORCED
REFERENCE_DIRECTORY_FORCED
TERMINAL_JOURNAL_FORCED
TERMINAL_JOURNAL_DIRECTORY_FORCED
```
Restart in a second JVM/process and assert exact receipt restoration or a documented typed
indeterminate/quarantine outcome, never producer replay or partial final bytes.
Also run a forked cross-process operation-lock proof using the same attested root and operation ID:
process A acquires and reports the OS lock, process B uses a bounded non-blocking/timed attempt and
must not enter the critical section while A is alive, then must acquire after A releases or is
forcibly terminated. This proof must exercise the OS `FileLock`; the same-JVM stripe test is not a
substitute and every wait requires a timeout.
- [x] **Step 2: Verify RED**
Run:
```bash
cd src
./gradlew :adapter:outbound:fileserver:test \
--tests '*LocalPersistentCrashRecoveryTest' --console=plain
```
Expected: failure until every fault point is injectable and recoverable.
Execution note: the contextual control-plane and payload fault seams introduced in Task 6 already
covered all eleven boundaries. The first complete forked-process run therefore passed without a
new production hook; no implementation was reverted merely to manufacture a RED result.
- [x] **Step 3: Implement only missing fault hooks/recovery transitions**
Fault hooks remain package-private test collaborators. No runtime setting or production bean may
allow arbitrary process termination.
- [x] **Step 4: Verify focused and module checks**
Run:
```bash
cd src
./gradlew :application-core:check :adapter:outbound:fileserver:check --console=plain
```
Expected: PASS.
- [x] **Step 5: Update readiness documentation**
Record:
- provider-neutral control plane and exact selector implemented;
- `local-persistent` is the only qualified R2 provider;
- `FILE_AND_DIRECTORY_SYNC` does not claim physical device power-loss protection;
- `shared-mounted`, SFTP, reaper/retention/quota/observability remain unimplemented;
- R1 compatibility artifacts are never auto-promoted.
Register the exact local provider environment keys from the design (`ROOT`, expected FileStore
name/type, sentinel digest, expected owner) with restart-only policy and conditional
`app.fileserver.enabled` validation. Do not add SFTP/NFS keys before those providers exist.
- [x] **Step 6: Run full repository gates**
Run:
```bash
cd src
./gradlew check --console=plain
./gradlew \
:application-core:verifyDependencyLocks \
:adapter:outbound:fileserver:verifyDependencyLocks \
:app-bootstrap:verifyDependencyLocks \
:sample-portfolio:verifyDependencyLocks \
verifyCleanArchitectureDependencies \
verifyPublicPathSnapshot \
verifyEnvKeys --console=plain
git diff --check
```
Expected: all commands PASS.
- [x] **Step 7: Request final independent review**
Review against:
- the R2 design spec;
- HARD-STOP rules;
- provider fallback/activation ambiguity;
- path/symlink/mount identity;
- crash ordering and recovery;
- receipt guarantee truthfulness;
- R1 compatibility and no unrelated adapter dependency.
Fix every Critical/Important issue and rerun the affected focused test plus full gates.
@@ -0,0 +1,120 @@
# HTTP Client Canonical Zero-Binding Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use
> `superpowers:subagent-driven-development` or `superpowers:executing-plans`. Repository policy
> overrides the skill's commit steps: do not stage, commit, amend, or push.
**Goal:** Make HTTP client activation an explicit canonical composition decision and prove that the
default zero-binding state creates no client, executor, shutdown guard, retry/circuit-breaker
registry, or transport resource.
**Architecture:** `adapter:outbound:httpclient` owns strict canonical configuration, immutable
binding/provider/catalog/readiness registries, and a pure activation resolver. `app-bootstrap` owns
the composition root that binds canonical properties and publishes an inert capability descriptor.
The existing JDK `OutboundHttpClient` remains an explicitly constructed R1 migration facade; its
legacy settings and infrastructure configuration must no longer be discovered automatically.
**Scope boundary:** This increment does not add Apache HC5, a provider factory, a real semantic
upstream binding, hard wire cancellation, TLS/DNS/proxy/auth, or an R2 readiness claim. Every current
ACTIVE selection must fail closed because the only derived readiness card remains
`NOT_IMPLEMENTED`.
---
### Task 1: Add strict canonical selection and provider binding models
**Files:**
- Create:
`src/adapter/outbound/httpclient/src/main/java/dev/caskeleton/adapter/outbound/httpclient/activation/HttpClientExpectedState.java`
- Create:
`src/adapter/outbound/httpclient/src/main/java/dev/caskeleton/adapter/outbound/httpclient/activation/HttpClientCanonicalConfiguration.java`
- Create:
`src/adapter/outbound/httpclient/src/main/java/dev/caskeleton/adapter/outbound/httpclient/activation/HttpClientCanonicalConfigurationBinder.java`
- Test:
`src/adapter/outbound/httpclient/src/test/java/dev/caskeleton/adapter/outbound/httpclient/activation/HttpClientCanonicalConfigurationBinderTest.java`
- [x] Write RED tests for the canonical YAML shape under
`ca-skeleton.capabilities.http-client` and `ca-skeleton.providers.http-client`.
- [x] Reject unknown fields, malformed IDs, unknown expected state, and any legacy input entering
canonical composition, including the DISABLED state.
- [x] Preserve `OutboundHttpSettings` constructors as migration API, but remove its global
`@ConfigurationPropertiesScan` participation.
- [x] Keep provider definitions inert data; configuration alone must not create a transport.
### Task 2: Add catalog/readiness registries and pure fail-closed activation resolution
**Files:**
- Create:
`src/adapter/outbound/httpclient/src/main/java/dev/caskeleton/adapter/outbound/httpclient/activation/HttpOperationCatalogRegistry.java`
- Create:
`src/adapter/outbound/httpclient/src/main/java/dev/caskeleton/adapter/outbound/httpclient/activation/HttpClientReadinessCardRegistry.java`
- Create:
`src/adapter/outbound/httpclient/src/main/java/dev/caskeleton/adapter/outbound/httpclient/activation/ResolvedHttpClientCapability.java`
- Create:
`src/adapter/outbound/httpclient/src/main/java/dev/caskeleton/adapter/outbound/httpclient/activation/HttpClientActivationResolver.java`
- Test:
`src/adapter/outbound/httpclient/src/test/java/dev/caskeleton/adapter/outbound/httpclient/activation/HttpClientActivationResolverTest.java`
- [x] Prove `DISABLED + bindings 0 + provider definitions 0` resolves to
`DISABLED_VERIFIED`, selected binding/card count 0.
- [x] Reject `DISABLED` with bindings or provider resources.
- [x] Reject `ACTIVE` with zero bindings.
- [x] For every binding, require an exact provider, provider destination, and registered operation
catalog for the same destination.
- [x] Derive the `httpclient-static-buffered` card from each current buffered classic profile.
- [x] Mark that card `NOT_IMPLEMENTED`; reject ACTIVE before any provider resource/factory exists.
### Task 3: Move HTTP Spring activation to the composition root
**Files:**
- Modify:
`src/adapter/outbound/httpclient/src/main/java/dev/caskeleton/adapter/outbound/httpclient/OutboundHttpClientConfig.java`
- Modify:
`src/adapter/outbound/httpclient/src/main/java/dev/caskeleton/adapter/outbound/httpclient/OutboundHttpSettings.java`
- Modify:
`src/adapter/outbound/httpclient/src/main/java/dev/caskeleton/adapter/outbound/httpclient/resilience/OutboundHttpResilienceConfig.java`
- Create:
`src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/httpclient/HttpClientCompositionConfig.java`
- Modify: `src/app-bootstrap/src/main/resources/application.yml`
- Modify:
`src/app-bootstrap/src/test/java/dev/caskeleton/adapter/outbound/OptionalAdapterBeanGatingTest.java`
- Test:
`src/app-bootstrap/src/test/java/dev/caskeleton/bootstrap/httpclient/HttpClientCompositionConfigTest.java`
- [x] Detach legacy HTTP infrastructure from component/configuration-properties scanning while
preserving direct constructors/factory methods used by forks and existing unit tests.
- [x] Register only canonical configuration, immutable registries, resolver, and inert descriptor
in the composition root.
- [x] Default application YAML to canonical `expected-state: DISABLED`, empty bindings, and empty
provider definitions; keep legacy migration keys out of both main and test application YAML.
- [x] Assert zero `OutboundHttpClient`, `RestClient`, `OutboundCallExecutor`,
`OutboundHttpShutdownGuard`, `OutboundHttpResilience`, `RetryRegistry`, and
`CircuitBreakerRegistry` beans/resources in the default context.
- [x] Assert contradictory/ACTIVE configurations fail startup before resource construction.
- [x] Load the real `application.yml` in composition tests and prove ACTIVE reaches the
`NOT_IMPLEMENTED` readiness card rather than a legacy conflict.
### Task 4: Document exact readiness and verify
**Files:**
- Modify: `src/adapter/outbound/httpclient/README.md`
- Modify: `src/adapter/outbound/httpclient/CLAUDE.md`
- Modify: `docs/superpowers/specs/2026-07-27-httpclient-production-capability-design.md`
- Modify:
`docs/superpowers/plans/2026-07-28-httpclient-production-capability-foundation.md`
- [x] Mark canonical zero-binding as implemented without marking HTTP R2 complete.
- [x] Keep HC5/provider resources/security/real-network qualification explicitly unimplemented.
- [x] Run focused tests:
```bash
cd src
./gradlew :adapter:outbound:httpclient:check --rerun-tasks --console=plain
./gradlew :app-bootstrap:check --rerun-tasks --console=plain
./gradlew :sample-portfolio:test --rerun-tasks --console=plain
./gradlew verifyCleanArchitectureDependencies verifyConfigurationPropertiesProcessor \
verifyEnvKeys verifyPublicPathSnapshot --console=plain
```
Do not edit unrelated notification, messaging, object-storage, JPA, MongoDB, GraphQL, gRPC, web, or
WebSocket files.
@@ -12,9 +12,10 @@ own feature-specific semantic ports. `adapter:outbound:httpclient` owns destinat
immutable operation descriptors, relative target construction, status/retry/body semantics, and
legacy provider fixes. The generic `OutboundHttpClient` remains a migration facade.
**Scope boundary:** This applies Phase 0 and a bounded Phase 1 foundation. Canonical binding
composition, exact readiness tuple registry, Apache HC5 pool, active cancellation, TLS/DNS/proxy,
auth, codec, and real-network qualification remain unimplemented.
**Scope boundary:** This applies Phase 0 and a bounded Phase 1 foundation. Canonical zero-binding
composition and active logical cancellation were implemented by later tracked plans. Exact
readiness tuple registry, Apache HC5 pool, TLS/DNS/proxy, auth, codec, and real-network
qualification remain unimplemented.
---
@@ -24,9 +25,9 @@ auth, codec, and real-network qualification remain unimplemented.
- Create: `src/application-core/src/main/java/dev/caskeleton/application/outbound/CallBudget.java`
- Test: `src/application-core/src/test/java/dev/caskeleton/application/outbound/CallBudgetTest.java`
- [ ] Write RED tests for expiry, remaining time, finite bounds, and parent/child intersection.
- [ ] Implement without Spring, wall-clock timestamps, scheduler, or HTTP types.
- [ ] Verify GREEN.
- [x] Write RED tests for expiry, remaining time, finite bounds, and parent/child intersection.
- [x] Implement without Spring, wall-clock timestamps, scheduler, or HTTP types.
- [x] Verify GREEN.
### Task 2: Add typed operation catalog and safe target construction
@@ -41,11 +42,11 @@ auth, codec, and real-network qualification remain unimplemented.
- Test: `src/adapter/outbound/httpclient/src/test/java/dev/caskeleton/adapter/outbound/httpclient/operation/HttpOperationCatalogTest.java`
- Test: `src/adapter/outbound/httpclient/src/test/java/dev/caskeleton/adapter/outbound/httpclient/target/HttpTargetBuilderTest.java`
- [ ] Write RED tests for ID/uniqueness/cross-field operation invariants.
- [ ] Write RED tests rejecting absolute, scheme-relative, traversal, user-info, query/fragment, and
- [x] Write RED tests for ID/uniqueness/cross-field operation invariants.
- [x] Write RED tests rejecting absolute, scheme-relative, traversal, user-info, query/fragment, and
multi-segment variables.
- [ ] Implement closed immutable descriptors and one-pass path-segment encoding.
- [ ] Verify GREEN.
- [x] Implement closed immutable descriptors and one-pass path-segment encoding.
- [x] Verify GREEN.
### Task 3: Correct characterized legacy provider safety defects
@@ -54,11 +55,11 @@ auth, codec, and real-network qualification remain unimplemented.
- Modify: `src/adapter/outbound/httpclient/src/main/java/dev/caskeleton/adapter/outbound/httpclient/OutboundHttpRestClientFactory.java`
- Test: `src/adapter/outbound/httpclient/src/test/java/dev/caskeleton/adapter/outbound/httpclient/OutboundHttpClientSafetyRegressionTest.java`
- [ ] Reproduce streaming 5xx body delivery and logical-call-only circuit-breaker counting.
- [ ] Make streaming validate status before exposing the body and discard error bodies.
- [ ] Put circuit breaker around each physical attempt and retry around the attempt loop.
- [ ] Set JDK redirects to `NEVER` explicitly and validate legacy base URI/relative request targets.
- [ ] Verify focused regressions and the full legacy test suite.
- [x] Reproduce streaming 5xx body delivery and logical-call-only circuit-breaker counting.
- [x] Make streaming validate status before exposing the body and discard error bodies.
- [x] Put circuit breaker around each physical attempt and retry around the attempt loop.
- [x] Set JDK redirects to `NEVER` explicitly and validate legacy base URI/relative request targets.
- [x] Verify focused regressions and the full legacy test suite.
### Task 4: Record exact readiness and verify
@@ -67,10 +68,10 @@ auth, codec, and real-network qualification remain unimplemented.
- Modify: `src/adapter/outbound/httpclient/CLAUDE.md`
- Modify: `docs/superpowers/specs/2026-07-27-httpclient-production-capability-design.md`
- [ ] Mark the implemented foundation and fixed legacy defects.
- [ ] Keep total deadline/cancellation, canonical zero-binding composition, Apache pool, fixed
egress, TLS/auth, bounded decoded streaming, and R2 cards unimplemented.
- [ ] Run:
- [x] Mark the implemented foundation and fixed legacy defects.
- [x] Track later total-deadline and canonical-zero-binding increments separately while keeping
Apache pool, fixed egress, TLS/auth, bounded decoded streaming, and R2 cards unimplemented.
- [x] Run:
```bash
cd src
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,52 @@
# Redis Cache Resilience Implementation Plan
> Repository commit policy is human-only. Do not stage, commit, amend or push.
**Goal:** Implement the approved cache-aside, bounded source protection and soft/hard TTL design
without promoting Redis beyond standalone cache R1.
### Task 1: Application cache-aside outcomes and policy
**Files:**
- Create/modify `src/application-core/src/main/java/dev/caskeleton/application/cache/*`
- Test `src/application-core/src/test/java/dev/caskeleton/application/cache/*`
- [x] Write RED tests for fresh/negative/miss/stale/source outcome transitions.
- [x] Add typed loader, failure, result, cancellation and immutable policy contracts.
- [x] Implement cache-aside sequencing; only authoritative absence may be negative-cached.
- [x] Preserve unclassified exceptions and interruption.
- [x] Verify focused application cache tests GREEN.
### Task 2: Bounded local single-flight and source bulkhead
**Files:**
- Create `CacheSingleFlight.java`
- Create `CacheSourceBulkhead.java`
- Test their concurrency behavior through focused unit tests.
- [x] Write RED concurrency tests.
- [x] Bound in-flight keys, waiters, admission wait and load wait.
- [x] Remove completed/failed/abandoned flights and preserve loader failure fan-out.
- [x] Prove Redis outage cannot create unlimited source concurrency.
### Task 3: Redis soft/hard TTL, jitter and stale envelope
**Files:**
- Modify `RedisCacheRegionPolicy.java`
- Modify `RedisCacheEnvelopeCodec.java`
- Modify `RedisStringCacheRegion.java`
- Modify/add focused Redis cache tests.
- [x] Write RED boundary, jitter, minimum and schema-compatibility tests.
- [x] Add an injected `Clock` and deterministic policy-revision jitter.
- [x] Encode absolute soft/hard expiry in envelope version 2.
- [x] Use the encoded hard expiry as physical Redis TTL.
- [x] Verify focused Redis tests GREEN.
### Task 4: Documentation and verification
- [x] Synchronize the completed foundation-plan checkboxes with existing code/evidence.
- [x] Update Redis README/CLAUDE/design readiness truth.
- [ ] Run application and Redis leaf checks.
- [ ] Run dependency locks, architecture, public path, env and diff checks.
- [x] Request independent specification and code-quality review.
@@ -0,0 +1,45 @@
# Redis Distributed Rate-Limit Implementation Plan
> Repository commit policy is human-only. Do not stage, commit, amend or push.
### Task 1: Shared edge rate-limit contract
- [x] Write RED contract/policy tests in `shared-contract`.
- [x] Add bounded request, algorithm parameters, policy, decision, outcome and port types.
- [x] Reject unsupported dedup/failure claims and unsafe fixed-point arithmetic.
- [x] Verify the shared contract without Redis/Spring types.
### Task 2: Structured Redis program execution
- [x] Write RED tests for MULTI reply arity/status/ASCII integer bounds and `NOSCRIPT`.
- [x] Add bounded structured `EVALSHA`/`EVAL` command support without changing scalar primitives.
- [x] Add exact catalog descriptors and resource digests for three rate programs.
### Task 3: Three atomic algorithms and semantic provider
- [x] Implement fixed-window Lua and golden vectors.
- [x] Implement sliding-counter Lua with conservative fixed-point arithmetic.
- [x] Implement token-bucket Lua with saturation and exact ceiling retry.
- [x] Add canonical private keys, policy lookup and typed failure mapping.
- [x] Prove denial does not consume quota and revision changes physical state.
### Task 4: Dedicated runtime and explicit composition
- [x] Add strict `app.rate-limit` settings and disabled-zero-side-effect configuration.
- [x] Use a dedicated coordination runtime rather than cache Redis beans/settings.
- [x] Add exact environment registry/application configuration entries.
- [x] Keep readiness at standalone provider R1.
### Task 5: Verification and review
- [x] Run shared/Redis/bootstrap focused checks.
- [x] Run architecture/dependency/env/diff gates.
- [ ] Run the public-path gate with the final combined change set.
- [x] Run an explicit real Redis lane when a service is available.
- [x] Request independent spec and quality review.
The Redis 7.4 service lane executes the exact-boundary admission after a denied non-consuming
request for all three algorithms, excessive clock-regression state immutability, token refill
remainder carry, malformed hash classification, cache NX, and observation-token compare-replace.
The program manifests therefore declare 7.4 as the minimum qualified version until a lower-version
service lane exists.
@@ -31,10 +31,10 @@ idempotency, lease, session, and R2/R3 evidence remain separate implementation p
- Create: `src/application-core/src/main/java/dev/caskeleton/application/cache/AuthoritativeAbsence.java`
- Test: `src/application-core/src/test/java/dev/caskeleton/application/cache/CacheRegionContractTest.java`
- [ ] Write a failing test for hit/negative/miss/unavailable distinctions and immutable metadata.
- [ ] Verify RED with `./gradlew :application-core:test --tests '*CacheRegionContractTest'`.
- [ ] Implement only framework-free values and ports.
- [ ] Verify GREEN.
- [x] Write a failing test for hit/negative/miss/unavailable distinctions and immutable metadata.
- [x] Verify RED with `./gradlew :application-core:test --tests '*CacheRegionContractTest'`.
- [x] Implement only framework-free values and ports.
- [x] Verify GREEN.
### Task 2: Add canonical Redis physical keys
@@ -45,12 +45,12 @@ idempotency, lease, session, and R2/R3 evidence remain separate implementation p
- Create: `src/adapter/outbound/cache-redis/src/main/java/dev/caskeleton/adapter/outbound/cache/redis/key/RedisKeyDigest.java`
- Test: `src/adapter/outbound/cache-redis/src/test/java/dev/caskeleton/adapter/outbound/cache/redis/key/RedisKeyBuilderTest.java`
- [ ] Write a failing test proving namespace isolation, one stable hash tag, bounded key bytes, and
- [x] Write a failing test proving namespace isolation, one stable hash tag, bounded key bytes, and
absence of raw sensitive resource identifiers.
- [ ] Verify RED.
- [ ] Implement SHA-256 for opaque IDs and HMAC-SHA-256 for sensitive scopes using defensive secret
- [x] Verify RED.
- [x] Implement SHA-256 for opaque IDs and HMAC-SHA-256 for sensitive scopes using defensive secret
copies and length-prefixed component encoding.
- [ ] Verify GREEN.
- [x] Verify GREEN.
### Task 3: Add a typed, versioned atomic-program catalog
@@ -66,11 +66,11 @@ idempotency, lease, session, and R2/R3 evidence remain separate implementation p
- Test: `src/adapter/outbound/cache-redis/src/test/java/dev/caskeleton/adapter/outbound/cache/redis/program/RedisProgramCatalogTest.java`
- Test: `src/adapter/outbound/cache-redis/src/test/java/dev/caskeleton/adapter/outbound/cache/redis/program/RedisAtomicPrimitivesTest.java`
- [ ] Write failing catalog and facade tests.
- [ ] Verify RED.
- [ ] Implement exact resource digest, key/argument bounds, typed status mapping, and no generic
- [x] Write failing catalog and facade tests.
- [x] Verify RED.
- [x] Implement exact resource digest, key/argument bounds, typed status mapping, and no generic
application-facing execution surface.
- [ ] Verify GREEN.
- [x] Verify GREEN.
### Task 4: Record exact readiness and verify
@@ -79,9 +79,9 @@ idempotency, lease, session, and R2/R3 evidence remain separate implementation p
- Modify: `src/adapter/outbound/cache-redis/CLAUDE.md`
- Modify: `docs/superpowers/specs/2026-07-26-redis-production-capability-design.md`
- [ ] Mark only contract/key/program foundation as implemented and all real runtime/capability
- [x] Mark only contract/key/program foundation as implemented and all real runtime/capability
promotion as unimplemented.
- [ ] Run:
- [x] Run:
```bash
cd src
@@ -89,5 +89,5 @@ cd src
./gradlew verifyCleanArchitectureDependencies --console=plain
```
- [ ] Do not claim Redis cache R1/R2 until a real standalone service lane and codec/runtime evidence
- [x] Do not claim Redis cache R1/R2 until a real standalone service lane and codec/runtime evidence
exist.
@@ -0,0 +1,662 @@
# Redis Production Capability Completion Plan
> **Scope:** Redis를 먼저 완료한다. 현재 실행 단위는 deep design Phase 5 전체가 아니라
> `Sentinel-first R2 qualification slice`다. 이 slice의 검증과 보고가 끝나면 멈추고
> fileserver, HTTP client, Redis Cluster/R3 중 다음 우선순위를 다시 정한다.
>
> **Workflow note:** 저장소가 지정한 Superpowers 설계·계획·TDD·디버깅·검증·리뷰 워크플로우를
> 적용한다. agent는 human-only commit 정책에 따라 stage/commit/amend/push하지 않는다.
**Goal:** `2026-07-26-redis-production-capability-design.md`의 Phase 15를 capability별로 구현하고,
standalone 기능의 존재를 production readiness로 오표기하지 않는 Redis platform을 만든다.
**Architecture:** `application-core``shared-contract`는 provider-neutral semantic contract만
소유한다. `adapter:outbound:cache-redis`가 Redis deployment, topology, key, codec, program,
runtime과 capability provider를 소유한다. `adapter:inbound:web`은 HTTP rate/session 보안 매핑만,
`app-bootstrap`은 provider/role/auth-mode composition만 소유한다. `domain-core`에는 Redis 개념을
추가하지 않는다.
**Readiness rule:** Redis leaf 전체에 단일 R2 label을 부여하지 않는다. `redis-cache`,
`redis-edge-rate-limit`, `redis-request-replay-idempotency`,
`redis-cache-refresh-soft-lease`, `redis-fenced-coordination`, `redis-session` card가 독립적으로
승격한다. R3 증거가 없는 failover/reshard/rotation은 R2 범위로 과장하지 않는다.
**Worktree rule:** 현재 `main` worktree의 다른 기술 변경은 사용자 소유다. Redis가 소유하지 않는
fileserver, HTTP client, messaging, notification, object storage 변경을 되돌리거나 포맷하지 않는다.
**Current milestone exit:** agent-side 목표는 `R2-ready candidate`다. clean committed source와
실제 remote GitHub Actions evidence가 없으면 card를 `selected`로 바꾸거나 R2라고 주장하지 않는다.
---
## Task 0 — Baseline과 acceptance registry 고정
**Files**
- Create: `src/config/redis/readiness-cards.yaml`
- Create: `src/gradle/redis-test-images.properties`
- Modify: `src/adapter/outbound/cache-redis/README.md`
- Modify: `docs/superpowers/specs/2026-07-26-redis-production-capability-design.md`
**Tests first**
- registry가 canonical card ID 여섯 개를 정확히 한 번 포함하는지 실패 테스트를 작성한다.
- image tag에 exact version과 digest가 없으면 configuration이 실패하는 테스트를 작성한다.
- `selected`, `implemented-candidate`, `not-implemented` 이외 상태를 거절한다.
- 현재 구현과 다른 readiness 표기를 거절한다.
**Implementation**
- 시작 상태는 cache/rate를 `implemented-candidate`, 나머지는 `not-implemented`로 기록한다.
- 실제 required evidence가 생기기 전에는 어떤 card도 `selected` R2로 승격하지 않는다.
- Redis minimum version은 실행 가능한 image/digest와 program manifest를 한 SSOT로 맞춘다.
**Verification**
```bash
cd src
./gradlew :adapter:outbound:cache-redis:test --tests '*RedisReadinessRegistryTest' --console=plain
```
## Task 1 — Canonical deployment/topology/role model
**Files**
- Create: `src/adapter/outbound/cache-redis/src/main/java/dev/caskeleton/adapter/outbound/cache/redis/config/RedisProviderProperties.java`
- Create: `src/adapter/outbound/cache-redis/src/main/java/dev/caskeleton/adapter/outbound/cache/redis/config/RedisDeploymentSettings.java`
- Create: `src/adapter/outbound/cache-redis/src/main/java/dev/caskeleton/adapter/outbound/cache/redis/config/RedisDeploymentSettingsFactory.java`
- Create: `src/adapter/outbound/cache-redis/src/main/java/dev/caskeleton/adapter/outbound/cache/redis/config/RedisRole.java`
- Create: `src/adapter/outbound/cache-redis/src/main/java/dev/caskeleton/adapter/outbound/cache/redis/config/RedisRoleBinding.java`
- Test: `src/adapter/outbound/cache-redis/src/test/java/dev/caskeleton/adapter/outbound/cache/redis/config/RedisDeploymentSettingsFactoryTest.java`
- Test: `src/adapter/outbound/cache-redis/src/test/java/dev/caskeleton/adapter/outbound/cache/redis/config/RedisProviderPropertiesBindingTest.java`
**Tests first**
- topology는 `standalone|sentinel|cluster` 중 정확히 하나다.
- endpoint는 non-empty, unique, bounded host/port다.
- Sentinel은 master name, 최소 3개 discovery endpoint, data/Sentinel auth와 TLS를 분리한다.
- Cluster는 database 0만 허용하고 seed가 비어 있으면 실패한다.
- role은 존재하는 deployment만 참조한다.
- cache와 session/coordination의 incompatible co-location을 startup 전에 거절한다.
- provider 정의만 있고 capability binding이 없으면 runtime side effect가 0이다.
**Implementation**
- Spring binding class와 validated sealed runtime model을 분리한다.
- legacy `app.cache.redis``app.rate-limit`은 migration compiler 입력으로만 허용하고 canonical
model과 동시에 설정되면 precedence를 정하지 않고 실패한다.
- `ClientMode.EXTERNAL`을 topology로 취급하지 않는다.
**Verification**
```bash
cd src
./gradlew :adapter:outbound:cache-redis:test --tests '*RedisDeploymentSettings*' --console=plain
```
## Task 2 — Topology-aware runtime, TLS/ACL과 secret material
**Files**
- Create: `.../redis/runtime/RedisDeploymentRuntime.java`
- Create: `.../redis/runtime/RedisDeploymentRuntimeFactory.java`
- Create: `.../redis/runtime/StandaloneRedisDeploymentRuntime.java`
- Create: `.../redis/runtime/SentinelRedisDeploymentRuntime.java`
- Create: `.../redis/runtime/ClusterRedisDeploymentRuntime.java`
- Create: `.../redis/security/RedisCredentialMaterialProvider.java`
- Create: `.../redis/security/RedisCredentialRotationCoordinator.java`
- Modify: `src/adapter/outbound/cache-redis/build.gradle`
- Modify: `src/adapter/outbound/cache-redis/gradle.lockfile`
**Tests first**
- standalone/Sentinel/Cluster가 각자 다른 native client/runtime을 만든다.
- Sentinel discovery credential/trust와 data-node credential/trust가 섞이지 않는다.
- Cluster client는 periodic+adaptive topology refresh, DB 0, bounded redirect/queue profile을 가진다.
- production profile에서 plaintext, trust-all, hostname verification off를 거절한다.
- named ACL username이 없거나 raw password가 YAML에 있으면 production activation이 실패한다.
- duplicate/out-of-order rotation event, expiry 재조회, new connection 검증 실패가 old traffic을
안전하게 보존한다.
- disabled capability는 client/event-loop/subscriber/scheduler를 만들지 않는다.
**Implementation**
- direct `spring-data-redis`, `lettuce-core` dependency를 leaf가 소유한다.
- deployment별 client resources와 lifecycle을 소유한다.
- connect/TLS/acquire/command/overall/shutdown timeout을 분리한다.
- 기존 no-replay, disconnected reject, finite queue/count/byte admission을 topology runtime에도
보존한다.
- secret value/reference/provider exception을 log/metric에 남기지 않는다.
## Task 3 — Key, codec, program manifest foundation
**Files**
- Create: `src/config/redis/program-set.schema.json`
- Modify: `src/adapter/outbound/cache-redis/src/main/resources/redis/program-set.json`
- Modify: `src/adapter/outbound/cache-redis/src/main/resources/redis/rate-program-set.json`
- Modify: `.../redis/RedisProgramDescriptor.java`
- Modify: `.../redis/RedisProgramCatalog.java`
- Modify: `.../redis/RedisLuaProgramExecutor.java`
- Create: `.../redis/key/RedisKeyMaterialProvider.java`
- Create: `.../redis/codec/RedisCapabilityCodec.java`
**Tests first**
- 모든 program은 exact source digest, semantic version, ordered KEYS/ARGV, result schema, slot rule,
state/TTL bound, minimum Redis version, retry/certainty, ACL command를 가진다.
- manifest와 Java descriptor가 drift하면 build가 실패한다.
- `NOSCRIPT` recovery는 bounded `SCRIPT LOAD -> EVALSHA`이고 arbitrary source 실행 surface가 없다.
- same-resource multi-key는 real `CLUSTER KEYSLOT`과 같은 slot이다.
- key digest material rotation은 fixed/dual-read-delete/cold-cutover rule을 지킨다.
- cache/idempotency/session codec은 N/N-1, future/corrupt/oversize/forbidden type을 구분한다.
**Implementation**
- foundation/rate manifest를 하나의 versioned registry contract로 통합하되 capability package와
facade는 분리한다.
- raw command, raw key, generic program executor를 Spring/application public surface에 노출하지 않는다.
## Task 4 — Cache consistency spine와 semantic region composition
**Files**
- Modify: `src/application-core/src/main/java/dev/caskeleton/application/cache/*`
- Create: `.../redis/cache/RedisCacheGenerationStore.java`
- Create: `.../redis/cache/RedisCacheRegionCompiler.java`
- Add resources: `region-generation-init-v1.lua`, `region-generation-bump-v1.lua`,
`cache-record-if-generation-v1.lua`
- Modify: `.../redis/RedisStringCacheRegion.java`
- Tests: application barrier tests, Redis real-service concurrency tests, binding tests
**Tests first**
- source load 중 generation bump가 일어나면 old result가 visible하지 않다.
- captured generation과 source revision이 바뀌면 stale writer가 새 값을 덮어쓰지 않는다.
- generation init race에서 하나의 canonical generation만 선택된다.
- operation ID가 같은 bump replay는 한 번만 적용된다.
- 여러 semantic region의 duplicate/missing binding은 fail-fast다.
- 실제 consumer가 semantic `CacheRegionPort``CacheAsideExecutor`를 사용하고 legacy fail-open
router와 암묵적으로 섞이지 않는다.
**Implementation decision**
- source revision은 opaque하므로 lexical “newer” 비교를 하지 않는다.
- region generation은 mass invalidation fence다.
- per-key invalidation은 해당 key의 revision/tombstone fence를 사용해 region 전체를 bump하지 않는다.
- write는 captured generation/revision condition을 만족할 때만 기록한다.
## Task 5 — Distributed refresh soft lease, L1/L2와 cache observability
**Files**
- Create application cache refresh coordination contracts without Redis types.
- Create Redis refresh claim/release programs and semantic provider.
- Create bounded L1 cache decorator and invalidation subscriber/reconciler.
- Create framework-free cache observation events and Micrometer adapter instrumentation.
- Update `docs/registries/metrics.yaml`.
**Tests first**
- 두 pod simulation에서 정상 시 refresh owner는 하나다.
- lease expiry에서는 duplicate load를 허용하지만 generation guard가 stale write를 차단한다.
- disconnected invalidation subscriber는 L1을 flush하고 generation을 재확인한다.
- Pub/Sub event loss에도 L1 TTL/generation reconciliation으로 stale bound를 지킨다.
- L1 max weight/cardinality/TTL, subscriber queue, refresh scheduler가 모두 bounded다.
- Redis liveness는 애플리케이션 liveness를 내리지 않는다.
- optional cache outage는 `DEGRADED`, required coordination/session outage는 `NOT_READY`다.
- cache role eviction/OOM에서 source concurrency와 queue가 bounded다.
## Task 6 — Edge rate limit end-to-end
**Files**
- Modify: `src/shared-contract/src/main/java/dev/caskeleton/shared/ratelimit/*`
- Modify: `src/adapter/inbound/web/src/main/java/dev/caskeleton/adapter/inbound/web/ratelimit/*`
- Modify: `src/adapter/outbound/cache-redis/src/main/java/.../redis/*rate*`
- Modify: `src/app-bootstrap` composition
**Tests first**
- inbound가 process-local map이 아니라 `EdgeRateLimitPort`를 호출한다.
- subject는 raw principal/IP가 아닌 bounded pseudonymous digest다.
- fixed/sliding-counter/token-bucket reference/property/concurrency vector를 통과한다.
- evaluation ID replay가 quota를 두 번 소비하지 않는다.
- bounded local emergency는 configured degraded provider일 때만 동작한다.
- Redis/local/disabled provider exclusivity, shadow/degraded source, 429/503와 `Retry-After` mapping을
검증한다.
- legacy unbounded map과 silent primary fallback을 제거한다.
## Task 7 — Idempotency v2와 Redis provider
**Files**
- Replace/extend `src/application-core/.../idempotency` with owner-safe v2 contracts.
- Add Redis idempotency state programs/provider/codec.
- Migrate the existing JPA provider to the same semantic contract only after checking its separate
worktree changes; never overwrite concurrent persistence work.
**Tests first**
- atomic claim, fingerprint mismatch, owner/attempt-safe start/renew/complete/fail/release/inspect.
- processing TTL과 replay TTL 분리.
- expired `CLAIMED` takeover, expired `EXECUTING -> RECOVERY_REQUIRED`.
- response-loss replay/reconciliation, conflicting response digest reject.
- unverified cross-store effect는 자동 discard/re-execution하지 않는다.
- JDBC/Redis provider가 같은 scope를 동시에 claim하지 않는다.
**Implementation**
- Redis가 cross-store exactly-once를 보장한다고 표현하지 않는다.
- JPA migration 충돌이 있으면 Redis completion의 명시적 integration blocker로 보고하고 해당
worktree의 결과와 재대조한다.
## Task 8 — Efficiency lease와 optional fenced coordination
**Tests first**
- acquire/inspect/renew/release가 owner+operation token을 비교한다.
- response loss는 `UNKNOWN/INDETERMINATE`이며 same token inspect로 reconcile한다.
- expired old owner는 renew/release할 수 없다.
- watchdog는 bounded scheduler와 cancellation을 사용하고 lost 상태를 전달한다.
- fenced card를 선택하면 durable epoch/high-watermark 등록과 protected-resource stale-token reject를
실제 fixture로 증명한다.
**Implementation**
- close-only `DistributedLock`은 compatibility facade로 유지하되 새 코드가 strong lock으로
오해하지 않게 guarantee를 명명한다.
- fencing 없는 Redis lease를 business correctness lock으로 광고하지 않는다.
## Task 9 — Redis Session과 JWT/session exclusive composition
**Files**
- Add direct `spring-session-core` and `spring-session-data-redis` to Redis leaf.
- Add adapter-internal versioned session store/programs/serializer.
- Add inbound web cookie/CSRF/fixation settings and security configuration.
- Add app-bootstrap `jwt|redis-session` exclusive composition.
**Tests first**
- JWT mode는 session Redis connection/bean/thread side effect가 0이다.
- pod A create/save, pod B read/touch/logout.
- idle/absolute expiry, rotation, old ID reject, stale save after logout reject.
- explicit allowlisted serializer N/N-1 and corrupt payload re-auth.
- secure/httpOnly/SameSite/host-only cookie, CSRF enabled, fixation rotation.
- repository outage/noeviction OOM/failover는 fail-open 인증으로 바뀌지 않는다.
- indexed repository는 별도 opt-in이며 Cluster event cleanup 한계를 독립 검증한다.
## Task 10 — Real-service, topology, fault와 readiness Gradle tasks
**Files**
- Create: `src/adapter/outbound/cache-redis/src/redisTest/**`
- Modify: `src/adapter/outbound/cache-redis/build.gradle`
- Modify: `src/build.gradle`
- Create/update Redis test topology resources and sanitized evidence reporter
**Public tasks**
- `redisStandaloneTest`, `redisSecurityTest`, `redisSentinelTest`, `redisClusterTest`,
`redisFaultTest`, `redisCompatibilityTest`
- capability card test/readiness tasks named exactly as Redis deep design §37.22
- root `redisProductionReadiness`, `redisAllImplementedCandidates`
**Rules**
- selected evidence에서 Docker/service 부재나 0 discovered tests는 failure다.
- unselected card는 skipped가 아니라 `not selected`다.
- image/program/config digest와 sanitized JUnit/topology timeline을 evidence artifact로 남긴다.
## Task 11 — Container topology와 3-node k3s qualification
이번 실행은 deep design §37.13/Phase 5A의 Sentinel-first slice만 다룬다. Cluster, fenced
coordination, R3 long chaos/soak, k3s control-plane HA, physical host/AZ failure, full
credential/certificate rotation은 후속 작업이다.
### Task 11.1 — Lab lifecycle contract와 host isolation RED
이 작업은 리뷰 경계를 다음처럼 분리한다. 두 하위 작업이 모두 독립 리뷰를 통과하기 전에는 부모
Task 11.1을 완료로 표시하지 않는다.
- `Task 11.1A-1`: VM lifecycle, ownership marker/state, lock/signal/handoff cleanup, host
fingerprint와 bounded command. 현재 구현을 동결한다.
- `Task 11.1A-2`: pinned K3s generated-kubeconfig strict validator/renderer. 실행 계획은
`docs/superpowers/plans/2026-07-30-redis-lab-strict-kubeconfig-renderer.md`를 따른다.
2026-07-30 상태: `Task 11.1A-1` lifecycle/ownership과 `Task 11.1A-2` strict renderer는
whole-task 독립 review에서 Critical `0`, Important `0`, Minor `0`, SPEC PASS /
QUALITY APPROVED를 받았다. fresh direct/Gradle fake-only 검증도 통과해 부모 `Task 11.1A`
fake-only 범위는 완료다. 이는 live VM/k3s/kubectl/network/host qualification이나 Redis
R2 readiness 완료를 의미하지 않는다.
**Tracked files**
- Create: `infra/redis-lab/README.md`
- Create: `infra/redis-lab/versions.env`
- Create: `infra/redis-lab/bin/redis-lab`
- Create: `infra/redis-lab/cloud-init/node.yaml`
- Create: `infra/redis-lab/test/redis-lab-contract.sh`
- Modify: Redis Gradle VM-free lifecycle contract task
**Tests first**
- VM 이름은 `ca-redis-lab-server`, `ca-redis-lab-agent-1`,
`ca-redis-lab-agent-2` exact allowlist만 허용한다.
- server 1 + agent 2, resource `2/3GiB/12GiB`, `2/2.5GiB/12GiB`,
`2/2.5GiB/12GiB`, pod CIDR `10.52.0.0/16`, service CIDR
`10.53.0.0/16`, context `ca-redis-lab`을 검증한다.
- host 관측은 default kubeconfig의 run-scoped copy와 원래 host context를 사용하고 read-only
allowlist만 허용한다. lab 호출은 별도 ignored `src/build/redis-lab/kubeconfig`와 exact
`ca-redis-lab` context를 사용한다.
- default kubeconfig merge/write, host context mutation, wildcard VM cleanup, global
`multipass purge`를 정적/동적 contract가 거절한다.
- preflight/postflight host kubeconfig/context/node/workload fingerprint가 다르면 실패한다.
- CI는 retain-on-failure를 거절하고, local opt-in만 exact VM 보존을 허용한다.
- fake `multipass`/`kubectl`을 주입하는 shell contract는 partial-create cleanup과 exact command
allowlist를 VM 생성 없이 검증하고 `redisLabContractTest`로 module `check`에 연결한다.
- launch 전 exact name을 run-owned `PENDING`으로 atomic 예약하고 성공 직후 `CREATED`
승격한다. timeout/실패/상태 승격 실패는 이 run이 예약한 exact name만 정리한다.
- private run-scoped rendered cloud-init은 non-secret `RUN_ID|VM_NAME` ownership marker를
기록한다. cleanup/down은 bounded marker read가 state owner와 exact name 일치를 증명할
때만 delete한다. launch timeout/error는 `RECONCILE` tombstone과 bounded late-create poll로
처리하며 absent/unreadable/mismatch는 delete/state removal 없이 fail-closed한다.
- lifecycle 전체는 nonblocking exclusive lock과 run identity를 사용한다. direct `up`
`run` 모두 첫 launch 전 emergency cleanup을 활성화하고, signal/concurrent 실행이 다른
run state나 VM을 채택·삭제하지 못한다. user command에는 lock file descriptor를 상속하지
않으며 기본 bounded external child도 FD를 닫고 lock acquisition만 예외로 유지한다.
`run`의 inner `up` 성공과 user command 시작 사이에도 cleanup-required flag가 연속 유지돼
zero-ownership handoff gap이 없어야 한다.
- host kubeconfig copy는 fingerprint/CIDR 관측 범위가 끝나면 성공/실패와 무관하게 제거한다.
- lab kubeconfig renderer는 denylist/generic-count 보강을 사용하지 않는다. pinned K3s의
canonical block-style one-cluster/context/user grammar를 별도 tracked AWK state machine으로
allowlist하며, catch-all pass-through 없이 duplicate/extra/reordered/unknown/flow-style
identity와 모든 비허용 구조를 fail-closed로 거절한다.
- external command와 3-node Ready 대기는 bounded이고, host service CIDR은 assigned
ClusterIP에서 추측하지 않고 명시적 validated input 또는 신뢰 가능한 host 설정에서 얻는다.
- mutable `curl | sudo sh` installer는 금지한다. exact K3s release URL과 SHA-256을 repository에
pin하고 host download와 각 VM transfer 뒤 다시 검증한 후에만 install/start한다.
- shell contract는 별도 fixture repository에서 실행하고 actual `src/build/redis-lab` canary를
byte-for-byte 보존한다. fake PATH는 explicit safe wrapper 외 모든 명령을 fail-closed한다.
### Task 11.2A — Sentinel manifest와 security static contract GREEN
**Tracked files**
- Create: `infra/redis-lab/config/redis.conf.tmpl`
- Create: `infra/redis-lab/config/sentinel.conf.tmpl`
- Create: `infra/redis-lab/config/redis-users.acl.tmpl`
- Create: `infra/redis-lab/config/sentinel-users.acl.tmpl`
- Create: `infra/redis-lab/k3s/namespace.yaml`
- Create: `infra/redis-lab/k3s/redis-data.yaml`
- Create: `infra/redis-lab/k3s/redis-sentinel.yaml`
- Create: `infra/redis-lab/k3s/network-policy.yaml`
- Create:
`src/adapter/outbound/cache-redis/src/test/java/dev/caskeleton/adapter/outbound/cache/redis/RedisLabManifestContractTest.java`
- Modify: Redis Gradle manifest contract task
- static contract와 live security evidence를 분리한다. YAML/템플릿 정적 통과는 TLS handshake,
ACL authorization, CNI enforcement, scheduling/failover의 실행 증거가 아니다.
- data Redis 3개와 Sentinel 3개는 각각 stable ordinal/headless DNS가 필요한 StatefulSet으로
구성하고 `kubernetes.io/hostname` required anti-affinity와 `maxSkew=1/DoNotSchedule`
topology spread, `podManagementPolicy: Parallel`을 적용한다.
- data는 PVC + AOF `appendfsync everysec`를 사용한다. Sentinel은 공식 동작상 writable config에
discovery/failover 상태를 rewrite하므로, bootstrap source를 pod별 writable PVC config로
최초 1회 atomic init-copy하고 restart 때 기존 rewritten config를 덮어쓰지 않는다.
비어 있거나 손상된 기존 config는 자동 복구로 덮지 않고 startup을 실패시킨다.
- Redis image SSOT는 `src/gradle/redis-test-images.properties`
`redis.minimum.image` exact tag+digest다. `redis.approved.image`나 임의 YAML image를 이
minimum-version Sentinel slice에 섞지 않는다.
- plaintext port는 data/Sentinel 모두 0이고 TLS port만 연다. `tls-replication yes`,
hostname resolution/announcement와 certificate SAN용 stable DNS를 사용한다. data plane과
Sentinel plane은 서로 다른 CA/leaf material을 가지며, peer 연결에 필요한 root만 명시적
trust bundle로 교차 포함한다.
- ACL identity를 하나의 `redis-user`로 합치지 않는다.
- application data user: 선택 capability/program command/key/channel만;
- replica user: `+psync +replconf +ping`;
- Sentinel-to-data user: 공식 최소 Sentinel control command/channel set;
- Sentinel peer user: Sentinel 간 통신에 필요한 동일 superuser credential;
- application Sentinel discovery user: auth/hello/ping/role과 allowlisted read-only
`SENTINEL` subcommand만.
default user는 off이며 application/data/discovery user에 `+@all`, `allkeys`,
`allchannels`를 주지 않는다.
- Redis data ACL과 Sentinel ACL은 별도 template/projection이다. Sentinel peer superuser가
data Redis에, data capability user가 Sentinel에 존재하면 static contract가 실패한다.
- Secret/CA/private key/rendered config는 run별 `umask 077` 아래 생성하고 tracked manifest에는
Secret value, PEM, password가 없다. probe/command line에 `--pass`를 쓰지 않는다.
- exec probe를 사용해 kubelet source CIDR 예외를 만들지 않는다. default-deny ingress/egress
뒤 data 6379, Sentinel 26379, kube-dns와 exact qualification/application pod selector만
허용한다.
- Service는 headless/ClusterIP만, PDB는 data/Sentinel 각각 `minAvailable: 2`, container는
non-root, read-only root filesystem, privilege escalation false, capabilities drop ALL,
seccomp RuntimeDefault, explicit requests/limits를 요구한다.
- structural positive test와 한 필드씩 제거/변조한 mutation-negative fixture가
anti-affinity, spread, PDB, probes, TLS-only, ACL separation, Secret reference,
NetworkPolicy, image SSOT를 실제로 fail시키는지 검증한다.
- `hostPath`, `hostNetwork`, `hostPID`, `hostIPC`, privileged, NodePort, LoadBalancer,
tracked Secret data/stringData/PEM과 implicit latest image를 거절한다.
- static validator는 exact document inventory, duplicate YAML key/identity, selector/template
일치, exact NetworkPolicy edge graph를 검증한다. 정적 ordinal bootstrap은 최초
`redis-data-0` primary와 두 replica만 증명하며, failover 뒤 old-primary 재합류와 stale
direct write 차단은 live gate에 남긴다.
### Task 11.2B — Sentinel workload와 live security baseline GREEN
- Redis primary 1 + replica 2와 Sentinel 3/quorum 2를 세 node에 분산한다.
- anti-affinity/topology spread, PDB, NetworkPolicy, separate data/Sentinel CA와 named ACL을
적용한다.
- secret/certificate/k3s token은 매 run `umask 077` transient material로 생성하고 tracked
manifest에는 값/PEM을 넣지 않는다. Sentinel bootstrap config는 Secret volume에서 pod별
writable PVC로 최초 1회 atomic init-copy하며, 기존 rewritten config를 덮어쓰지 않는다.
- Redis image는 `redis.minimum.image` exact image/digest를 render하고 실제 pod image
ID/digest가 일치하는지 수집한다.
- data credential/CA로 Sentinel discovery가 실패하고 Sentinel material로 data command가
실패하는 negative test, untrusted CA/hostname mismatch/plaintext rejection을 실행한다.
- `SENTINEL CKQUORUM`, writable config rewrite/restart, exact 3 Ready placement, PDB,
default-deny/explicit-allow NetworkPolicy enforcement를 live k3s에서 검증한다.
- failover 중 죽어 있던 old primary가 재합류할 때 readiness가 stale direct write를 허용하지
않고 새 primary의 replica로 수렴하는지 live 검증한다.
### Task 11.3 — Sentinel client runtime TDD
- current `UnsupportedOperationException`을 먼저 고정하는 test를 quorum-consistent discovery와
분리된 discovery/data material contract로 교체한다.
- 2-of-3 Sentinel이 같은 primary를 보고할 때만 후보를 만들고 loopback/wildcard/unexpected
endpoint를 거절한다.
- active Sentinel role이 있을 때만 registry당 daemon worker 1개, role당 fixed-delay task 1개를
만들고 `sentinel-discovery-refresh-period`(기본 30초, 5초..5분)를 적용한다.
- scheduled poll과 command failure-triggered immediate rediscovery는 role별 같은 single-flight를
공유한다. `snapshot()`은 보조 trigger일 뿐 정상 polling을 대신하지 않는다.
- 정상 poll은 Sentinel material만 해석하고 현재 route identity와 같으면 data material/client를
만들지 않는다. 바뀐 quorum-approved endpoint에만 data candidate를 연다.
- command failure listener는 route lease 반환 뒤 topology/connectivity `UNAVAILABLE`에만
동작하며 listener 실패가 원래 certainty를 덮어쓰지 않는다.
- 새 data runtime은 version/program/semantic readiness를 통과한 뒤 router에 install한다.
- opaque route identity와 monotonic generation token으로 stale/same-primary candidate를
거절하고, install된 경우 old runtime은 new admission을 닫고 bounded drain/close한다.
- close는 task/worker를 bounded 종료하고 late candidate를 install하지 않고 정확히 한 번 닫는다.
- mutation을 자동 replay하지 않고 실행 여부가 불명확하면 `INDETERMINATE`를 보존한다.
### Task 11.4 — Multi-pod normal/failover qualification
1. host/lab preflight와 3 node/Sentinel quorum readiness를 수집한다.
2. 서로 다른 application pod에서 rate limit evaluation replay, idempotency
claim/start/renew/complete, session create/read/touch/rotate/revoke를 검증한다.
3. current primary pod를 kill하고 readiness unavailable timestamp를 기록한다.
4. Sentinel quorum election, client rediscovery, runtime generation swap/drain, semantic
readiness recovery를 실제 순서대로 기록한다.
5. election 60초, 추가 rediscovery/swap 30초, 총 recovery 90초의 regression limit을 적용한다.
6. rate state가 조용히 reset되지 않고 idempotency owner/terminal 결과가 중복되지 않으며
confirmed session state가 유지되는지 확인한다.
7. old primary의 replica 재합류와 모든 actor의 동일 generation 관측을 확인한다.
correctness role에는 bounded `min-replicas-to-write`/`min-replicas-max-lag`와 명시적 replica
acknowledgement policy를 사용한다. zero-data-loss/strong consistency를 주장하지 않으며
response-only cut 등 실행 여부가 불확실한 mutation은 `INDETERMINATE`이고 blind retry하지 않는다.
### Task 11.5 — Evidence와 exact teardown
- actual image digest/image ID, config/program digest, sanitized fault/election/recovery timeline,
capability별 outcome/certainty, Kubernetes/Sentinel 관측을 allowlist schema로 생성한다.
- `NOT_CAPTURED` placeholder는 qualification 성공으로 인정하지 않는다.
- sanitizer/reconciler 성공 뒤에도 human clean commit/remote CI 전에는
`releaseQualification=NOT_CLAIMED`를 유지한다.
- 성공/실패 모두 exact VM allowlist를 teardown하고 lab resource가 0인지 확인한다. local
retain-on-failure opt-in은 명시된 경우만 허용하고 CI에서는 금지한다.
## Task 12 — CI, runbook, verification와 Wiki capture
**CI**
- PR blocking `redis-standalone` job을 `release-gate.needs`와 result loop에 실제 포함한다.
- nightly/release Redis production readiness workflow를 추가한다.
- workflow contract test로 blocking job/aggregator 집합 동등성을 검증한다.
**Verification**
```bash
cd src
./gradlew :application-core:redisPolicyContractTest --console=plain
./gradlew :shared-contract:edgeRateLimitContractTest --console=plain
./gradlew :adapter:outbound:cache-redis:check --console=plain
./gradlew :app-bootstrap:redisCompositionTest --console=plain
./gradlew redisProductionReadiness --console=plain
./gradlew test --console=plain
./gradlew check --console=plain
./gradlew verifyCleanArchitectureDependencies --console=plain
./gradlew verifyPublicPathSnapshot --console=plain
./gradlew verifyEnvKeys --console=plain
```
**Documentation**
- capability별 실제 readiness와 남은 R3 한계를 README/spec/runbook에 동기화한다.
- 실행 명령, image/config/program digest, 실패/차단을 public LLM Wiki
`/home/donghyeon/workspace/ai-tools/llm-wiki/raw/branch-notes/main.md`
기록하고 실제 파생 오류/면접/블로그 raw 문서를 양방향 링크한다.
**Completion gate**
- Task 11의 exit gate를 통과하면 `Sentinel-first R2-ready candidate`라고만 보고한다.
- clean committed source와 실제 remote CI가 없으면 selected/R2로 승격하지 않는다.
- 이 milestone 보고 뒤 멈추고 Cluster/R3/fenced coordination 또는 fileserver/HTTP client 중
다음 작업을 사용자와 다시 정한다.
## Task 13 — Resume blocker: selection-driven role activation과 default boot
**Problem**
- provider definition뿐 아니라 role binding도 capability가 선택되지 않으면 inert여야 한다.
- 현재 구현은 role binding 전체를 runtime으로 열고 health contributor도 role property 존재만으로
활성화한다.
- local 기본값에서 inbound rate-limit은 provider 없이 활성화되면 안 된다.
**Tests first**
- CACHE/COORDINATION/SESSION deployment와 role을 모두 사전 선언해도 cache/rate/idempotency/lease/
session capability가 비활성이면 credential/trust resolution, native client, scheduler/subscriber,
Redis health contributor가 모두 0이다.
- 각 capability가 `redis`를 선택할 때만 해당 role이 활성화된다.
- 같은 role을 쓰는 coordination capability 둘 이상은 하나의 runtime만 공유한다.
- 선택 capability의 role binding이 빠지면 material resolution 전에 startup이 실패한다.
- shipped `.env`와 실제 `application.yml`은 transport disabled/provider disabled 조합으로 기동
가능하고 중복 legacy rate-limit block이 없다.
**Implementation**
- deployment/role registry validation과 runtime activation을 분리한다.
- `selectedCapabilities`가 비어 있는 role은 registry/router/health에서 제외한다.
- bootstrap health condition도 role property가 아니라 effective selected capability로 판단한다.
- provider 설정은 inert 후보로 남기되 선택된 capability의 잘못된 role은 fail closed 한다.
## Task 14 — Resume blocker: capability-aware semantic readiness
**Problem**
- PING만으로 `AVAILABLE/PROBE_SUCCEEDED`를 선언하지 않는다.
- required coordination/session은 실제 선택 capability의 program ACL과 최소 read/write 계약이
동작해야 ready다.
**Tests first**
- PING은 성공하지만 `SCRIPT LOAD`/`EVALSHA`가 ACL로 거절된 coordination/session user는
`redisRequired=DOWN`이다.
- capability별 representative program의 실제 key count와 command-to-key mapping을 그대로
검증한다. rate-limit의 state/dedup/order key와 session tombstone key 중 하나만 ACL pattern에서
빠져도 semantic readiness는 실패한다.
- Redis 7.2 미만 server는 metadata 표기만으로 통과하지 않고 bounded runtime handshake에서
sanitized unsupported-version 상태가 된다.
- 대표 program과 ACL probe script가 이미 warm인 상태에서도 runtime user의 `SCRIPT LOAD`
권한 누락을 별도로 탐지한다.
- cache optional role에서 semantic probe 실패는 application liveness/readiness를 내리지 않고
`DEGRADED`만 보고한다.
- 선언된 optional cache가 cold-start connect/PING에 일시 실패해도 context는 bounded unavailable
route로 시작하고, health-triggered bounded single-flight reconnect 뒤 재시작 없이 복구한다.
invalid configuration/material/program/schema는 계속 startup failure이며 required
coordination/session은 fail closed다.
- probe는 raw key/value, credential, server exception을 health detail에 노출하지 않는다.
- probe key는 bounded, namespaced, TTL이 있고 성공/실패 후 잔여 상태가 없다.
- saturation/recent command failure/closed route를 distinct sanitized reason으로 분류한다.
- health scrape는 role별 minimum cadence와 single-flight로 full semantic suite 실행을 제한하고,
cached observation의 시각/age를 노출해 stale success를 숨기지 않는다.
**Implementation**
- role별 선택 capability를 입력으로 immutable semantic probe plan을 만든다.
- probe는 catalog-owned bounded program과 capability-safe ephemeral operation만 사용한다.
- optional cold-start outage는 resource-free unavailable runtime과 bounded on-demand reconnect로
표현하며 별도 unbounded scheduler/thread를 만들지 않는다. L1 invalidation subscription은
route recovery 시 실제 runtime에 다시 연결된다.
- eviction은 runtime `CONFIG` 권한을 열지 않고 `CONFIGURED_EXPECTATION_ONLY`로 유지하며 외부
attestation 미완료를 readiness detail에 명시한다.
## Task 15 — Resume blocker: bounded common primitive catalog
**Problem**
- Deep design §14.6–§14.9의 자주 쓰는 race-safe helper가 아직 compare/delete 중심 R0 foundation에
머물러 있다.
**Tests first**
- String, counter, hash, set, sorted-set, list baseline은 typed/versioned key, value/count/byte/deadline,
role, slot, TTL, certainty bound를 강제한다.
- bitmap/HLL/geo는 billing/auth correctness에 사용할 수 없는 explicit semantic classification과
offset/result/fan-in bound를 강제한다.
- `INCR -> EXPIRE`, set/list admission, revision-CAS는 실제 Redis concurrency에서 atomic하다.
- unbounded `HGETALL`, `SMEMBERS`, `LRANGE`, arbitrary command/script surface는 제공하지 않는다.
**Implementation**
- package-private `RedisPrimitiveCatalog`과 structure별 bounded facade를 Redis leaf 내부에 둔다.
- application/shared public API에는 Redis command나 raw key를 노출하지 않는다.
- 아직 실제 semantic consumer가 없는 primitive는 Spring bean/public capability로 노출하지 않는다.
## Task 16 — Resume blocker: capability observability와 graceful lifecycle
**Tests first**
- cache/rate/idempotency/lease/session의 operation, outcome, certainty, role, queue/latency가 bounded
low-cardinality metric/event로 관측된다.
- raw key, subject, session/idempotency/lease token, secret reference/value, exception message는
tag/log/trace에 들어가지 않는다.
- optional cache와 required coordination/session의 failure signal이 health와 metric에서 일치한다.
- shutdown은 subscriber/scheduler/router/runtime 순서로 bounded drain되고 새 command를 거절한다.
**Implementation**
- framework-neutral observation event/port와 Micrometer rendering을 계층 소유권에 맞게 둔다.
- trace/log는 기존 skeleton observability 경계를 재사용하고 Redis native type을 core에 유출하지
않는다.
- `docs/registries/metrics.yaml`과 runbook을 실제 emitted metric과 동기화한다.
## Task 17 — Resume final review, readiness truth, verification와 Wiki
- Task 1316을 task별 spec/code-quality review한다.
- Redis deep design §39/§40을 독립 재검토해 selected/implemented-candidate/not-implemented를 실제
evidence와 일치시킨다.
- Sentinel/Cluster/k3s/R3 evidence가 없으면 지원/완료로 표기하지 않는다.
- Task 12의 전체 검증을 실행하고 동시 작업의 비-Redis 실패는 소유 파일과 증거를 분리한다.
- Redis README/spec/runbook, readiness registry, CI artifact 계약을 동기화한다.
- LLM Wiki branch-note와 실제 파생 raw 문서를 양방향 링크로 캡처한다.
@@ -0,0 +1,241 @@
# Redis Lab Strict Kubeconfig Renderer Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use
> `superpowers:subagent-driven-development` to implement this plan task-by-task. Steps use checkbox
> (`- [ ]`) syntax for tracking.
**Goal:** Complete parent Task 11.1A by replacing mutation-by-mutation kubeconfig filtering with a
pinned-K3s, strict block-grammar validator/renderer and passing an independent safety review.
**Architecture:** Freeze the already-reviewed lifecycle/ownership state machine as Task 11.1A-1.
Move kubeconfig validation/rendering into one tracked AWK program, Task 11.1A-2. The program accepts
only the exact single-cluster/context/user block grammar emitted by the pinned K3s slice, transforms
only lab identity fields, and rejects every non-allowlisted structure before any lab `kubectl`
command.
**Tech Stack:** Bash 5 strict mode, POSIX-compatible AWK features already used by the repository,
the fake-command shell contract, Gradle 9, Java 21.
## Global Constraints
- Do not create a VM, run real Multipass/k3s/kubectl, inspect host inventory, or access the network.
- Do not modify Task 11.1A-1 ownership, state, signal, lock, cleanup or fingerprint behavior.
- Do not add `yq`, PyYAML, Ruby, Java YAML runtime, or another downloadable parser dependency.
- The only accepted source grammar is the pinned K3s admin kubeconfig block-style shape defined in
deep design §37.13.4.1.
- `preferences: {}` is the only permitted flow collection.
- Validation failure removes the destination, emits only `redis-lab: lab kubeconfig invalid`, and
occurs before lab `kubectl`.
- Preserve prior `CREATED|RECONCILE` state and delete only exact marker-proven current-run VMs.
- Tests must show RED against the current implementation before production changes.
- Human-only Git policy applies: do not stage, commit, amend or push.
---
### Task 1: Extract a strict generated-kubeconfig renderer
**Files:**
- Create: `infra/redis-lab/lib/render-kubeconfig.awk`
- Modify: `infra/redis-lab/bin/redis-lab`
- Modify: `infra/redis-lab/test/redis-lab-contract.sh`
**Interfaces:**
- Consumes: `awk -v address=<validated IPv4> -v target=ca-redis-lab -f <renderer> <source>`.
- Produces: rendered kubeconfig on stdout and exit `0`, or no accepted output and non-zero exit.
- Integration: `render_lab_kubeconfig <source> <destination> <server-address>` performs atomic
temporary render, mode `0600`, destination replacement only after renderer success.
- [x] **Step 1: Add realistic positive and sibling-flow RED fixtures**
Change the fake `valid` kubeconfig to this complete credential-data shape, using canary values
rather than real certificate material:
```yaml
apiVersion: v1
clusters:
- cluster:
certificate-authority-data: preserve-default-ca-canary
server: https://127.0.0.1:6443
name: default
contexts:
- context:
cluster: default
namespace: team-default
user: default
name: default
current-context: default
kind: Config
preferences: {}
users:
- name: default
user:
client-certificate-data: preserve-default-client-cert-canary
client-key-data: preserve-default-client-key-canary
```
Add separate public `up` variants containing, after their canonical item:
```yaml
cluster : {server: https://foreign.invalid:6443}
```
and:
```yaml
context : {cluster: foreign, user: foreign}
```
Each variant must assert failure, zero lab `kubectl`, three exact marker-proven deletes, removed
rendered kubeconfig, and no forbidden fake invocation.
- [x] **Step 2: Run the direct contract and verify RED**
Run:
```bash
bash -n infra/redis-lab/bin/redis-lab infra/redis-lab/test/redis-lab-contract.sh
bash infra/redis-lab/test/redis-lab-contract.sh
```
Expected: syntax succeeds and the first new sibling-flow case fails because the current renderer
unexpectedly accepts it.
- [x] **Step 3: Implement the strict AWK state machine**
`render-kubeconfig.awk` must use an explicit `state` transition for every accepted line. It must
not print from a catch-all rule. The accepted transition sequence is:
```text
apiVersion -> clusters -> cluster-item -> ca-data -> server -> cluster-name
-> contexts -> context-item -> context-cluster -> optional-namespace -> context-user
-> context-name -> current-context -> kind -> preferences -> users -> user-name
-> user-body -> client-cert -> client-key -> EOF
```
Exact identity transitions print these replacements:
```awk
print " server: https://" address ":6443"
print " name: " target
print " cluster: " target
print " user: " target
print "current-context: " target
print "- name: " target
```
CA/client credential and namespace transitions print `$0` unchanged. Any unmatched line sets
`invalid=1`; `END` exits non-zero unless the final state is `client-key`, every required
transition occurred once, the input had no tab/CR/YAML marker, and no trailing line exists.
- [x] **Step 4: Integrate the renderer fail-closed**
Add:
```bash
KUBECONFIG_RENDERER="${REPOSITORY_ROOT}/infra/redis-lab/lib/render-kubeconfig.awk"
```
`validate_static_contract` must require a readable regular non-symlink renderer at that exact
canonical path. Replace the inline AWK body with:
```bash
local render_next="${destination_file}.next"
rm -f -- "${render_next}"
if ! awk -v address="${server_address}" -v target="${CONTEXT_NAME}" \
-f "${KUBECONFIG_RENDERER}" "${source_file}" >"${render_next}"; then
rm -f -- "${render_next}" "${destination_file}"
fail 'lab kubeconfig invalid'
return 1
fi
chmod 0600 -- "${render_next}"
mv -f -- "${render_next}" "${destination_file}"
```
Add the `.next` destination to symlink-child validation. Propagate `rm`, `chmod` and `mv`
failures with the same sanitized error and without retaining a partially accepted destination.
- [x] **Step 5: Run focused GREEN**
Run the direct contract again. Expected: `redis-lab-contract: PASS`, exit `0`.
### Task 2: Complete the mutation matrix and parent acceptance
**Files:**
- Modify: `infra/redis-lab/test/redis-lab-contract.sh`
- Modify: `infra/redis-lab/README.md`
- Modify: `docs/superpowers/plans/2026-07-29-redis-production-capability-completion.md`
- Modify:
`.superpowers/sdd/2026-07-29-redis-production-capability-completion/progress.md`
- Create:
`.superpowers/sdd/2026-07-29-redis-production-capability-completion/task-11-1a-2-brief.md`
- Create:
`.superpowers/sdd/2026-07-29-redis-production-capability-completion/task-11-1a-2-report.md`
**Interfaces:**
- Consumes: Task 1 strict renderer and existing lifecycle fake runtime.
- Produces: parent Task 11.1A review package with no open Critical/Important finding.
- [x] **Step 1: Add one mutation per grammar boundary**
Add table-driven fixture variants for missing, duplicate, reordered and unknown keys; whitespace
before colon; quoted/tagged/explicit keys; anchor/alias/merge; unexpected `{}`/`[]`; tab, CRLF,
`---`/`...`, and trailing content. Every case must assert failure before lab `kubectl`, exact
current-run cleanup and removed render output.
- [x] **Step 2: Prove scalar preservation and exact transformation**
The positive case must assert:
```text
server: https://192.0.2.10:6443
name/current-context: ca-redis-lab
namespace: team-default
preserve-default-ca-canary
preserve-default-client-cert-canary
preserve-default-client-key-canary
```
It must also assert that no `name: default`, `cluster: default`, `user: default`,
`current-context: default` or loopback server remains.
- [x] **Step 3: Re-run the full fake-only verification**
Run:
```bash
bash -n infra/redis-lab/bin/redis-lab infra/redis-lab/test/redis-lab-contract.sh
bash infra/redis-lab/test/redis-lab-contract.sh
cd src
./gradlew :adapter:outbound:cache-redis:redisLabContractTest --console=plain
./gradlew :adapter:outbound:cache-redis:test --console=plain
./gradlew :adapter:outbound:cache-redis:check --dry-run --console=plain
```
Expected: direct `PASS`; both Gradle executions `BUILD SUCCESSFUL`; dry-run includes
`redisLabContractTest`.
- [x] **Step 4: Run an independent scoped review**
Reviewer acceptance:
- strict renderer has no catch-all pass-through;
- the valid pinned fixture reaches EOF exactly once;
- every non-allowlisted structural line fails;
- destination publication is atomic/fail-closed;
- Task 11.1A-1 lifecycle code is unchanged except the renderer call and static path checks;
- Critical `0`, Important `0`, both spec and quality PASS.
- [x] **Step 5: Close the parent task**
Only after Step 4 passes, replace the ledger `BLOCKED` state with an additive resolution line:
```text
Task 11.1A-2: complete (human-only commit policy; strict renderer review clean)
Task 11.1A: complete (11.1A-1 lifecycle + 11.1A-2 renderer; fake-only evidence)
```
Do not claim live readiness, R2 or VM/k3s qualification.