feat: add production capability foundations
This commit is contained in:
@@ -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 1–5를 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 13–16을 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.
|
||||
Reference in New Issue
Block a user