chore: initialize from backend template 0a6dd0e
This commit is contained in:
@@ -0,0 +1,131 @@
|
||||
> **SUPERSEDED — HISTORICAL PROVENANCE ONLY (2026-07-25):** The user-approved harness-free
|
||||
> Mode B amendment supersedes this plan. Retain the body as historical provenance; it is not
|
||||
> executable instruction.
|
||||
|
||||
# Harness Policy Engine Implementation Plan
|
||||
|
||||
> **Spec:** `docs/superpowers/specs/2026-07-20-harness-policy-engine-design.md`
|
||||
|
||||
**Goal:** Replace topology- and platform-specific duplicated harness rules with a registry,
|
||||
strict evidence validators, generated platform variants, and risk-based review policies.
|
||||
|
||||
**Working policy:** human-only commits. Each task leaves changes in the working tree.
|
||||
|
||||
## Task 1 — Registry, resolver, and Gradle SSOT
|
||||
|
||||
**Files:**
|
||||
|
||||
- Add `.harness/project/modules.yaml`
|
||||
- Add `.harness/lib/module_registry.py`
|
||||
- Add `.harness/validators/validate_modules.py`
|
||||
- Add `.harness/tests/test_module_registry.py`
|
||||
- Modify `src/settings.gradle`
|
||||
- Modify the dependency-verifier section of `src/build.gradle`
|
||||
|
||||
**Steps:**
|
||||
|
||||
- [ ] Write failing tests for 19-leaf loading, nested owner resolution, nearest `CLAUDE.md`,
|
||||
unknown paths, and settings/registry parity.
|
||||
- [ ] Add the registry and stdlib loader/resolver.
|
||||
- [ ] Make Gradle settings and dependency verification consume registry data.
|
||||
- [ ] Run Python tests and `./gradlew projects verifyCleanArchitectureDependencies`.
|
||||
|
||||
## Task 2 — Registry-driven import gate and mutation suite
|
||||
|
||||
**Files:**
|
||||
|
||||
- Modify `.claude/hooks/ca_import_gate.py`
|
||||
- Modify `.claude/hooks/test_ca_import_gate.py`
|
||||
- Add `.harness/tests/test_import_gate_mutations.py`
|
||||
|
||||
**Steps:**
|
||||
|
||||
- [ ] Add failing real-path tests for every registered production module.
|
||||
- [ ] Replace flat-path regex/prefix rules with registry owner and role policy.
|
||||
- [ ] Normalize Claude snake_case and Antigravity camelCase tool events.
|
||||
- [ ] Fail closed on malformed in-scope events and marker failures.
|
||||
- [ ] Run all import-gate tests.
|
||||
|
||||
## Task 3 — Verdict schema, evidence artifacts, and platform adapters
|
||||
|
||||
**Files:**
|
||||
|
||||
- Add `.harness/schemas/verdict.schema.json`
|
||||
- Add `.harness/schemas/evidence.schema.json`
|
||||
- Add `.harness/lib/verdict.py`
|
||||
- Add `.harness/validators/validate_verdict.py`
|
||||
- Add `.harness/validators/validate_evidence.py`
|
||||
- Add `.harness/adapters/antigravity_hook.py`
|
||||
- Add `.harness/tests/test_verdict.py`
|
||||
- Modify `.claude/hooks/ca_verdict_gate.py`
|
||||
- Modify `.claude/hooks/test_ca_verdict_gate.py`
|
||||
- Add `.agents/plugins/ca-superpowers/hooks.json`
|
||||
|
||||
**Steps:**
|
||||
|
||||
- [ ] Write negative tests for missing required enums, negative counts, Gradle arithmetic,
|
||||
behavior change without red, missing upstream artifacts, malformed input, and revision
|
||||
mismatch.
|
||||
- [ ] Implement strict validation and evidence recording with source/diff hashes.
|
||||
- [ ] Adapt Claude fenced verdicts to the common model.
|
||||
- [ ] Add Antigravity Stop/pre-tool adapter and plugin hook wiring.
|
||||
- [ ] Run validator, adapter, and JSON syntax tests.
|
||||
|
||||
## Task 4 — Canonical agents and deterministic rendering
|
||||
|
||||
**Files:**
|
||||
|
||||
- Add `.harness/agents/*.md`
|
||||
- Add `.harness/project/platforms.yaml`
|
||||
- Add `.harness/generators/render_agents.py`
|
||||
- Add `.harness/tests/test_platform_parity.py`
|
||||
- Regenerate `.claude/agents/*`, `.codex/agents/*.toml`, `.agents/agents/*/agent.json`
|
||||
- Update `.agents/plugins/ca-superpowers/README.md` and `plugin.json`
|
||||
- Update `.codex/agents/README.md`
|
||||
|
||||
**Steps:**
|
||||
|
||||
- [ ] Seed canonical sources from the newest human-only Claude policy, then update module
|
||||
discovery and runner validation to use the registry.
|
||||
- [ ] Add generated metadata and stable output ordering.
|
||||
- [ ] Render all variants and add a `--check` parity mode.
|
||||
- [ ] Assert commit policy, source hashes, tool permissions, and body parity in tests.
|
||||
|
||||
## Task 5 — Risk/profile policies and guidance drift cleanup
|
||||
|
||||
**Files:**
|
||||
|
||||
- Add `.harness/manifest.yaml`
|
||||
- Add `.harness/core/risk-policy.yaml`, `.harness/core/evidence-policy.yaml`
|
||||
- Add current architecture/language/build/framework/capability profile files
|
||||
- Add `.harness/validators/resolve_task.py` and tests
|
||||
- Modify `AGENTS.md`, root `CLAUDE.md`, clean-architecture rule, workflow skill,
|
||||
advisory-depth rule, reporting-standards rule, and plugin README
|
||||
- Modify stale module `CLAUDE.md` files and add missing leaf-module guidance where useful
|
||||
|
||||
**Steps:**
|
||||
|
||||
- [ ] Add failing task-classification tests for high-risk one-file changes and low-risk
|
||||
multi-file fixture/docs changes.
|
||||
- [ ] Implement profile resolution.
|
||||
- [ ] Replace `N!`, routine all-quote grep, file-count report split, and unconditional
|
||||
counterargument policies with the design profiles.
|
||||
- [ ] Replace flat module documentation and focused commands with registry-backed nested names.
|
||||
- [ ] Run policy grep assertions and harness tests.
|
||||
|
||||
## Task 6 — Full review and verification
|
||||
|
||||
- [ ] Run harness unit/mutation/parity suite.
|
||||
- [ ] Run `./gradlew projects` and `./gradlew verifyCleanArchitectureDependencies`.
|
||||
- [ ] Run the focused ArchUnit suite.
|
||||
- [ ] Run `./gradlew check`.
|
||||
- [ ] Audit the working-tree diff in order: architecture → spec → quality.
|
||||
- [ ] Fix findings and restart the review chain, up to three loops.
|
||||
|
||||
## Task 7 — LLM Wiki capture
|
||||
|
||||
- [ ] Read the LLM Wiki authority and branch-note template.
|
||||
- [ ] Update/create the detached-HEAD branch note with implementation decisions, changed files,
|
||||
verification evidence, failures, and open risks.
|
||||
- [ ] Create/link derived error, interview, or blog-topic raw notes only when supported by the
|
||||
completed work; otherwise record an explicit “none” judgment in the branch note.
|
||||
+144
@@ -0,0 +1,144 @@
|
||||
# Application Outbox Failure Reporting Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development
|
||||
> (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use
|
||||
> checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Make `application-core` framework/logging-free while preserving one safe structured ERROR
|
||||
after each confirmed outbox FAILED/DEAD transition.
|
||||
|
||||
**Architecture:** The application owns a narrow typed reporting port and safe report value.
|
||||
Messaging renders the report through SLF4J, and bootstrap only injects it. Transition state remains
|
||||
authoritative; reporter failures are non-authoritative and contained.
|
||||
|
||||
**Tech Stack:** Java 21 records, JUnit Jupiter, AssertJ, Spring Boot 4 configuration, SLF4J 2 fluent
|
||||
logging, Logback capture tests, ArchUnit, Gradle Groovy DSL, dependency locking.
|
||||
|
||||
---
|
||||
|
||||
### Task 1: Safe Application Report Contract
|
||||
|
||||
**Files:**
|
||||
|
||||
- Create: `src/application-core/src/test/java/dev/caskeleton/application/outbox/OutboxRelayFailureReportTest.java`
|
||||
- Create: `src/application-core/src/main/java/dev/caskeleton/application/outbox/OutboxRelayFailureReport.java`
|
||||
- Create: `src/application-core/src/main/java/dev/caskeleton/application/outbox/OutboxRelayFailureReportPort.java`
|
||||
|
||||
- [ ] Write factory, invariant, and reflection-whitelist tests for the exact eight record components.
|
||||
- [ ] Run `./gradlew :application-core:test --tests '*OutboxRelayFailureReportTest' --console=plain`
|
||||
and record the expected missing-type RED.
|
||||
- [ ] Implement the immutable record, exact invariants, factories, and functional port.
|
||||
- [ ] Re-run the focused value test and record GREEN.
|
||||
|
||||
### Task 2: Relay Reporting Behavior
|
||||
|
||||
**Files:**
|
||||
|
||||
- Modify: `src/application-core/src/test/java/dev/caskeleton/application/outbox/PublishPendingOutboxEventsUseCaseTest.java`
|
||||
- Modify: `src/application-core/src/main/java/dev/caskeleton/application/outbox/PublishPendingOutboxEventsUseCase.java`
|
||||
- Modify direct test constructor sites under
|
||||
`src/app-bootstrap/src/test/java/dev/caskeleton/bootstrap/integration/outbox/`
|
||||
|
||||
- [ ] Add recording/throwing reporters and tests for exact FAILED/DEAD reports, all no-report paths,
|
||||
transition failure propagation, and reporter-failure continuation.
|
||||
- [ ] Run the relay test and record constructor/behavior RED.
|
||||
- [ ] Inject the reporter after the publish port, remove SLF4J, report only after successful
|
||||
transition, and contain reporter `RuntimeException`.
|
||||
- [ ] Update test-only direct constructors with explicit lambdas and re-run relay tests GREEN.
|
||||
|
||||
### Task 3: Structured Messaging Adapter and Publish-Adapter Deduplication
|
||||
|
||||
**Files:**
|
||||
|
||||
- Create:
|
||||
`src/adapter/outbound/messaging/src/test/java/dev/caskeleton/adapter/outbound/messaging/outbox/Slf4jOutboxRelayFailureReportAdapterTest.java`
|
||||
- Create:
|
||||
`src/adapter/outbound/messaging/src/main/java/dev/caskeleton/adapter/outbound/messaging/outbox/Slf4jOutboxRelayFailureReportAdapter.java`
|
||||
- Modify:
|
||||
`src/adapter/outbound/messaging/src/test/java/dev/caskeleton/adapter/outbound/messaging/outbox/OutboxMessagePublishAdapterTest.java`
|
||||
- Modify:
|
||||
`src/adapter/outbound/messaging/src/main/java/dev/caskeleton/adapter/outbound/messaging/outbox/OutboxMessagePublishAdapter.java`
|
||||
|
||||
- [ ] Write Logback capture tests for exact ERROR count, fixed fields, throwable, retry-only time,
|
||||
unsafe-data absence, internal logging failure containment, and the adapter contract that
|
||||
`report(null)` never throws.
|
||||
- [ ] Run
|
||||
`./gradlew :adapter:outbound:messaging:test --tests '*Slf4jOutboxRelayFailureReportAdapterTest' --console=plain`
|
||||
and record missing-type RED.
|
||||
- [ ] Implement the SLF4J 2 fluent adapter and re-run GREEN.
|
||||
- [ ] Replace outbox publish WARN expectations with no-log and propagation expectations; run RED.
|
||||
- [ ] Remove `FailOpenDependencyLogger` from the outbox adapter and re-run its tests GREEN, leaving
|
||||
`OutboundMessagePublisher` unchanged.
|
||||
|
||||
### Task 4: Unconditional Reporter Wiring
|
||||
|
||||
**Files:**
|
||||
|
||||
- Modify: `src/adapter/outbound/messaging/src/main/java/dev/caskeleton/adapter/outbound/messaging/MessagingConfig.java`
|
||||
- Modify: `src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/outbox/OutboxConfig.java`
|
||||
- Modify: `src/app-bootstrap/src/test/java/dev/caskeleton/adapter/outbound/OptionalAdapterBeanGatingTest.java`
|
||||
- Modify: `src/app-bootstrap/src/test/java/dev/caskeleton/bootstrap/outbox/OutboxConfigTest.java`
|
||||
|
||||
- [ ] Add disabled and active context assertions for exactly one structured reporter bean.
|
||||
- [ ] Run `OptionalAdapterBeanGatingTest` and record RED.
|
||||
- [ ] Add the unconditional messaging reporter bean, use `disabled` for blank broker, update outbox
|
||||
publish adapter construction, and inject the port through bootstrap.
|
||||
- [ ] Re-run the gating and outbox configuration tests GREEN.
|
||||
|
||||
### Task 5: Application Dependency Purity
|
||||
|
||||
**Files:**
|
||||
|
||||
- Modify: `src/build.gradle`
|
||||
- Modify: `src/application-core/build.gradle`
|
||||
- Mechanically regenerate only: `src/application-core/gradle.lockfile`
|
||||
|
||||
- [ ] Add `verifyApplicationCoreDependencyPurity`, wire it into `:application-core:check`, and run it
|
||||
against the current starter declaration to record RED.
|
||||
- [ ] Give `application-core` only JUnit Jupiter and AssertJ test dependencies while retaining the
|
||||
shared Boot test dependencies for every other leaf.
|
||||
- [ ] Remove the application Spring Boot starter and re-run the purity task GREEN.
|
||||
- [ ] Run
|
||||
`./gradlew :application-core:resolveAndLockAll --write-locks --console=plain`; confirm no other
|
||||
lockfile changes.
|
||||
- [ ] Run application lock verification, tests, and compile/test runtime dependency reports.
|
||||
|
||||
### Task 6: Non-Vacuous Diagnostic Architecture Rule
|
||||
|
||||
**Files:**
|
||||
|
||||
- Modify:
|
||||
`src/app-bootstrap/src/test/java/dev/caskeleton/bootstrap/architecture/CleanArchitectureTest.java`
|
||||
- Create:
|
||||
`src/app-bootstrap/src/test/java/dev/caskeleton/application/architecture/violations/ApplicationDiagnosticFrameworkViolation.java`
|
||||
- Modify:
|
||||
`src/app-bootstrap/src/test/java/dev/caskeleton/bootstrap/architecture/ArchitectureViolationFixtureTest.java`
|
||||
|
||||
- [ ] Add the violation fixture inside the exact `dev.caskeleton.application..` rule scope and its
|
||||
mutation assertion; run it before the rule to record RED.
|
||||
- [ ] Add `APPLICATION_HAS_NO_DIAGNOSTIC_FRAMEWORK`, scoped exactly to
|
||||
`dev.caskeleton.application..`, for SLF4J, JUL, Logback, Log4j, and Micrometer.
|
||||
- [ ] Run the mutation test and production `CleanArchitectureTest` GREEN.
|
||||
|
||||
### Task 7: Documentation and Verification
|
||||
|
||||
**Files:**
|
||||
|
||||
- Modify: `src/application-core/CLAUDE.md`
|
||||
- Modify: `src/application-core/README.md`
|
||||
- Modify: `src/adapter/outbound/messaging/CLAUDE.md`
|
||||
- Modify: `src/adapter/outbound/messaging/README.md`
|
||||
- Modify relevant wiring guidance in `src/app-bootstrap/README.md`
|
||||
|
||||
- [ ] Document the framework-free application contract, typed report semantics, messaging ownership,
|
||||
duplicate-log rule, and bootstrap wiring-only role.
|
||||
- [ ] Run focused application, messaging, gating, architecture mutation, production architecture,
|
||||
and available outbox integration tests.
|
||||
- [ ] Run `verifyCleanArchitectureDependencies`, dependency evidence reports, and `check`.
|
||||
- [ ] Run required safety greps, `git diff --check`, and `git status --short`; report any skip or
|
||||
remaining risk.
|
||||
- [ ] Hand the exact LLM Wiki capture responsibility and evidence back to the top-level controller;
|
||||
do not write the vault from this dispatched scope.
|
||||
|
||||
No step authorizes staging, committing, amending, pushing, public-path changes, CI changes, module
|
||||
registry changes, or `.harness` changes.
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,99 @@
|
||||
# Harness-Free Mode B Amendment Implementation Plan
|
||||
|
||||
> **For agentic workers:** Execute this plan task-by-task with
|
||||
> `superpowers:executing-plans`; use `superpowers:test-driven-development` for the build behavior
|
||||
> change and `superpowers:verification-before-completion` before reporting results.
|
||||
|
||||
**Goal:** Restore Gradle bootstrap and Clean Architecture dependency enforcement without recreating
|
||||
the absent development harness.
|
||||
|
||||
**Architecture:** One strict JSON registry under `src/config/architecture/` owns all 19 leaf
|
||||
identities, paths, and allowed production project edges. Gradle settings validate and include the
|
||||
registry fail-closed; the root dependency verification task reads the same file and checks actual
|
||||
production project dependencies against it.
|
||||
|
||||
**Tech Stack:** Gradle Groovy DSL, Groovy `JsonSlurper`, strict JSON, Java 21.
|
||||
|
||||
**Working policy:** Human-only git handling. Do not stage, commit, amend, push, or create a PR.
|
||||
|
||||
---
|
||||
|
||||
### Task 1: Capture the broken bootstrap
|
||||
|
||||
**Files:**
|
||||
|
||||
- Read: `src/settings.gradle`
|
||||
|
||||
- [x] Run `cd src && ./gradlew help --console=plain`.
|
||||
- [x] Confirm exit 1 is caused by the missing `.harness/project/modules.yaml`, not dependency
|
||||
resolution or an unrelated build failure.
|
||||
|
||||
### Task 2: Add the Gradle-owned registry
|
||||
|
||||
**Files:**
|
||||
|
||||
- Create: `src/config/architecture/modules.json`
|
||||
- Read: each of the 19 leaf-module `build.gradle` files
|
||||
|
||||
- [x] Record exactly 19 unique module IDs, Gradle paths, and repository-relative source paths.
|
||||
- [x] Set `allowed_dependencies` from each leaf's current `api`, `implementation`, `compileOnly`,
|
||||
and `runtimeOnly` project dependencies.
|
||||
- [x] Exclude test/fixture configurations from production policy and keep `sample-portfolio` a
|
||||
fixture consumer that no production leaf may depend on.
|
||||
- [x] Parse the file with Python's strict JSON parser and compare its edges with the checked-in
|
||||
leaf build declarations.
|
||||
|
||||
### Task 3: Restore Gradle bootstrap and dependency enforcement
|
||||
|
||||
**Files:**
|
||||
|
||||
- Modify: `src/settings.gradle`
|
||||
- Modify: `src/build.gradle`
|
||||
|
||||
- [x] Make settings load only `config/architecture/modules.json`.
|
||||
- [x] Fail closed on a missing registry, wrong root/module/field types, empty values, duplicate
|
||||
identities or paths, unsafe path shapes, unknown/self dependencies, count drift, or missing
|
||||
source directories.
|
||||
- [x] Include every registered Gradle path and map it to its repository-root-relative source
|
||||
directory.
|
||||
- [x] Make `verifyCleanArchitectureDependencies` read the same registry without a second module
|
||||
list.
|
||||
- [x] Preserve all-leaf coverage and forbidden-edge checks, explicitly reject a production edge
|
||||
to `sample-portfolio`, and replace stale error wording with actionable registry guidance.
|
||||
|
||||
### Task 4: Align active repository guidance
|
||||
|
||||
**Files:**
|
||||
|
||||
- Modify: `AGENTS.md`
|
||||
- Modify: `CLAUDE.md`
|
||||
- Modify: `README.md`
|
||||
- Modify: `src/README.md`
|
||||
- Modify: all 19 nearest leaf-module `CLAUDE.md` files that name the old registry
|
||||
- Annotate as superseded: the 2026-07-20 harness design and plan
|
||||
|
||||
- [x] Point active topology and allowed-edge guidance to
|
||||
`src/config/architecture/modules.json`.
|
||||
- [x] State that focused commands are derived from the owning Gradle path rather than a task
|
||||
packet.
|
||||
- [x] Keep all eight local HARD-STOP meanings, architecture boundaries, human-only git policy,
|
||||
verification discipline, and LLM Wiki capture requirements.
|
||||
- [x] Make the earlier harness documents explicit historical provenance rather than active
|
||||
reconstruction instructions.
|
||||
|
||||
### Task 5: Verify from a fresh Gradle invocation
|
||||
|
||||
**Files:**
|
||||
|
||||
- Verify: all changed files
|
||||
|
||||
- [ ] Run `cd src && ./gradlew help --console=plain`.
|
||||
- [ ] Run `cd src && ./gradlew projects --console=plain`.
|
||||
- [ ] Run `cd src && ./gradlew verifyCleanArchitectureDependencies --console=plain`.
|
||||
- [ ] Run a deterministic strict-JSON script proving exactly 19 unique IDs/Gradle paths and
|
||||
existing source directories.
|
||||
- [ ] Run a deterministic comparison between registry edges and leaf production project
|
||||
dependencies.
|
||||
- [ ] Run `git diff --check` and `git status --short`.
|
||||
- [ ] Report exact exits, any unavailable checks, LLM Wiki capture outcome, and remaining risks
|
||||
without claiming the broader Phase A/refactor is complete.
|
||||
@@ -0,0 +1,117 @@
|
||||
# Harness-Free Quality and Security CI Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use `superpowers:executing-plans` to implement this
|
||||
> plan task-by-task, `superpowers:test-driven-development` for executable drift controls, and
|
||||
> `superpowers:verification-before-completion` before reporting. Git remains human-only: do not
|
||||
> stage, commit, amend, or push.
|
||||
|
||||
**Goal:** Reconstruct a harness-free, repository-internal quality and dependency-security CI
|
||||
control plane that is truthful to the current Gradle build and `main` branch.
|
||||
|
||||
**Architecture:** Canonical workflows live only under `.github/workflows`. A small YAML gate matrix
|
||||
maps current controls to real Gradle tasks/plugins/tests and workflow jobs, while a portable Bash
|
||||
verifier rejects drift; vulnerability policy is enforced by a platform-neutral Trivy filesystem
|
||||
job with guarded GitHub-only complements.
|
||||
|
||||
**Tech Stack:** GitHub Actions-compatible YAML, Bash, Gradle 9 Groovy DSL, Java/Temurin 21, Trivy,
|
||||
jq, lychee.
|
||||
|
||||
---
|
||||
|
||||
### Task 1: Capture missing-control RED
|
||||
|
||||
**Files:**
|
||||
|
||||
- Verify absent: `.trivyignore.yaml`
|
||||
- Verify absent: `.github/ci-gate-matrix.yml`
|
||||
- Verify absent: `.github/scripts/verify-gate-matrix.sh`
|
||||
|
||||
- [ ] Run `cd src && ./gradlew verifyTrivyignore --console=plain`.
|
||||
- [ ] Confirm the failure names the missing repository-root `.trivyignore.yaml`.
|
||||
- [ ] Confirm the matrix, verifier, and canonical workflows are absent.
|
||||
|
||||
### Task 2: Add repository baselines
|
||||
|
||||
**Files:**
|
||||
|
||||
- Create: `.tool-versions`
|
||||
- Create: `.gitattributes`
|
||||
- Create: `.trivyignore.yaml`
|
||||
|
||||
- [ ] Pin `java temurin-21.0.11+10`, matching candidate evidence and the local Gradle launcher JDK.
|
||||
- [ ] Normalize source, YAML, Markdown, Gradle, and shell text to LF; keep `gradlew.bat` CRLF and
|
||||
mark common binary formats `-text`.
|
||||
- [ ] Add the four structured empty Trivy sections with suppression governance comments.
|
||||
- [ ] Run `cd src && ./gradlew verifyTrivyignore --console=plain` and expect zero suppressions
|
||||
validated.
|
||||
|
||||
### Task 3: Add quality governance and drift verification
|
||||
|
||||
**Files:**
|
||||
|
||||
- Create: `.github/CODEOWNERS`
|
||||
- Create: `.github/pull_request_template.md`
|
||||
- Create: `.github/ci-gate-matrix.yml`
|
||||
- Create: `.github/scripts/verify-gate-matrix.sh`
|
||||
- Create: `.github/workflows/ci-quality-gates.yml`
|
||||
- Create: `.github/workflows/link-check.yml`
|
||||
|
||||
- [ ] Record only current Gradle/task/test/job mechanisms in the matrix.
|
||||
- [ ] Implement repository-root-safe matrix parsing with schema, uniqueness, task/plugin/test, and
|
||||
workflow-job checks.
|
||||
- [x] Before Java/Gradle, fail unless `docs/security/public-paths-snapshot.txt` is committed and
|
||||
non-empty; do not let the Gradle task create a first-run CI baseline.
|
||||
- [ ] Have a human track and commit the canonical snapshot; agents do not stage or commit, and CI's
|
||||
`git ls-files` precondition rejects an untracked worktree file.
|
||||
- [ ] Add required `quality-gates`, `sample-off`, and `gate-matrix-lint` jobs plus the advisory
|
||||
quarantine job.
|
||||
- [ ] Make `release-gate` depend exactly on the three required jobs and fail unless all succeeded.
|
||||
- [ ] Add path-scoped link checking for PR and `main` push.
|
||||
- [ ] Pin every workflow `uses:` reference to a verified full commit SHA and retain its immutable
|
||||
release label in an inline comment.
|
||||
- [ ] Run Bash syntax and gate-matrix checks.
|
||||
|
||||
### Task 4: Add dependency-vulnerability controls
|
||||
|
||||
**Files:**
|
||||
|
||||
- Create: `.github/dependency-review-config.yml`
|
||||
- Create: `.github/dependency-vulnerability-policy.md`
|
||||
- Create: `.github/scripts/install-jq.sh`
|
||||
- Create: `.github/workflows/dependency-vulnerability.yml`
|
||||
|
||||
- [ ] Configure PR dependency review to block new High/Critical runtime vulnerabilities and
|
||||
forbidden strong/network-copyleft licenses without posting PR summary comments.
|
||||
- [ ] Document High/Critical blocking, Medium/Low advisory, KEV fail-closed handling, suppression
|
||||
review, GitHub/Gitea differences, egress, and mirror requirements.
|
||||
- [ ] Install checksum-pinned jq and version-pinned Trivy under `${RUNNER_TEMP}`, adding them through
|
||||
`${GITHUB_PATH}` without privileged writes.
|
||||
- [ ] Guard GitHub-only review/submission and keep `trivy-fs` platform-neutral on all required
|
||||
triggers.
|
||||
- [ ] Pass `--ignorefile .trivyignore.yaml` to every Trivy invocation.
|
||||
- [ ] Reject KEV catalogs with blank metadata, non-positive/non-integral or mismatched counts,
|
||||
empty vulnerability arrays, invalid CVE identifiers, or duplicate identifiers before
|
||||
intersection.
|
||||
- [ ] Reject malformed or empty Trivy JSON before extracting candidate vulnerability identifiers.
|
||||
|
||||
### Task 5: Verify the reconstructed slice
|
||||
|
||||
**Files:**
|
||||
|
||||
- Verify: all files created by this plan
|
||||
|
||||
- [ ] Parse strict policy/matrix YAML with an available parser and document GitHub `on` parser
|
||||
limitations if applicable.
|
||||
- [ ] Prove only `main` is an active branch trigger and no active `master` remains.
|
||||
- [ ] Prove every Trivy scan consumes the root ignore file.
|
||||
- [ ] Prove the release fan-in is exact and excludes quarantine.
|
||||
- [x] Prove the missing/empty/untracked snapshot precondition exits non-zero; the canonical
|
||||
`/api/healthcheck` snapshot now exists in the worktree but still requires a human commit.
|
||||
- [ ] Exercise the KEV predicate with empty/malformed/count/CVE/duplicate failures and a valid
|
||||
synthetic catalog.
|
||||
- [ ] Exercise the Trivy JSON predicate with malformed Results/Vulnerabilities/IDs and a realistic
|
||||
valid Results array.
|
||||
- [ ] Prove no harness call or `.gitea/workflows` shadow was introduced.
|
||||
- [ ] Run `git diff --check` and `git status --short`.
|
||||
- [ ] Capture the work in the required LLM Wiki branch note, including evidence and external
|
||||
blockers, without claiming server Actions or full Phase A completion.
|
||||
@@ -0,0 +1,103 @@
|
||||
# Harness-Free Module and Gradle Hygiene Implementation Plan
|
||||
|
||||
**Goal:** Apply the approved 19-leaf dependency and boundary cleanup without `.harness`.
|
||||
|
||||
**Spec:** `docs/superpowers/specs/2026-07-25-module-gradle-hygiene-harness-free-design.md`
|
||||
|
||||
**Policy:** TDD for behavior/boundary changes; focused proof before dependency removal; human-only
|
||||
Git operations.
|
||||
|
||||
## Task 1: Lock Phase B and characterize the Phase C baseline
|
||||
|
||||
- [ ] Confirm the Phase B focused tests, dependency-purity gate, spec review, and quality review
|
||||
are green.
|
||||
- [ ] Record the current 19-leaf registry and affected lockfiles.
|
||||
- [ ] Run the existing OpenAPI runtime tests before changing springdoc.
|
||||
|
||||
## Task 2: Isolate pure-core tests
|
||||
|
||||
- [ ] Change the root test convention so `domain-core`, `application-core`, and
|
||||
`shared-contract` receive only JUnit Jupiter, AssertJ, and the platform launcher.
|
||||
- [ ] Run the three core test suites and dependency reports.
|
||||
- [ ] Regenerate only their affected locks and prove no Spring coordinate remains on their test
|
||||
runtime classpaths.
|
||||
|
||||
## Task 3: Prune core/inbound declarations and align Boot 4
|
||||
|
||||
- [ ] Before editing, run and record each affected leaf's `compileJava`, `compileTestJava`, `test`,
|
||||
runtime dependency report, and relevant dependency insight.
|
||||
- [ ] Remove the approved unused project edges from application and inbound leaves.
|
||||
- [ ] Upgrade springdoc to `3.0.0`.
|
||||
- [ ] Remove unused GraphQL/WebSocket Jackson 2 declarations and unused gRPC direct declarations.
|
||||
- [ ] Characterize `jackson-databind-nullable` with dependency insight and focused
|
||||
present/null/undefined Jackson 3 tests; exclude its Jackson 2 transitive dependency only if the
|
||||
tests and real-server OpenAPI contract remain green.
|
||||
- [ ] Run each affected leaf test plus the two real-server `/v3/api-docs` tests.
|
||||
- [ ] Update the OpenAPI snapshot only if the generated public contract is semantically unchanged.
|
||||
|
||||
## Task 4: Prune outbound declarations
|
||||
|
||||
- [ ] Before editing, run and record each affected leaf's `compileJava`, `compileTestJava`, `test`,
|
||||
runtime dependency report, and relevant dependency insight.
|
||||
- [ ] Apply the approved support/cache/httpclient/identifier/messaging/notification project-edge
|
||||
removals.
|
||||
- [ ] Remove Groovy/Spock only from leaves with no Groovy tests.
|
||||
- [ ] Narrow fileserver/objectstorage from the broad Boot starter to autoconfigure plus SLF4J API.
|
||||
- [ ] Remove the JPA domain edge and remove explicit Flyway core only if focused compile/test proves
|
||||
it is redundant.
|
||||
- [ ] Run affected compile/tests before and after each dependency group.
|
||||
|
||||
## Task 5: Enforce configuration-processor parity
|
||||
|
||||
- [ ] Add a failing verification fixture or temporary mutation proving the exact
|
||||
`@ConfigurationProperties(` parity check detects missing and extra processors.
|
||||
- [ ] Register `verifyConfigurationPropertiesProcessor` from the JSON registry and wire it into
|
||||
leaf `check`.
|
||||
- [ ] Add processors to settings-owning leaves and remove the unused GraphQL processor.
|
||||
- [ ] Run the new gate and affected settings tests.
|
||||
|
||||
## Task 6: Remove the Mongo example domain
|
||||
|
||||
- [ ] Add tests for disabled mode, enable-flag binding, and enabled infrastructure with a mock
|
||||
`MongoClient`.
|
||||
- [ ] Delete all production/test `Example*` types and remove the fixed example bean/repository
|
||||
scanning.
|
||||
- [ ] Remove obsolete project and Testcontainers dependencies.
|
||||
- [ ] Run the Mongo tests and an `rg` assertion that production contains no `Example*`.
|
||||
|
||||
## Task 7: Invert sample correlation access
|
||||
|
||||
- [ ] Add framework-free `CorrelationIdPort` contract tests/fakes.
|
||||
- [ ] Add and test the inbound web MDC implementation.
|
||||
- [ ] Change the two sample application collaborators to use the port while retaining event-id
|
||||
fallback behavior.
|
||||
- [ ] Add an architecture assertion that sample application source has no SLF4J dependency.
|
||||
- [ ] Run application, web, sample outbox/poster, and architecture focused tests.
|
||||
|
||||
## Task 8: Clean generated state and composition documentation
|
||||
|
||||
- [ ] Delete tracked `src/sample-portfolio/.jqwik-database` and ignore future files.
|
||||
- [ ] Correct app-bootstrap “every module” wording and document default versus opt-in runtime
|
||||
composition.
|
||||
- [ ] Preserve the existing default runtime dependency set.
|
||||
|
||||
## Task 9: Locks, full verification, and review
|
||||
|
||||
- [ ] Regenerate strict lockfiles only with each affected leaf's
|
||||
`:leaf-path:resolveAndLockAll --write-locks`; do not run the root all-leaf writer.
|
||||
- [ ] Run all commands in the design verification section.
|
||||
- [ ] Run `git diff --check` and inspect the complete unstaged/untracked status.
|
||||
- [ ] Request spec and code-quality review; fix all actionable findings.
|
||||
- [ ] Update the mandated LLM Wiki raw branch note and derived raw notes, or record the exact
|
||||
missing-vault blocker.
|
||||
|
||||
## Final review hardening
|
||||
|
||||
- [x] Pin the Springdoc 3 `ApiError.details` widening with a real-server RED test.
|
||||
- [x] Add a web-owned OpenAPI customizer, import it in both real-server test applications, and
|
||||
restore the committed `type: object` snapshot without adding Swagger to `shared-contract`.
|
||||
- [x] Reproduce starter-driven Mongo activation through an actual `@EnableAutoConfiguration`
|
||||
context in both default and explicit-false modes.
|
||||
- [x] Register a module-level Boot 4 `AutoConfigurationImportFilter` that blocks Mongo
|
||||
auto-configuration until the module enable flag is true.
|
||||
- [x] Re-run affected formatting, locks, focused tests, and all design verification commands.
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,66 @@
|
||||
# Fileserver Durable Recovery Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this
|
||||
> plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. Repository policy is
|
||||
> human-only, so no step stages or commits changes.
|
||||
|
||||
**Goal:** Make the local publication provider restart-safe for completed and sealed operations
|
||||
without re-running the row producer.
|
||||
|
||||
**Architecture:** Keep the application port unchanged. The adapter owns a private operation journal
|
||||
under `.ca-fileserver/operations`, writes records through forced temp files and atomic rename, and
|
||||
uses a deterministic request fingerprint. A retry restores a verified terminal receipt or resumes a
|
||||
sealed staged artifact; disagreement is a conflict or indeterminate outcome, never an overwrite.
|
||||
|
||||
**Tech Stack:** Java 21 NIO, JUnit 5, AssertJ, existing Gradle quality gates.
|
||||
|
||||
---
|
||||
|
||||
### Task 1: Define deterministic journal records and request fingerprints
|
||||
|
||||
**Files:**
|
||||
- Create: `src/adapter/outbound/fileserver/src/main/java/dev/caskeleton/adapter/outbound/fileserver/LocalPublicationJournalRecord.java`
|
||||
- Create: `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/FilePublishRequestFingerprint.java`
|
||||
- Test: `src/adapter/outbound/fileserver/src/test/java/dev/caskeleton/adapter/outbound/fileserver/LocalPublicationJournalTest.java`
|
||||
|
||||
- [x] Write a failing test proving stable request fingerprints and different fingerprints for
|
||||
source/schema changes.
|
||||
- [x] Write a failing test proving journal round-trip and rejection of corrupt/newer records.
|
||||
- [x] Run
|
||||
`./gradlew :adapter:outbound:fileserver:test --tests '*LocalPublicationJournalTest' --console=plain`
|
||||
and confirm the missing types fail compilation.
|
||||
- [x] Implement a bounded flat JSON codec with schema version, state, fingerprint, locator token,
|
||||
checksum/counts and receipt snapshot fields. It must reject duplicate/unknown keys and never
|
||||
serialize absolute paths or row data.
|
||||
- [x] Run the focused test and confirm GREEN.
|
||||
|
||||
### Task 2: Add forced atomic journal persistence and recovery
|
||||
|
||||
**Files:**
|
||||
- Create: `src/adapter/outbound/fileserver/src/main/java/dev/caskeleton/adapter/outbound/fileserver/LocalPublicationJournal.java`
|
||||
- Modify: `src/adapter/outbound/fileserver/src/main/java/dev/caskeleton/adapter/outbound/fileserver/LocalFilePublicationAdapter.java`
|
||||
- Test: `src/adapter/outbound/fileserver/src/test/java/dev/caskeleton/adapter/outbound/fileserver/LocalFilePublicationRecoveryTest.java`
|
||||
|
||||
- [x] Write a failing test where a completed operation is retried with a producer that throws; the
|
||||
original receipt must be returned and the producer must remain uncalled.
|
||||
- [x] Write a failing test that reconstructs a new adapter over a sealed journal plus staged bytes
|
||||
and resumes publication without calling the producer.
|
||||
- [x] Write a failing test proving the same operation ID with a different request is a conflict and
|
||||
a digest mismatch is indeterminate.
|
||||
- [x] Run the recovery test and confirm RED.
|
||||
- [x] Persist `WRITING`, `SEALED`, and `PUBLISHED` records with temp + force + atomic move. Verify
|
||||
the target size and SHA-256 before terminal reconstruction.
|
||||
- [x] Run all Fileserver tests and confirm GREEN.
|
||||
|
||||
### Task 3: Report the exact readiness boundary
|
||||
|
||||
**Files:**
|
||||
- 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`
|
||||
|
||||
- [x] Mark single-node local restart recovery as implemented.
|
||||
- [x] Keep multi-node fencing, bounded background reaper, SFTP, NFS and HA evidence explicitly
|
||||
unimplemented.
|
||||
- [x] Run `./gradlew :adapter:outbound:fileserver:check --console=plain`.
|
||||
@@ -0,0 +1,236 @@
|
||||
# Fileserver Production Capability Foundation Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Replace the list-materializing CSV demo boundary with the Phase 1 framework-free publication contract and a bounded, staged local CSV R1 provider without claiming crash-safe R2 guarantees.
|
||||
|
||||
**Architecture:** `application-core` owns typed publication requests, rows, cells, producer/sink callbacks, opaque references, and receipts. `adapter:outbound:fileserver` owns CSV encoding, spreadsheet-formula mitigation, staging, digest/count limits, and local atomic publication. The legacy `FileExportPort` remains temporarily for compatibility and is explicitly documented as deprecated R0/R1 behavior.
|
||||
|
||||
**Tech Stack:** Java 21, JUnit 5, AssertJ, Spring Boot configuration properties, JDK NIO filesystem and SHA-256.
|
||||
|
||||
---
|
||||
|
||||
### Task 1: Add the framework-free publication contract
|
||||
|
||||
**Files:**
|
||||
- Create: `src/application-core/src/main/java/dev/caskeleton/application/filepublication/FilePublicationPort.java`
|
||||
- Create: `src/application-core/src/main/java/dev/caskeleton/application/filepublication/FilePublishRequest.java`
|
||||
- Create: `src/application-core/src/main/java/dev/caskeleton/application/filepublication/FilePublishOperationId.java`
|
||||
- Create: `src/application-core/src/main/java/dev/caskeleton/application/filepublication/FileDestinationId.java`
|
||||
- Create: `src/application-core/src/main/java/dev/caskeleton/application/filepublication/LogicalFileName.java`
|
||||
- Create: `src/application-core/src/main/java/dev/caskeleton/application/filepublication/SourceRevision.java`
|
||||
- Create: `src/application-core/src/main/java/dev/caskeleton/application/filepublication/ExportSchema.java`
|
||||
- Create: `src/application-core/src/main/java/dev/caskeleton/application/filepublication/TabularCell.java`
|
||||
- Create: `src/application-core/src/main/java/dev/caskeleton/application/filepublication/TabularRow.java`
|
||||
- Create: `src/application-core/src/main/java/dev/caskeleton/application/filepublication/TabularRowProducer.java`
|
||||
- Create: `src/application-core/src/main/java/dev/caskeleton/application/filepublication/TabularRowSink.java`
|
||||
- Create: `src/application-core/src/main/java/dev/caskeleton/application/filepublication/FilePublishReceipt.java`
|
||||
- Create: `src/application-core/src/main/java/dev/caskeleton/application/filepublication/PublishedFileReference.java`
|
||||
- Create: `src/application-core/src/main/java/dev/caskeleton/application/filepublication/FileVersion.java`
|
||||
- Test: `src/application-core/src/test/java/dev/caskeleton/application/filepublication/FilePublicationContractTest.java`
|
||||
|
||||
- [ ] **Step 1: Write the failing contract test**
|
||||
|
||||
```java
|
||||
@Test
|
||||
void requestRejectsPathLikeLogicalNamesAndSchemaRejectsDuplicateColumns() {
|
||||
assertThatThrownBy(() -> new LogicalFileName("../report.csv"))
|
||||
.isInstanceOf(IllegalArgumentException.class);
|
||||
assertThatThrownBy(
|
||||
() ->
|
||||
new ExportSchema(
|
||||
"worklog-v1",
|
||||
1,
|
||||
List.of(
|
||||
new ExportSchema.Column(
|
||||
"id", ExportSchema.CellType.INTEGER, false,
|
||||
ExportSchema.FormulaPolicy.REJECT, 64),
|
||||
new ExportSchema.Column(
|
||||
"id", ExportSchema.CellType.TEXT, false,
|
||||
ExportSchema.FormulaPolicy.MITIGATE, 128))))
|
||||
.isInstanceOf(IllegalArgumentException.class);
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Verify RED**
|
||||
|
||||
Run: `cd src && ./gradlew :application-core:test --tests '*FilePublicationContractTest' --console=plain`
|
||||
|
||||
Expected: compilation failure because the `filepublication` contract does not exist.
|
||||
|
||||
- [ ] **Step 3: Implement immutable validated values**
|
||||
|
||||
The contract must expose this shape and no `Path`, `File`, stream, Spring, or provider type:
|
||||
|
||||
```java
|
||||
public interface FilePublicationPort {
|
||||
FilePublishReceipt publish(FilePublishRequest request, TabularRowProducer producer);
|
||||
}
|
||||
|
||||
@FunctionalInterface
|
||||
public interface TabularRowProducer {
|
||||
void produce(TabularRowSink sink);
|
||||
}
|
||||
|
||||
public interface TabularRowSink {
|
||||
void write(TabularRow row);
|
||||
void checkpoint();
|
||||
}
|
||||
```
|
||||
|
||||
`TabularCell` is a sealed interface with nested records for text, integer, decimal, boolean, date,
|
||||
instant, and null. `ExportSchema` owns ordered columns, cell type, nullability, formula policy, and
|
||||
per-cell byte bounds. Records reject null/blank IDs, path separators in `LogicalFileName`, duplicate
|
||||
column names, empty schemas, and non-positive limits.
|
||||
|
||||
- [ ] **Step 4: Verify GREEN**
|
||||
|
||||
Run: `cd src && ./gradlew :application-core:test --tests '*FilePublicationContractTest' --console=plain`
|
||||
|
||||
Expected: PASS.
|
||||
|
||||
### Task 2: Add streaming CSV encoding and staged local publication
|
||||
|
||||
**Files:**
|
||||
- Create: `src/adapter/outbound/fileserver/src/main/java/dev/caskeleton/adapter/outbound/fileserver/StreamingCsvEncoder.java`
|
||||
- Create: `src/adapter/outbound/fileserver/src/main/java/dev/caskeleton/adapter/outbound/fileserver/LocalFilePublicationAdapter.java`
|
||||
- Create: `src/adapter/outbound/fileserver/src/main/java/dev/caskeleton/adapter/outbound/fileserver/LocalFilePublicationPolicy.java`
|
||||
- Test: `src/adapter/outbound/fileserver/src/test/java/dev/caskeleton/adapter/outbound/fileserver/LocalFilePublicationAdapterTest.java`
|
||||
|
||||
- [ ] **Step 1: Write the failing streaming publication tests**
|
||||
|
||||
```java
|
||||
@Test
|
||||
void publishesRowsThroughTheSinkAndReturnsAnOpaqueReceipt() {
|
||||
AtomicInteger calls = new AtomicInteger();
|
||||
FilePublishReceipt receipt =
|
||||
adapter.publish(
|
||||
request(),
|
||||
sink -> {
|
||||
calls.incrementAndGet();
|
||||
sink.write(new TabularRow(List.of(new IntegerCell(1), new TextCell("=cmd"))));
|
||||
});
|
||||
|
||||
assertThat(calls).hasValue(1);
|
||||
assertThat(receipt.reference().value()).doesNotContain(tempDir.toString());
|
||||
assertThat(Files.readString(publishedFile(receipt), UTF_8)).contains("1,'=cmd");
|
||||
}
|
||||
|
||||
@Test
|
||||
void abortsBeforeFinalPublicationWhenTheByteLimitIsExceeded() {
|
||||
assertThatThrownBy(
|
||||
() -> adapter.publish(request(), sink -> sink.write(oversizedRow())))
|
||||
.isInstanceOf(FilePublicationException.class);
|
||||
assertThat(finalArtifacts()).isEmpty();
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Verify RED**
|
||||
|
||||
Run: `cd src && ./gradlew :adapter:outbound:fileserver:test --tests '*LocalFilePublicationAdapterTest' --console=plain`
|
||||
|
||||
Expected: compilation failure because the staged provider does not exist.
|
||||
|
||||
- [ ] **Step 3: Implement the minimum staged provider**
|
||||
|
||||
`LocalFilePublicationPolicy` validates a fixed destination ID, base directory, maximum rows,
|
||||
maximum encoded bytes, and the only initial format profile `csv-rfc4180-v1`.
|
||||
|
||||
`LocalFilePublicationAdapter` must:
|
||||
|
||||
```text
|
||||
validate request/schema before producer invocation
|
||||
create a private .staging directory
|
||||
exclusive-create an operation-scoped .part file
|
||||
write header and each row directly through StreamingCsvEncoder
|
||||
enforce schema/cell/row/byte limits at each sink call
|
||||
prefix dangerous spreadsheet text with a single quote when policy is MITIGATE
|
||||
compute SHA-256 and counts while writing
|
||||
flush and FileChannel.force(true)
|
||||
move staging to the final operation-scoped file with ATOMIC_MOVE
|
||||
delete staging on pre-publish failure
|
||||
return an opaque reference and never an absolute path
|
||||
```
|
||||
|
||||
The first release is labelled local R1. Existing final artifacts cause a typed conflict; durable
|
||||
operation journals, crash reconciliation, replace semantics, and SFTP/NFS remain unimplemented and
|
||||
must not be advertised.
|
||||
|
||||
- [ ] **Step 4: Verify GREEN**
|
||||
|
||||
Run: `cd src && ./gradlew :adapter:outbound:fileserver:test --tests '*LocalFilePublicationAdapterTest' --console=plain`
|
||||
|
||||
Expected: PASS.
|
||||
|
||||
### Task 3: Add opt-in R1 composition and truthful documentation
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/adapter/outbound/fileserver/src/main/java/dev/caskeleton/adapter/outbound/fileserver/FileExportProperties.java`
|
||||
- Modify: `src/adapter/outbound/fileserver/src/main/java/dev/caskeleton/adapter/outbound/fileserver/FileExportConfig.java`
|
||||
- Create: `src/adapter/outbound/fileserver/src/test/java/dev/caskeleton/adapter/outbound/fileserver/FilePublicationConfigTest.java`
|
||||
- Modify: `src/adapter/outbound/fileserver/README.md`
|
||||
- Modify: `src/adapter/outbound/fileserver/CLAUDE.md`
|
||||
|
||||
- [ ] **Step 1: Write the failing composition test**
|
||||
|
||||
```java
|
||||
@Test
|
||||
void disabledConfigurationCreatesNoPublicationPort() {
|
||||
contextRunner
|
||||
.withUserConfiguration(FileExportConfig.class)
|
||||
.run(context -> assertThat(context).doesNotHaveBean(FilePublicationPort.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void enabledConfigurationCreatesExactlyOneLocalR1PublicationPort() {
|
||||
contextRunner
|
||||
.withUserConfiguration(FileExportConfig.class)
|
||||
.withPropertyValues(
|
||||
"ca-skeleton.fileserver.enabled=true",
|
||||
"ca-skeleton.fileserver.destination-id=local-export",
|
||||
"ca-skeleton.fileserver.base-directory=" + tempDir)
|
||||
.run(context -> assertThat(context).hasSingleBean(FilePublicationPort.class));
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Verify RED**
|
||||
|
||||
Run: `cd src && ./gradlew :adapter:outbound:fileserver:test --tests '*FilePublicationConfigTest' --console=plain`
|
||||
|
||||
Expected: FAIL because the new port is not composed.
|
||||
|
||||
- [ ] **Step 3: Wire only the local R1 provider**
|
||||
|
||||
Add validated destination ID, row limit, byte limit, and format-profile settings. Contribute
|
||||
`FilePublicationPort` only when explicitly enabled. Keep `FileExportPort` as a deprecated compatibility
|
||||
bean and document that it materializes caller rows and is not R2 evidence.
|
||||
|
||||
- [ ] **Step 4: Verify module and architecture gates**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
cd src
|
||||
./gradlew :application-core:test :adapter:outbound:fileserver:check --console=plain
|
||||
./gradlew verifyCleanArchitectureDependencies --console=plain
|
||||
./gradlew :app-bootstrap:test --tests '*CleanArchitectureTest' --console=plain
|
||||
```
|
||||
|
||||
Expected: all commands PASS.
|
||||
|
||||
### Task 4: Record the unfinished R2 boundary
|
||||
|
||||
**Files:**
|
||||
- Modify: `docs/superpowers/specs/2026-07-26-fileserver-production-capability-design.md`
|
||||
|
||||
- [ ] **Step 1: Update implementation status without weakening completion criteria**
|
||||
|
||||
Record Phase 0–1/local R1 foundation as implemented. Keep Phase 2 durable journal/reconciliation,
|
||||
Phase 3 operations, Phase 4 SFTP, Phase 5 NFS/HA/bootstrap, and Phase 6 optional operations marked
|
||||
unimplemented. The document must still say that local R1 is not Fileserver R2.
|
||||
|
||||
- [ ] **Step 2: Verify documentation structure**
|
||||
|
||||
Run: `rg -n 'R1|R2|구현 상태|미구현' docs/superpowers/specs/2026-07-26-fileserver-production-capability-design.md`
|
||||
|
||||
Expected: explicit R1 implementation and remaining R2 gaps are both present.
|
||||
@@ -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.
|
||||
@@ -0,0 +1,80 @@
|
||||
# HTTP Client Production Capability Foundation Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development
|
||||
> (recommended) or superpowers:executing-plans to implement this plan task-by-task.
|
||||
|
||||
**Goal:** Establish the framework-free call-budget and typed operation/target foundation, then close
|
||||
two proven safety defects in the legacy JDK provider without claiming Apache HC5, hard total
|
||||
deadline, egress security, or R2 readiness.
|
||||
|
||||
**Architecture:** `application-core` owns only a monotonic `CallBudget`. Product forks continue to
|
||||
own feature-specific semantic ports. `adapter:outbound:httpclient` owns destination/operation IDs,
|
||||
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 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.
|
||||
|
||||
---
|
||||
|
||||
### Task 1: Add a framework-free monotonic call budget
|
||||
|
||||
**Files:**
|
||||
- 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`
|
||||
|
||||
- [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
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/adapter/outbound/httpclient/build.gradle`
|
||||
- Create: `src/adapter/outbound/httpclient/src/main/java/dev/caskeleton/adapter/outbound/httpclient/operation/HttpDestinationId.java`
|
||||
- Create: `src/adapter/outbound/httpclient/src/main/java/dev/caskeleton/adapter/outbound/httpclient/operation/HttpOperationId.java`
|
||||
- Create: `src/adapter/outbound/httpclient/src/main/java/dev/caskeleton/adapter/outbound/httpclient/operation/HttpOperationDescriptor.java`
|
||||
- Create: `src/adapter/outbound/httpclient/src/main/java/dev/caskeleton/adapter/outbound/httpclient/operation/HttpOperationCatalog.java`
|
||||
- Create: `src/adapter/outbound/httpclient/src/main/java/dev/caskeleton/adapter/outbound/httpclient/target/FixedHttpDestination.java`
|
||||
- Create: `src/adapter/outbound/httpclient/src/main/java/dev/caskeleton/adapter/outbound/httpclient/target/HttpTargetBuilder.java`
|
||||
- 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`
|
||||
|
||||
- [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.
|
||||
- [x] Implement closed immutable descriptors and one-pass path-segment encoding.
|
||||
- [x] Verify GREEN.
|
||||
|
||||
### Task 3: Correct characterized legacy provider safety defects
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/adapter/outbound/httpclient/src/main/java/dev/caskeleton/adapter/outbound/httpclient/OutboundHttpClient.java`
|
||||
- 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`
|
||||
|
||||
- [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
|
||||
|
||||
**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`
|
||||
|
||||
- [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
|
||||
./gradlew :application-core:check :adapter:outbound:httpclient:check --console=plain
|
||||
./gradlew verifyCleanArchitectureDependencies --console=plain
|
||||
```
|
||||
@@ -0,0 +1,59 @@
|
||||
# HTTP Client Total Deadline Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this
|
||||
> plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. Repository policy is
|
||||
> human-only, so no step stages or commits changes.
|
||||
|
||||
**Goal:** Enforce `CallBudget` across the legacy HTTP logical call, including retry wait and blocking
|
||||
I/O, and cancel the executing task when the absolute monotonic deadline wins.
|
||||
|
||||
**Architecture:** Preserve the current migration facade but inject a bounded executor owned by each
|
||||
client. Every call intersects the caller budget with the configured maximum, passes the same
|
||||
absolute deadline to retry policy, waits through `Future.get(remaining)`, and cancels on timeout or
|
||||
shutdown. This is R1 cancellation evidence, not Apache pool or hard-wire-cancellation R2 evidence.
|
||||
|
||||
**Tech Stack:** Java 21 virtual-thread executor, Spring RestClient/JDK HttpClient, Resilience4j,
|
||||
JUnit loopback HTTP server.
|
||||
|
||||
---
|
||||
|
||||
### Task 1: Add deadline execution and explicit timeout vocabulary
|
||||
|
||||
**Files:**
|
||||
- Create: `src/adapter/outbound/httpclient/src/main/java/dev/caskeleton/adapter/outbound/httpclient/OutboundCallDeadlineExceededException.java`
|
||||
- Create: `src/adapter/outbound/httpclient/src/main/java/dev/caskeleton/adapter/outbound/httpclient/OutboundCallExecutor.java`
|
||||
- Test: `src/adapter/outbound/httpclient/src/test/java/dev/caskeleton/adapter/outbound/httpclient/OutboundCallExecutorTest.java`
|
||||
|
||||
- [x] Write failing tests proving an expired budget does not start work, a running task is
|
||||
interrupted on expiry, and completion wins before the deadline.
|
||||
- [x] Confirm RED.
|
||||
- [x] Implement absolute monotonic remaining-time calculation, `Future.get`, cancellation and
|
||||
exact exception mapping.
|
||||
- [x] Confirm GREEN.
|
||||
|
||||
### Task 2: Connect the budget to buffered and streaming calls
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/adapter/outbound/httpclient/src/main/java/dev/caskeleton/adapter/outbound/httpclient/OutboundHttpClient.java`
|
||||
- Modify: `src/adapter/outbound/httpclient/src/main/java/dev/caskeleton/adapter/outbound/httpclient/OutboundRetryPolicy.java`
|
||||
- Test: `src/adapter/outbound/httpclient/src/test/java/dev/caskeleton/adapter/outbound/httpclient/OutboundHttpClientDeadlineTest.java`
|
||||
|
||||
- [x] Write a failing loopback test where response delay exceeds the budget and confirm bounded
|
||||
return; record that JDK-provider server-side hard close is not proven by this lane.
|
||||
- [x] Write a failing test proving a shorter caller budget wins and retry cannot start after expiry.
|
||||
- [x] Confirm RED.
|
||||
- [x] Add overloads accepting `CallBudget`; existing methods create a configured maximum budget.
|
||||
Intersect budgets once and use the same deadline for retry and blocking execution.
|
||||
- [x] Confirm GREEN and run the complete HTTP leaf tests.
|
||||
|
||||
### Task 3: Record provider limits 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`
|
||||
|
||||
- [x] Record active logical-call deadline/cancellation as implemented.
|
||||
- [x] Keep explicit pool lease, Apache exact provider, DNS rebinding, TLS/auth/proxy and R2 hard
|
||||
cancellation evidence unimplemented.
|
||||
- [x] Run the HTTP leaf check and architecture/public-path gates.
|
||||
@@ -0,0 +1,484 @@
|
||||
# JPA/PostgreSQL Production Capability Implementation Plan
|
||||
|
||||
> 상태: Phase 0~3 기반과 Phase 4의 idempotency/outbox polling/inbox 후보 구현 및 전체
|
||||
> local/real PostgreSQL 검증을 마쳤다. 검증을 통과한 항목은 `implemented-candidate`이며
|
||||
> immutable 운영 evidence가 없는 항목을 R2로 승격하지 않는다. Phase 5~7은 외부 topology와
|
||||
> policy prerequisite가 없어 `not-implemented`를 유지한다.
|
||||
|
||||
- 작성일: 2026-07-28
|
||||
- 구현 branch: `codex/jpa-production-capability`
|
||||
- worktree:
|
||||
`/home/donghyeon/workspace/clean-architecture-backend-template-jpa`
|
||||
- 시작 revision: `b3add0162df8d4a0a11e749e514901defe0a62a3`
|
||||
- 설계 원본:
|
||||
`/home/donghyeon/workspace/clean-architecture-backend-template/docs/superpowers/specs/2026-07-28-jpa-production-capability-design.md`
|
||||
- 설계 SHA-256:
|
||||
`c02eaef2a193a6ca66f4814087cc4d6bce723509aec251f40ea7b029046fd234`
|
||||
|
||||
설계 문서는 `main` worktree의 untracked 사용자 변경이므로 stage/commit/copy하지 않는다. 구현
|
||||
중에는 위 절대 경로와 hash를 승인된 정본 snapshot으로 사용한다. 정본이 바뀌면 hash drift를
|
||||
먼저 보고하고 해당 task의 설계를 재검토한다.
|
||||
|
||||
## 1. 목표와 완료 경계
|
||||
|
||||
목표는 JPA/PostgreSQL leaf의 각 capability를 독립적으로 구현·검증하는 것이다.
|
||||
|
||||
```text
|
||||
truthful baseline
|
||||
-> transaction/failure/deadline
|
||||
-> entity/query discipline
|
||||
-> migration/lifecycle/security
|
||||
-> owner-safe reliability
|
||||
-> optional replica
|
||||
-> optional tenant/coordination
|
||||
-> R3 rehearsal
|
||||
```
|
||||
|
||||
한 phase의 unit test 통과를 전체 JPA R2로 확대하지 않는다. card가 R2가 되려면 설계 §31.3의
|
||||
prerequisite, real PostgreSQL task, zero-skip sentinel과 immutable evidence manifest를 모두
|
||||
충족해야 한다.
|
||||
|
||||
현재 구현 작업의 완료 경계는 다음과 같다.
|
||||
|
||||
1. 독립 worktree와 계획이 존재한다.
|
||||
2. Phase 0의 SQLState, Duration, OSIV/DDL, machine-readable readiness baseline이
|
||||
fail-closed한다.
|
||||
3. named transaction policy, absolute deadline, PostgreSQL local timeout, phase-aware outcome,
|
||||
bounded serialization/deadlock retry가 구현된다.
|
||||
4. PostgreSQL 16 real test source set에서 lifecycle/security/migration/transaction/
|
||||
aggregate/query가 무-skip로 실행된다.
|
||||
5. owner-safe idempotency V2, immutable outbox storage V2, polling delivery V2, same-store
|
||||
inbox가 독립 migration stream과 real PostgreSQL concurrency test를 가진다.
|
||||
6. 외부 CDC, replica, tenant/RLS, R3는 토폴로지/evidence 없이 선택하거나 R2로 광고하지 않는다.
|
||||
7. 전체 test/check와 Wiki capture 결과를 기록한다.
|
||||
|
||||
## 2. 공통 구현 규칙
|
||||
|
||||
- `src/config/architecture/modules.json`의 19개 leaf와 edge를 유지한다.
|
||||
- `domain-core`에는 Spring/JPA/JDBC/PostgreSQL type을 추가하지 않는다.
|
||||
- application contract에는 framework-neutral Java type만 둔다.
|
||||
- transaction boundary는 application use case가 `TransactionPort`로 소유한다.
|
||||
- controller/repository/mapper/configuration에 business policy를 두지 않는다.
|
||||
- PostgreSQL 전용 code/import는 persistence-jpa leaf의 `.postgresql` package에 둔다.
|
||||
- 동작 변경은 failing test를 먼저 확인한 뒤 최소 production code를 작성한다.
|
||||
- applied Flyway V1/V3/V4/V5는 수정하지 않는다.
|
||||
- agent는 stage/commit/amend/push하지 않는다.
|
||||
- 다른 worktree의 dirty/untracked 변경을 복사하거나 되돌리지 않는다.
|
||||
|
||||
worktree 생성 직후 `src/gradlew.bat`는 CRLF blob과 checkout/attribute line-ending
|
||||
normalization 차이 때문에 dirty로 표시된다. 비교 결과 의미 있는 텍스트 변경은 없지만 raw
|
||||
worktree hash와 HEAD blob hash는 EOL 표현 때문에 다르다. targeted restore로도 사라지지 않는
|
||||
known baseline drift이므로 구현 diff와 완료 판정에서 분리하고 stage하지 않는다.
|
||||
|
||||
## 3. Phase 0 — Truthful baseline과 contract freeze
|
||||
|
||||
### Task 0.1 SQLState mapping duplicate fail-fast
|
||||
|
||||
상태: 2026-07-28 구현 및 focused/architecture 검증 완료.
|
||||
|
||||
소유 leaf: `adapter-outbound-persistence-jpa`
|
||||
|
||||
파일:
|
||||
|
||||
- 수정:
|
||||
`src/adapter/outbound/persistence-jpa/src/test/java/dev/caskeleton/adapter/outbound/persistence/failure/PersistenceExceptionTranslatorTest.java`
|
||||
- 수정:
|
||||
`src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/failure/PersistenceExceptionTranslator.java`
|
||||
- 필요 시 수정:
|
||||
`src/adapter/outbound/persistence-jpa/README.md`
|
||||
|
||||
TDD:
|
||||
|
||||
1. 서로 다른 두 `SqlStateErrorMapping`이 같은 exact SQLState에 같은
|
||||
`OperationalError`를 등록해도 constructor가 실패하는 test를 작성한다.
|
||||
2. 같은 SQLState에 서로 다른 `OperationalError`를 등록하면 실패하는 test를 작성한다.
|
||||
3. error message가 raw SQL, credential, endpoint 없이 duplicate SQLState와 mapping
|
||||
contributor type을 식별하는지 검증한다.
|
||||
4. focused test를 실행해 RED를 확인한다.
|
||||
5. `putAll`을 explicit merge로 바꾸고 first/duplicate provenance를 보존한다.
|
||||
6. null mapping/map/key/value와 `08*` pseudo-entry를 fail-fast할지 현재 SPI 계약에 맞춰
|
||||
validation test를 추가한다. 이 세부 계약은 범위를 키우지 않고 constructor invariant로
|
||||
한정한다.
|
||||
7. focused test를 GREEN으로 만든다.
|
||||
|
||||
검증:
|
||||
|
||||
```bash
|
||||
cd src
|
||||
./gradlew :adapter:outbound:persistence-jpa:test \
|
||||
--tests 'dev.caskeleton.adapter.outbound.persistence.failure.PersistenceExceptionTranslatorTest' \
|
||||
--console=plain
|
||||
./gradlew :adapter:outbound:persistence-jpa:test --console=plain
|
||||
./gradlew verifyCleanArchitectureDependencies --console=plain
|
||||
```
|
||||
|
||||
### Task 0.2 Duration/OSIV/DDL production safety
|
||||
|
||||
상태: 2026-07-28 strict Duration와 prod DDL guard 구현 완료. OSIV guard는 기존 구현을
|
||||
재사용하고 함께 회귀 검증했다.
|
||||
|
||||
소유 leaf:
|
||||
|
||||
- `app-bootstrap`: runtime settings/startup validator
|
||||
- `adapter-outbound-persistence-jpa`: typed provider settings가 필요할 때만
|
||||
|
||||
선행 조사 파일:
|
||||
|
||||
- `src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/runtime/HikariPoolConstraintValidator.java`
|
||||
- `src/app-bootstrap/src/test/java/dev/caskeleton/bootstrap/runtime/HikariPoolConstraintValidatorTest.java`
|
||||
- `src/app-bootstrap/src/main/resources/application.yml`
|
||||
- `src/app-bootstrap/CLAUDE.md`
|
||||
|
||||
TDD:
|
||||
|
||||
1. `5s`, `PT5S`, millisecond number의 canonical/legacy 허용 matrix를 test로 고정한다.
|
||||
2. invalid/unknown Duration을 skip하지 않고 startup failure로 만드는 RED를 확인한다.
|
||||
3. `spring.jpa.open-in-view=true`를 거절한다.
|
||||
4. production profile의 `ddl-auto=update|create|create-drop`을 거절한다.
|
||||
5. local/sample compatibility를 별도 test로 유지한다.
|
||||
|
||||
검증:
|
||||
|
||||
```bash
|
||||
cd src
|
||||
./gradlew :app-bootstrap:test \
|
||||
--tests 'dev.caskeleton.bootstrap.runtime.HikariPoolConstraintValidatorTest' \
|
||||
--console=plain
|
||||
./gradlew :app-bootstrap:test --tests '*CleanArchitectureTest' --console=plain
|
||||
./gradlew verifyEnvKeys --console=plain
|
||||
```
|
||||
|
||||
### Task 0.3 Machine-readable readiness baseline
|
||||
|
||||
상태: 2026-07-28 구현 및 mutation/registry 검증 완료.
|
||||
|
||||
파일:
|
||||
|
||||
- 추가: `src/config/jpa/readiness-cards.yaml`
|
||||
- 수정: `src/build.gradle`
|
||||
- 추가: persistence-jpa readiness registry parser/validation tests
|
||||
|
||||
구현:
|
||||
|
||||
1. 설계 §31.3의 15 card와 7 owned migration stream을 exact key로 옮긴다.
|
||||
2. unknown/missing card, duplicate task, cycle, missing prerequisite, duplicate
|
||||
location/history를 fail-closed한다.
|
||||
3. 현재 구현되지 않은 task/card는 `not-implemented`로 유지한다.
|
||||
4. 존재하지 않는 target task를 통과 증거로 만들지 않는다.
|
||||
5. registry structural verification task를 `check`의 architecture policy chain에 연결하되
|
||||
real PostgreSQL readiness를 거짓으로 통과시키지 않는다.
|
||||
|
||||
## 4. Phase 1 — Transaction/failure/deadline foundation
|
||||
|
||||
상태: 2026-07-28 application contract, Spring executor, local timeout, phase-aware outcome,
|
||||
bounded retry/backoff 후보 구현 완료. commit fault injection과 immutable R2 manifest는 남아 있다.
|
||||
|
||||
### Task 1.1 Additive application transaction contract
|
||||
|
||||
소유 leaf: `application-core`
|
||||
|
||||
예상 파일:
|
||||
|
||||
- 추가: `transaction/TransactionPolicy.java`
|
||||
- 추가: `transaction/CallBudget.java`
|
||||
- 추가: `transaction/OperationId.java`
|
||||
- 추가: `transaction/TransactionOutcome.java`
|
||||
- 추가: `transaction/PolicyTransactionPort.java`
|
||||
- 수정: `transaction/TransactionPort.java`
|
||||
- tests: 같은 package의 pure unit tests
|
||||
|
||||
계약:
|
||||
|
||||
- 기존 `inWrite`, `inRead`, `inNew` source compatibility 유지
|
||||
- named write policy는 stable operation ID 요구
|
||||
- legacy facade는 non-replayable/uncorrelated policy로 격리
|
||||
- absolute deadline과 finite timeout intersection
|
||||
- core에는 Spring `TransactionDefinition`/`DurationStyle`을 노출하지 않음
|
||||
|
||||
### Task 1.2 Spring policy executor와 propagation ownership
|
||||
|
||||
소유 leaf: `adapter-outbound-persistence-jpa`
|
||||
|
||||
예상 파일:
|
||||
|
||||
- 수정: `transaction/SpringTransactionPort.java`
|
||||
- 추가: `transaction/SpringPolicyTransactionPort.java`
|
||||
- 추가: transaction phase/outcome collaborator
|
||||
- tests: unit + real PostgreSQL task
|
||||
|
||||
검증:
|
||||
|
||||
- REQUIRED physical owner와 participant 구분
|
||||
- REQUIRES_NEW depth/capacity admission
|
||||
- read/write route mismatch fail-fast
|
||||
- commit callback ordering
|
||||
- locale 없는 `toLowerCase()` 제거
|
||||
|
||||
### Task 1.3 Deadline와 PostgreSQL local timeout
|
||||
|
||||
- Hikari acquisition은 fixed pool timeout으로 유지
|
||||
- action 시작 전 remaining budget pre-gate
|
||||
- first statement 전 `SET LOCAL statement_timeout`, `lock_timeout`
|
||||
- transaction/statement/lock rounding boundary test
|
||||
- pool wait 뒤 total budget overshoot negative test
|
||||
|
||||
### Task 1.4 Phase-aware failure/retry
|
||||
|
||||
- operation/query executor를 모든 production persistence path에 연결
|
||||
- constraint name allowlist
|
||||
- begin/action/flush/commit/after-completion phase 분류
|
||||
- `COMMIT_INDETERMINATE`는 blind retry 금지
|
||||
- pre-commit + replay-safe + budget 조건에서만 whole-transaction retry
|
||||
|
||||
## 5. Phase 2 — Entity/query discipline
|
||||
|
||||
상태: production template에 임의 business aggregate를 추가하지 않고 sample의 기존 entity/
|
||||
mapper/query discipline을 실제 PostgreSQL aggregate CAS와 query-plan fixture로 검증했다.
|
||||
|
||||
### Task 2.1 Aggregate persistence baseline
|
||||
|
||||
- domain aggregate와 persistence entity 분리
|
||||
- mapper round-trip과 invariant failure test
|
||||
- optimistic version/expected-version conflict
|
||||
- audit creation carry-forward와 bulk DML guard
|
||||
- bounded persistence-context batch
|
||||
|
||||
### Task 2.2 Purpose-built query model
|
||||
|
||||
- application projection `*QueryPort`
|
||||
- allowlisted query ID
|
||||
- max page/IN bound와 signed/versioned keyset cursor
|
||||
- N+1 statement budget
|
||||
- native/JDBC query는 `.postgresql` package
|
||||
- representative `EXPLAIN` invariant task
|
||||
|
||||
## 6. Phase 3 — Migration/lifecycle/security
|
||||
|
||||
상태: legacy V1/V3/V4/V5/V6 adoption, independent core stream, PostgreSQL 16 lifecycle/security/
|
||||
migration/transaction/aggregate/query candidate task와 content-addressed manifest producer 구현
|
||||
완료. TLS verify-full/role/redaction, pool lifecycle, fresh/interrupted/rolling migration,
|
||||
transaction concurrency/fault dimension을 실제 PostgreSQL과 transport test로 채웠다. clean CI
|
||||
provenance와 외부 restore rehearsal이 없으면 R2/R3 aggregation은 계속 fail-closed한다.
|
||||
|
||||
### Task 3.1 Legacy adoption과 independent streams
|
||||
|
||||
- legacy V1/V3/V4/V5 checksum/object fingerprint
|
||||
- `capability_schema_registry`
|
||||
- explicit target stream version-0 adoption command
|
||||
- core/optional history table ownership
|
||||
- fresh/LEGACY_ADOPTED/interrupted paths
|
||||
- old/target dual authority rejection
|
||||
|
||||
### Task 3.2 Real PostgreSQL qualification source set
|
||||
|
||||
canonical tasks:
|
||||
|
||||
```text
|
||||
postgresqlLifecycleIntegrationTest
|
||||
postgresqlSecurityBaselineIntegrationTest
|
||||
postgresqlMigrationIntegrationTest
|
||||
postgresqlTransactionIntegrationTest
|
||||
postgresqlAggregateIntegrationTest
|
||||
postgresqlQueryIntegrationTest
|
||||
verifyJpaPrimaryFoundationEvidence
|
||||
```
|
||||
|
||||
Docker/Testcontainers가 없으면 R2 lane은 skip이 아니라 fail이다. local optional task와 evidence
|
||||
producer를 분리한다.
|
||||
|
||||
구현된 evidence task:
|
||||
|
||||
```text
|
||||
verifyJpaEvidenceHarnessContract
|
||||
generateJpaEvidenceManifests
|
||||
verifyJpaCandidateEvidence
|
||||
verifyJpaPrimaryFoundationEvidence
|
||||
```
|
||||
|
||||
candidate task는 11개 active card의 exact JUnit selector, zero-skip count, source/이미지/의존성
|
||||
version과 prerequisite manifest ID를 SHA-256 filename manifest로 남긴다. primary task는
|
||||
`-PjpaEvidenceProfile=r2`, clean revision, CI job/artifact metadata, 모든 base dimension과
|
||||
prerequisite R2를 추가로 요구한다.
|
||||
|
||||
### Task 3.3 Lifecycle/security
|
||||
|
||||
- migration/runtime role 분리
|
||||
- trusted schema/search_path, `PUBLIC CREATE`/`TEMP` revoke
|
||||
- TLS verify-full profile
|
||||
- startup/readiness/shutdown/quiesce
|
||||
- bounded/redacted metric/trace/log
|
||||
- restore/forward-recovery runbook
|
||||
|
||||
## 7. Phase 4 — Owner-safe same-store reliability
|
||||
|
||||
상태: idempotency V2, outbox storage V2, polling delivery V2, inbox V1은 각각
|
||||
`implemented-candidate`. 네 stream 모두 fresh-disabled/first-enable/disable/re-enable/
|
||||
interrupted-recovery의 non-destructive lifecycle을 실제 PostgreSQL에서 검증한다. CDC는 external
|
||||
messaging prerequisite가 없어 `not-implemented`다.
|
||||
|
||||
독립 implementation slice:
|
||||
|
||||
1. `jpa-idempotency-owner-safe-v2`
|
||||
2. `jpa-outbox-storage-v2`
|
||||
3. `jpa-outbox-polling-delivery-v2` 또는 `jpa-outbox-cdc-retention-v1`
|
||||
4. `jpa-inbox-same-store-v1`
|
||||
|
||||
각 slice는 자기 migration stream/task/manifest를 가진다.
|
||||
|
||||
outbox storage 구현은:
|
||||
|
||||
- V3 `outbox_event`를 수정하지 않음
|
||||
- `outbox_publication_control_v2`
|
||||
- `outbox_publication_cutover_v2`
|
||||
- `outbox_event_identity_v2`
|
||||
- `outbox_event_log_v2`
|
||||
- polling 선택 시에만 `outbox_delivery_v2`
|
||||
- fresh/legacy genesis sentinel
|
||||
- legacy mutation trigger/ACL fence
|
||||
- paused old writer와 cutover barrier test
|
||||
|
||||
를 포함한다.
|
||||
|
||||
## 8. Phase 5–7
|
||||
|
||||
상태: 선택된 replica topology, tenant mode/RLS policy, target-like backup/failover environment가
|
||||
없으므로 registry에서 `not-implemented`를 유지한다. 로컬 단일 PostgreSQL 테스트를 해당
|
||||
운영 보장의 대체 evidence로 사용하지 않는다.
|
||||
|
||||
### Phase 5 — Primary/replica
|
||||
|
||||
- 별도 pool/route context
|
||||
- explicit `ReadConsistency`
|
||||
- endpoint-bound lag evidence
|
||||
- strong/RYW primary default
|
||||
- failover authority reconciliation
|
||||
|
||||
### Phase 6 — Tenant/RLS와 JDBC coordination
|
||||
|
||||
- tenant-prefixed unique/FK/query
|
||||
- missing context fail-closed
|
||||
- optional FORCE RLS
|
||||
- runtime role bypass negative test
|
||||
- JDBC coordination은 `EFFICIENCY_ONLY`
|
||||
|
||||
### Phase 7 — R3
|
||||
|
||||
- target-like load/capacity
|
||||
- failover, rolling migration, certificate rotation
|
||||
- backup/PITR restore
|
||||
- outbox/idempotency/inbox reconciliation
|
||||
- measured RPO/RTO와 operator game day
|
||||
|
||||
## 9. 공통 verification ladder
|
||||
|
||||
변경 leaf focused test부터 실행한다.
|
||||
|
||||
```bash
|
||||
cd src
|
||||
./gradlew :application-core:test --console=plain
|
||||
./gradlew :adapter:outbound:persistence-jpa:test --console=plain
|
||||
./gradlew :app-bootstrap:test --tests '*CleanArchitectureTest' --console=plain
|
||||
./gradlew verifyCleanArchitectureDependencies --console=plain
|
||||
./gradlew verifyPublicPathSnapshot --console=plain
|
||||
./gradlew verifyEnvKeys --console=plain
|
||||
```
|
||||
|
||||
전체 `test`/`check`와 real PostgreSQL task는 해당 phase가 경계를 실제로 변경하거나 required
|
||||
task를 추가한 시점에 실행한다. 실행하지 못한 명령은 이유와 남은 위험을 branch-note와 최종
|
||||
응답에 기록한다.
|
||||
|
||||
## 10. Wiki capture
|
||||
|
||||
각 의미 있는 slice가 끝날 때 실제 vault의 branch-note:
|
||||
|
||||
```text
|
||||
raw/branch-notes/codex-jpa-production-capability.md
|
||||
```
|
||||
|
||||
에 다음을 누적한다.
|
||||
|
||||
- design hash와 plan path
|
||||
- 변경 파일/decision ID
|
||||
- RED/GREEN/architecture command와 결과
|
||||
- 실패/차단/known baseline drift
|
||||
- evidence grade와 아직 R2가 아닌 이유
|
||||
- 실제 파생 raw interview/blog/error 판단
|
||||
|
||||
canonical 문서는 별도 요청 전 생성하지 않는다.
|
||||
|
||||
## 11. 최종 실행 결과
|
||||
|
||||
2026-07-28:
|
||||
|
||||
- `./gradlew :sample-portfolio:test --console=plain`
|
||||
→ 성공, 176 tests.
|
||||
- `./gradlew test --console=plain`
|
||||
→ 성공, 1m 59s.
|
||||
- PostgreSQL readiness task 10개
|
||||
(`lifecycle`, `security`, `migration`, `transaction`, `aggregate`, `query`, `idempotency`,
|
||||
`outbox-storage`, `outbox-polling`, `inbox`)
|
||||
→ 성공, 49s. XML 합계 23 tests, `skipped=0`, `failures=0`, `errors=0`.
|
||||
- `./gradlew check --console=plain`
|
||||
→ 성공, 2m 9s, 209 actionable tasks. 같은 실행에서 root architecture policy,
|
||||
Checkstyle, Spotless, SpotBugs와 custom PostgreSQL source set 검증을 통과했다.
|
||||
- `./gradlew verifyCleanArchitectureDependencies verifyPublicPathSnapshot verifyEnvKeys
|
||||
verifyJpaReadinessRegistry --console=plain`
|
||||
→ 성공. 19개 leaf edge, 1개 public path, 113 env keys, exact 15 cards/7 streams 검증.
|
||||
- `git diff --check`
|
||||
→ 진단 없음.
|
||||
- `./gradlew :adapter:outbound:persistence-jpa:verifyJpaPrimaryFoundationEvidence --console=plain`
|
||||
→ 기존 unconditional sentinel을 제거했다. content-addressed candidate manifest를 검증한 뒤
|
||||
candidate profile과 observability, TLS/role/redaction, fresh/interrupted/rolling migration,
|
||||
transaction concurrency 누락을 card별 blocker로 보고 R2를 차단한다.
|
||||
- `./gradlew :adapter:outbound:persistence-jpa:verifyJpaCandidateEvidence --console=plain`
|
||||
→ 성공, active card 11개 manifest 생성. PostgreSQL 23 tests와 primary base aggregation
|
||||
7 tests 모두 zero-skip이고 content hash/prerequisite link를 검증했다.
|
||||
- `bash .github/scripts/verify-gate-matrix.sh`
|
||||
→ 성공, 21 gates verified. PR candidate evidence job과 conditional R2 workflow가 registry에
|
||||
반영됐다.
|
||||
- CI metadata를 주입한
|
||||
`verifyJpaPrimaryFoundationEvidence -PjpaEvidenceProfile=r2`
|
||||
→ PostgreSQL 23 tests와 r2-profile manifest 11개 생성 뒤 의도된 실패, 1m 21s.
|
||||
`worktree-is-dirty`, observability, TLS/roles/redaction, migration
|
||||
fresh/interrupted/rolling, transaction concurrency를 실제 blocker로 보고했다.
|
||||
- `./gradlew test --console=plain`
|
||||
→ 성공, 15s, 78 tasks up-to-date. 직전 evidence lane에서 persistence/app test는 강제
|
||||
재실행했다.
|
||||
- `./gradlew check verifyPublicPathSnapshot verifyDependencyLocks --console=plain`
|
||||
→ 성공, 9s, 230 actionable tasks(37 executed, 193 up-to-date).
|
||||
|
||||
전체 test에서 발견한 sample Flyway 회귀는 independent `V1` stream을 broad
|
||||
`classpath:db/migration`으로 합친 문제와 production/sample `V6` 충돌이었다. sample slice를
|
||||
legacy PostgreSQL location으로 한정하고 disposable poster migration을 `V7`로 이동했다. 세부
|
||||
재현·해결 기록은 Wiki
|
||||
`raw/errors/flyway-independent-stream-broad-root-collision-2026-07-28.md`에 남겼다.
|
||||
|
||||
2026-07-29 completion pass:
|
||||
|
||||
- primary foundation의 pool lifecycle/observability, TLS verify-full/role/redaction,
|
||||
fresh/interrupted/rolling migration, transaction concurrency/fault evidence를 추가했다.
|
||||
- idempotency/outbox storage/outbox polling/inbox 네 독립 stream에
|
||||
fresh-disabled/first-enable/disable/re-enable/interrupted-recovery 실제 PostgreSQL
|
||||
lifecycle test를 추가했다.
|
||||
- `./gradlew :adapter:outbound:persistence-jpa:verifyJpaCandidateEvidence --console=plain`
|
||||
→ **BUILD SUCCESSFUL in 1m 55s**. 11개 manifest 모두 `missing=none`, zero-skip.
|
||||
PostgreSQL producer 38 tests와 web redaction support 2 tests가 실행됐으며 primary
|
||||
aggregation은 20 tests다.
|
||||
- `./gradlew test --console=plain`
|
||||
→ **BUILD SUCCESSFUL in 55s**, 78 actionable tasks.
|
||||
- `./gradlew check verifyPublicPathSnapshot verifyDependencyLocks --console=plain`
|
||||
→ 포맷과 test fixture SQL construction을 수정한 뒤 **BUILD SUCCESSFUL in 12s**,
|
||||
231 actionable tasks. 19 leaf architecture, Checkstyle, Spotless, SpotBugs, dependency lock,
|
||||
env/readiness/public-path gate를 통과했다.
|
||||
- CI 메타데이터 형식만 주입한
|
||||
`verifyJpaPrimaryFoundationEvidence -PjpaEvidenceProfile=r2`
|
||||
→ **의도된 BUILD FAILED in 2m 6s**. missing evidence는 없고 root blocker는
|
||||
`worktree-is-dirty`; 다른 blocker는 prerequisite R2 전파뿐이다.
|
||||
- `bash .github/scripts/verify-gate-matrix.sh`
|
||||
→ **OK**, 21 gates/21 verified.
|
||||
- `git diff --check`
|
||||
→ 진단 없음.
|
||||
|
||||
현재 환경에서 선택된 Phase 0~4 후보의 로컬 구현·검증은 완료됐다. R2 승격은 사람의
|
||||
commit/push, clean revision에서의 retained CI artifact가 필요하고, R3는 target-like
|
||||
backup/failover/load/operator rehearsal 환경이 필요하다.
|
||||
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.
|
||||
@@ -0,0 +1,93 @@
|
||||
# Redis Production Capability Foundation Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development
|
||||
> (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use
|
||||
> checkbox syntax for tracking.
|
||||
|
||||
**Goal:** Replace the adapter-only cache seam with a framework-free semantic cache contract, safe
|
||||
physical key construction, and a versioned typed atomic-program foundation without claiming that a
|
||||
real Redis runtime or any R2 capability is complete.
|
||||
|
||||
**Architecture:** `application-core` owns provider-neutral cache outcomes and mutation intent.
|
||||
`adapter:outbound:cache-redis` owns physical key construction, digesting, Lua resources, program
|
||||
descriptors, and typed primitive facades. Existing legacy routing remains compatible while migration
|
||||
is incremental. No Redis SDK, raw command, raw key, or Lua concept crosses into core.
|
||||
|
||||
**Scope boundary:** This batch implements Phase 0 and selected Phase 1 foundations. Spring Data
|
||||
Redis/Lettuce runtime, codec/envelope, real-service integration, topology, distributed rate limit,
|
||||
idempotency, lease, session, and R2/R3 evidence remain separate implementation phases.
|
||||
|
||||
---
|
||||
|
||||
### Task 1: Add the provider-neutral cache contract
|
||||
|
||||
**Files:**
|
||||
- Create: `src/application-core/src/main/java/dev/caskeleton/application/cache/CacheRegionPort.java`
|
||||
- Create: `src/application-core/src/main/java/dev/caskeleton/application/cache/CacheLookup.java`
|
||||
- Create: `src/application-core/src/main/java/dev/caskeleton/application/cache/CacheRecordMetadata.java`
|
||||
- Create: `src/application-core/src/main/java/dev/caskeleton/application/cache/CacheRecordIntent.java`
|
||||
- Create: `src/application-core/src/main/java/dev/caskeleton/application/cache/CacheRecordOutcome.java`
|
||||
- Create: `src/application-core/src/main/java/dev/caskeleton/application/cache/CacheInvalidationOutcome.java`
|
||||
- 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`
|
||||
|
||||
- [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
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/adapter/outbound/cache-redis/build.gradle`
|
||||
- Create: `src/adapter/outbound/cache-redis/src/main/java/dev/caskeleton/adapter/outbound/cache/redis/key/RedisKeyNamespace.java`
|
||||
- Create: `src/adapter/outbound/cache-redis/src/main/java/dev/caskeleton/adapter/outbound/cache/redis/key/RedisKeyBuilder.java`
|
||||
- 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`
|
||||
|
||||
- [x] Write a failing test proving namespace isolation, one stable hash tag, bounded key bytes, and
|
||||
absence of raw sensitive resource identifiers.
|
||||
- [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.
|
||||
- [x] Verify GREEN.
|
||||
|
||||
### Task 3: Add a typed, versioned atomic-program catalog
|
||||
|
||||
**Files:**
|
||||
- Create: `src/adapter/outbound/cache-redis/src/main/java/dev/caskeleton/adapter/outbound/cache/redis/program/RedisProgramId.java`
|
||||
- Create: `src/adapter/outbound/cache-redis/src/main/java/dev/caskeleton/adapter/outbound/cache/redis/program/RedisProgramDescriptor.java`
|
||||
- Create: `src/adapter/outbound/cache-redis/src/main/java/dev/caskeleton/adapter/outbound/cache/redis/program/RedisProgramCatalog.java`
|
||||
- Create: `src/adapter/outbound/cache-redis/src/main/java/dev/caskeleton/adapter/outbound/cache/redis/program/RedisProgramExecutor.java`
|
||||
- Create: `src/adapter/outbound/cache-redis/src/main/java/dev/caskeleton/adapter/outbound/cache/redis/program/RedisAtomicPrimitives.java`
|
||||
- Create: `src/adapter/outbound/cache-redis/src/main/resources/redis/scripts/compare-and-delete-v1.lua`
|
||||
- Create: `src/adapter/outbound/cache-redis/src/main/resources/redis/scripts/compare-and-expire-v1.lua`
|
||||
- Create: `src/adapter/outbound/cache-redis/src/main/resources/redis/scripts/set-if-absent-with-ttl-v1.lua`
|
||||
- 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`
|
||||
|
||||
- [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.
|
||||
- [x] Verify GREEN.
|
||||
|
||||
### Task 4: Record exact readiness and verify
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/adapter/outbound/cache-redis/README.md`
|
||||
- Modify: `src/adapter/outbound/cache-redis/CLAUDE.md`
|
||||
- Modify: `docs/superpowers/specs/2026-07-26-redis-production-capability-design.md`
|
||||
|
||||
- [x] Mark only contract/key/program foundation as implemented and all real runtime/capability
|
||||
promotion as unimplemented.
|
||||
- [x] Run:
|
||||
|
||||
```bash
|
||||
cd src
|
||||
./gradlew :application-core:check :adapter:outbound:cache-redis:check --console=plain
|
||||
./gradlew verifyCleanArchitectureDependencies --console=plain
|
||||
```
|
||||
|
||||
- [x] Do not claim Redis cache R1/R2 until a real standalone service lane and codec/runtime evidence
|
||||
exist.
|
||||
@@ -0,0 +1,64 @@
|
||||
# Redis Runtime And Semantic Cache Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this
|
||||
> plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. Repository policy is
|
||||
> human-only, so no step stages or commits changes.
|
||||
|
||||
**Goal:** Replace the SDK-less Redis seam with an opt-in managed Lettuce runtime, a real Lua
|
||||
executor, and a bounded semantic string-cache implementation.
|
||||
|
||||
**Architecture:** A package-private runtime owns `RedisClient`, connection and synchronous binary
|
||||
commands. The Lua executor uses the compiled catalog checksum and `EVALSHA`, falling back to `EVAL`
|
||||
only for `NOSCRIPT`. A versioned binary envelope distinguishes positive, negative and incompatible
|
||||
entries behind `CacheRegionPort<String,String>`.
|
||||
|
||||
**Tech Stack:** Java 21, Lettuce Core managed by Spring Boot 4 BOM, Spring Boot configuration
|
||||
properties, JUnit 5, optional Docker-backed Redis qualification.
|
||||
|
||||
---
|
||||
|
||||
### Task 1: Add the managed runtime and typed program execution
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/adapter/outbound/cache-redis/build.gradle`
|
||||
- Create: `src/adapter/outbound/cache-redis/src/main/java/dev/caskeleton/adapter/outbound/cache/redis/RedisRuntimeSettings.java`
|
||||
- Create: `src/adapter/outbound/cache-redis/src/main/java/dev/caskeleton/adapter/outbound/cache/redis/LettuceRedisRuntime.java`
|
||||
- Modify: `src/adapter/outbound/cache-redis/src/main/java/dev/caskeleton/adapter/outbound/cache/redis/RedisCacheAdapterConfig.java`
|
||||
- Create: `src/adapter/outbound/cache-redis/src/test/java/dev/caskeleton/adapter/outbound/cache/redis/LettuceRedisRuntimeTest.java`
|
||||
|
||||
- [x] Write failing tests for URI/timeout validation, lifecycle close, binary get/set/delete and
|
||||
`EVALSHA -> NOSCRIPT -> EVAL`.
|
||||
- [x] Confirm RED before adding the Lettuce production dependency.
|
||||
- [x] Add `io.lettuce:lettuce-core` using the Boot BOM and update the affected dependency locks.
|
||||
- [x] Implement a package-private runtime with finite command/shutdown timeouts, bounded reconnect
|
||||
behavior, and no connection side effects while disabled or in external-client mode.
|
||||
- [x] Verify focused tests GREEN.
|
||||
|
||||
### Task 2: Implement the semantic cache region
|
||||
|
||||
**Files:**
|
||||
- Create: `src/adapter/outbound/cache-redis/src/main/java/dev/caskeleton/adapter/outbound/cache/redis/RedisCacheRegionPolicy.java`
|
||||
- Create: `src/adapter/outbound/cache-redis/src/main/java/dev/caskeleton/adapter/outbound/cache/redis/RedisCacheEnvelopeCodec.java`
|
||||
- Create: `src/adapter/outbound/cache-redis/src/main/java/dev/caskeleton/adapter/outbound/cache/redis/RedisStringCacheRegion.java`
|
||||
- Test: `src/adapter/outbound/cache-redis/src/test/java/dev/caskeleton/adapter/outbound/cache/redis/RedisStringCacheRegionTest.java`
|
||||
|
||||
- [x] Write failing tests for hit, negative hit, miss, incompatible schema, positive/negative TTL,
|
||||
invalidation and provider failure certainty.
|
||||
- [x] Confirm RED.
|
||||
- [x] Implement a bounded versioned binary envelope and HMAC-derived physical keys. Support UPSERT;
|
||||
return `NOT_RECORDED_PROVIDER_POLICY` for opaque revision ordering the provider cannot prove.
|
||||
- [x] Confirm GREEN and run the complete Redis leaf test suite.
|
||||
|
||||
### Task 3: Qualify and document without false promotion
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/adapter/outbound/cache-redis/README.md`
|
||||
- Modify: `src/adapter/outbound/cache-redis/CLAUDE.md`
|
||||
- Modify: `docs/superpowers/specs/2026-07-26-redis-production-capability-design.md`
|
||||
- Modify: runtime configuration and env-key registry only for settings actually introduced.
|
||||
|
||||
- [x] If a local Redis image is available, run an explicit real-service program/cache test; never
|
||||
silently skip it.
|
||||
- [x] Mark standalone runtime/cache as R1 unless real service, restart, ACL/TLS and fault evidence
|
||||
required by the readiness card all pass.
|
||||
- [x] Run the leaf check, dependency lock check, env-key gate and architecture gate.
|
||||
@@ -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.
|
||||
@@ -0,0 +1,510 @@
|
||||
# Release Hygiene Refactoring Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Make every release-hygiene path truthful by fixing the sample-off architecture gate, aligning the Gradle 9.0.0 wrapper and CI validation, making Docker cache stages valid without `.git`, completing SpotBugs analysis classpaths, and removing the observed Gradle 10 deprecation.
|
||||
|
||||
**Architecture:** Leaf-specific architecture rules move to their owning leaf while root tests remain cross-module. Build inputs become explicit: Docker copies registry inputs, evidence-only Git validation executes only in evidence tasks, wrapper bytes/checksums are fixed, and SpotBugs derives auxiliary inputs from the source set it analyzes.
|
||||
|
||||
**Tech Stack:** Java 21, Spring Boot 4.0.0, Gradle 9.0.0 Groovy DSL, ArchUnit 1.3.0, SpotBugs Gradle plugin 6.5.6/SpotBugs 4.10.2, Bash, Docker/BuildKit, GitHub Actions.
|
||||
|
||||
## Global Constraints
|
||||
|
||||
- Preserve all 19 leaf identities and production dependency edges from `src/config/architecture/modules.json`.
|
||||
- `domain-core` and `application-core` gain no framework, transport, database, or cloud dependency.
|
||||
- Do not weaken an architecture rule with a global `allowEmptyShould(true)`.
|
||||
- Keep Gradle at exactly `9.0.0` in this plan.
|
||||
- Set `distributionSha256Sum=8fad3d78296ca518113f3d29016617c7f9367dc005f932bd9d93bf45ba46072b`.
|
||||
- The official Gradle 9.0.0 wrapper JAR SHA-256 is `76805e32c009c0cf0dd5d206bddc9fb22ea42e84db904b764f3047de095493f3`.
|
||||
- Pin `gradle/actions/wrapper-validation` to commit `3f131e8634966bd73d06cc69884922b02e6faf92` in workflows that invoke Gradle.
|
||||
- Docker images do not receive `.git`; full evidence revisions arrive through `-PgitRevision`/CI attestation.
|
||||
- SpotBugs dependency scopes are not widened to silence missing-class output.
|
||||
- Agents do not stage, commit, amend, or push; commit steps from the generic workflow are replaced by diff/status evidence.
|
||||
|
||||
---
|
||||
|
||||
### Task 1: Move the Object Storage Architecture Rule to Its Owning Leaf
|
||||
|
||||
**Files:**
|
||||
- Create: `src/adapter/outbound/objectstorage/src/test/java/dev/caskeleton/adapter/outbound/objectstorage/ObjectStorageArchitectureTest.java`
|
||||
- Modify: `src/adapter/outbound/objectstorage/build.gradle`
|
||||
- Modify: `src/adapter/outbound/objectstorage/gradle.lockfile`
|
||||
- Modify: `src/app-bootstrap/src/test/java/dev/caskeleton/bootstrap/architecture/CleanArchitectureTest.java:1586-1608`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: production classes under `dev.caskeleton.adapter.outbound.objectstorage..` and application/shared contracts already on the Object Storage test classpath.
|
||||
- Produces: an owner-local ArchUnit rule named `OBJECT_STORAGE_ADAPTER_METHOD_RETURNS_ONLY_APPLICATION_OR_PRIMITIVES`; a sample-off root suite with no Object Storage presence requirement.
|
||||
|
||||
- [ ] **Step 1: Reproduce the existing failing regression**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
cd src
|
||||
./gradlew :app-bootstrap:sampleOffTest --tests '*CleanArchitectureTest' --console=plain
|
||||
```
|
||||
|
||||
Expected: FAIL only at `OBJECT_STORAGE_ADAPTER_METHOD_RETURNS_ONLY_APPLICATION_OR_PRIMITIVES` because no matching classes are present.
|
||||
|
||||
- [ ] **Step 2: Add the owner-local test before removing the root rule**
|
||||
|
||||
Create a package-local ArchUnit test that imports production classes from the Object Storage package and applies this rule:
|
||||
|
||||
```java
|
||||
@AnalyzeClasses(packages = "dev.caskeleton.adapter.outbound.objectstorage")
|
||||
class ObjectStorageArchitectureTest {
|
||||
@ArchTest
|
||||
static final ArchRule OBJECT_STORAGE_ADAPTER_METHOD_RETURNS_ONLY_APPLICATION_OR_PRIMITIVES =
|
||||
methods()
|
||||
.that()
|
||||
.areDeclaredInClassesThat()
|
||||
.resideInAPackage("..adapter.outbound.objectstorage..")
|
||||
.and()
|
||||
.areDeclaredInClassesThat()
|
||||
.haveSimpleNameEndingWith("Adapter")
|
||||
.and()
|
||||
.arePublic()
|
||||
.and()
|
||||
.areNotStatic()
|
||||
.should()
|
||||
.notHaveRawReturnType(
|
||||
JavaClass.Predicates.resideInAnyPackage(
|
||||
"..adapter.outbound..",
|
||||
"..adapter.inbound.web..",
|
||||
"..adapter.outbound.persistence.."))
|
||||
.allowEmptyShould(false);
|
||||
}
|
||||
```
|
||||
|
||||
Add the owner-local test dependency:
|
||||
|
||||
```groovy
|
||||
testImplementation 'com.tngtech.archunit:archunit-junit5:1.3.0'
|
||||
```
|
||||
|
||||
Refresh only the Object Storage leaf lock state with its existing `resolveAndLockAll --write-locks`
|
||||
task. This is a test-scope dependency; do not add a production project or external dependency edge.
|
||||
|
||||
- [ ] **Step 3: Run the owner test while the root regression remains red**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
cd src
|
||||
./gradlew :adapter:outbound:objectstorage:resolveAndLockAll --write-locks --console=plain
|
||||
./gradlew :adapter:outbound:objectstorage:test --tests '*ObjectStorageArchitectureTest' --console=plain
|
||||
```
|
||||
|
||||
Expected: PASS with matching production adapter methods.
|
||||
|
||||
- [ ] **Step 4: Remove only the misplaced root rule**
|
||||
|
||||
Delete the `OBJECT_STORAGE_ADAPTER_METHOD_RETURNS_ONLY_APPLICATION_OR_PRIMITIVES` field from `CleanArchitectureTest`; do not change neighboring cross-module rules.
|
||||
|
||||
- [ ] **Step 5: Verify both ownership paths**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
cd src
|
||||
./gradlew :adapter:outbound:objectstorage:test :app-bootstrap:sampleOffTest --console=plain
|
||||
```
|
||||
|
||||
Expected: PASS, zero failed tests.
|
||||
|
||||
- [ ] **Step 6: Record diff evidence without committing**
|
||||
|
||||
Run `git diff --check` and `git status --short`; retain the output for the task review.
|
||||
|
||||
### Task 2: Align and Validate the Gradle 9.0.0 Wrapper
|
||||
|
||||
**Files:**
|
||||
- Create: `.github/scripts/verify-gradle-wrapper.sh`
|
||||
- Modify: `src/gradle/wrapper/gradle-wrapper.properties`
|
||||
- Regenerate: `src/gradle/wrapper/gradle-wrapper.jar`, `src/gradlew`, `src/gradlew.bat`
|
||||
- Modify: `.github/workflows/ci-quality-gates.yml`
|
||||
- Modify: `.github/workflows/dependency-vulnerability.yml`
|
||||
- Modify: `.github/workflows/jpa-r2-evidence.yml`
|
||||
- Modify: `.github/workflows/object-storage-qualification.yml`
|
||||
- Modify: `.github/workflows/redis-production-readiness.yml`
|
||||
- Lock without modification: `.github/workflows/link-check.yml`
|
||||
- Modify: `src/app-bootstrap/src/test/java/dev/caskeleton/bootstrap/contract/DeveloperExperienceContractTest.java`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: repository root as argument 1, wrapper properties/JAR, and every YAML workflow under `.github/workflows`.
|
||||
- Produces: executable `verify-gradle-wrapper.sh` with exit 0 only for the exact Gradle 9.0.0 wrapper, the reviewed six-file workflow path/SHA-256 lock, the repository's restricted canonical workflow grammar, and jobs where an unconditional pinned validation step gates every reachable Gradle invocation.
|
||||
|
||||
- [ ] **Step 1: Write failing executable-contract tests**
|
||||
|
||||
Add a `DeveloperExperienceContractTest` case that runs:
|
||||
|
||||
```java
|
||||
Process process =
|
||||
new ProcessBuilder("bash", ".github/scripts/verify-gradle-wrapper.sh", REPOSITORY_ROOT.toString())
|
||||
.directory(REPOSITORY_ROOT.toFile())
|
||||
.redirectErrorStream(true)
|
||||
.start();
|
||||
assertThat(process.waitFor()).as(new String(process.getInputStream().readAllBytes(), UTF_8)).isZero();
|
||||
```
|
||||
|
||||
Add a second case that copies wrapper properties/JAR and workflows to `@TempDir`, changes the distribution checksum, runs the script against that fixture root, and asserts a non-zero exit. The production mutation this test catches is accepting a wrong wrapper or distribution checksum.
|
||||
|
||||
- [ ] **Step 2: Verify RED**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
cd src
|
||||
./gradlew :app-bootstrap:test --tests '*DeveloperExperienceContractTest' --console=plain
|
||||
```
|
||||
|
||||
Expected: FAIL because `.github/scripts/verify-gradle-wrapper.sh` does not exist and the checked-in wrapper is not the Gradle 9.0.0 JAR.
|
||||
|
||||
- [ ] **Step 3: Implement the wrapper verifier**
|
||||
|
||||
The Bash script must:
|
||||
|
||||
```text
|
||||
1. require exactly one repository-root argument;
|
||||
2. require the exact ordered eight-line wrapper-properties file, including the Gradle 9.0.0 URL
|
||||
and distribution checksum from Global Constraints;
|
||||
3. reject duplicate, alternate-separator, escaped, continued, reordered, or extra properties;
|
||||
4. compare the wrapper JAR SHA-256 with the exact Gradle 9.0.0 JAR hash;
|
||||
5. enumerate every top-level `.yml`/`.yaml` workflow, reject symlinks/special files, and compare the
|
||||
exact sorted six-path set and SHA-256 values to the verifier's embedded reviewed workflow lock;
|
||||
additions, removals, renames, or byte changes are failures;
|
||||
6. structurally validate the supported block grammar before admission and emit specific diagnostics
|
||||
for recognized noncanonical `jobs`/job/`steps` containers, flow collections, aliases, anchors,
|
||||
tags, merge keys, encoded or multiline action scalars, and quoted/escaped run scalars; YAML
|
||||
semantics outside this deliberately partial diagnostic parser remain covered by the primary
|
||||
byte lock rather than an overclaim of complete Bash YAML parsing;
|
||||
7. require every Gradle-running job to order checkout, the exact wrapper-validation action with
|
||||
stable `id: gradle-wrapper-validation`, and every Gradle invocation;
|
||||
8. accept the validation step only with its exact canonical name/id/uses fields and no `if`,
|
||||
`continue-on-error`, `with`, `env`, timeout, or other weakening field;
|
||||
9. finalize every Gradle step, not only the first. A Gradle step may have no condition or exactly
|
||||
`${{ always() && steps.gradle-wrapper-validation.outcome == 'success' }}`; bare `always()`,
|
||||
failure/cancelled paths, `continue-on-error`, and other reachability expressions fail closed;
|
||||
10. treat literal run-block body text only as shell data, never as an action field, and require each
|
||||
raw Gradle reference admitted by the gate to resolve to a canonical job;
|
||||
11. print `gradle-wrapper-contract: PASS` only when every check succeeds.
|
||||
```
|
||||
|
||||
For an intentional workflow edit, review the complete workflow diff, verify that no workflow path
|
||||
is a symlink/special file, regenerate the entire sorted `sha256sum` list with:
|
||||
|
||||
```bash
|
||||
find .github/workflows -mindepth 1 -maxdepth 1 \
|
||||
\( -name '*.yml' -o -name '*.yaml' \) ! -type f -print # must print nothing
|
||||
find .github/workflows -mindepth 1 -maxdepth 1 -type f \
|
||||
\( -name '*.yml' -o -name '*.yaml' \) -print0 \
|
||||
| LC_ALL=C sort -z | xargs -0 sha256sum
|
||||
```
|
||||
|
||||
Replace the complete sorted embedded array in the same reviewed change. Never refresh only the
|
||||
failing digest as a build-unblock shortcut.
|
||||
|
||||
- [ ] **Step 4: Regenerate the wrapper twice and add the distribution checksum**
|
||||
|
||||
Run in `src/`:
|
||||
|
||||
```bash
|
||||
./gradlew wrapper --gradle-version 9.0.0 --distribution-type bin
|
||||
./gradlew wrapper --gradle-version 9.0.0 --distribution-type bin
|
||||
```
|
||||
|
||||
Then add the exact `distributionSha256Sum` property immediately after `distributionUrl`.
|
||||
|
||||
- [ ] **Step 5: Add the pinned validation action to every Gradle workflow job**
|
||||
|
||||
After each checkout step and before setup/cache/build invokes Gradle, add:
|
||||
|
||||
```yaml
|
||||
- name: Validate Gradle wrapper
|
||||
id: gradle-wrapper-validation
|
||||
uses: gradle/actions/wrapper-validation@3f131e8634966bd73d06cc69884922b02e6faf92 # gradle/actions@v6
|
||||
```
|
||||
|
||||
Jobs without a Gradle invocation do not need the action. A sanitizer that intentionally executes
|
||||
after a failed test must use the exact guarded condition shown above so wrapper-validation failure
|
||||
still prevents Gradle. Preserve that behavior in Redis rather than using bare `always()`.
|
||||
|
||||
- [ ] **Step 6: Verify GREEN and mutation rejection**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
bash .github/scripts/verify-gradle-wrapper.sh .
|
||||
cd src
|
||||
./gradlew :app-bootstrap:test --tests '*DeveloperExperienceContractTest' --console=plain
|
||||
```
|
||||
|
||||
Expected: script prints `gradle-wrapper-contract: PASS`; focused tests pass; executable mutations
|
||||
reject checksum/property overrides, missing validation per job, named/anonymous/quoted/escaped and
|
||||
continued action variants, encoded run scalars, block/alias/merge/flow YAML forms, validation-step
|
||||
control fields, Gradle steps reachable after validation failure, custom-shell or alternate-wrapper
|
||||
paths, duplicate encoded jobs, workflow additions/removals/symlinks, and otherwise innocuous byte
|
||||
drift through the primary workflow lock.
|
||||
|
||||
- [ ] **Step 7: Record diff evidence without committing**
|
||||
|
||||
Run `sha256sum src/gradle/wrapper/gradle-wrapper.jar`, `git diff --check`, and `git status --short`.
|
||||
|
||||
### Task 3: Make Docker Build Configuration Inputs Explicit
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/Dockerfile:39-66`
|
||||
- Modify: `src/Dockerfile.sample:50-75`
|
||||
- Modify: `src/build.gradle:2153-2181` and all Redis evidence consumers
|
||||
- Modify: `src/adapter/outbound/cache-redis/build.gradle` (leaf evidence consumers)
|
||||
- Modify: `src/app-bootstrap/build.gradle`
|
||||
- Modify: `src/sample-portfolio/build.gradle`
|
||||
- Modify: `src/app-bootstrap/src/test/java/dev/caskeleton/bootstrap/contract/DeveloperExperienceContractTest.java`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `config/**`, Gradle source/build files, `-PgitRevision`, and the `bootJar` archive provider.
|
||||
- Produces: `:app-bootstrap:stageDockerJar` and `:sample-portfolio:stageDockerJar`, each writing exactly `build/docker/application.jar`; evidence metadata is resolved only when a Redis evidence task executes.
|
||||
|
||||
- [ ] **Step 1: Write failing build-contract tests**
|
||||
|
||||
Add tests that split each Dockerfile at its first `RUN ./gradlew` and assert the preceding section
|
||||
uses repository-preserving `WORKDIR /build/src` and contains `COPY config/ ./config/`. Add tests
|
||||
that require the Dockerfiles to run `stageDockerJar` and copy the exact
|
||||
`build/docker/application.jar`, with no `ls | grep | head` selection. Add a test that runs
|
||||
`./gradlew help -PgitRevision=0123456789abcdef0123456789abcdef01234567` from a temporary Git-less
|
||||
copy containing the same files as the dependency-cache stage. Add three self-contained evidence-task
|
||||
fixtures under temporary repository roots: one uses a `.git` directory, one uses a worktree `.git`
|
||||
metadata file, and one uses a dangling `.git` symlink. All prepend a fake `git` to `PATH` and require
|
||||
the exact named failure for `rev-parse` or `status` process errors; the symlink fixture must also prove
|
||||
the link entry exists with `NOFOLLOW_LINKS`. These tests must copy the minimum build/registry inputs
|
||||
and invoke the fixture wrapper; they must not assert or execute the ambient checkout's `.git`.
|
||||
|
||||
- [ ] **Step 2: Verify RED**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
cd src
|
||||
./gradlew :app-bootstrap:test --tests '*DeveloperExperienceContractTest' --console=plain
|
||||
```
|
||||
|
||||
Expected: FAIL because neither cache stage copies `config/**`, both select JARs with shell matching, and Git is resolved during configuration.
|
||||
|
||||
- [ ] **Step 3: Add deterministic Docker staging tasks**
|
||||
|
||||
In both executable modules register:
|
||||
|
||||
```groovy
|
||||
tasks.register('stageDockerJar', Sync) {
|
||||
dependsOn tasks.named('bootJar')
|
||||
from(tasks.named('bootJar').flatMap { it.archiveFile })
|
||||
into(layout.buildDirectory.dir('docker'))
|
||||
rename { 'application.jar' }
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Update both Dockerfiles**
|
||||
|
||||
Use `WORKDIR /build/src` so repository-relative registry paths resolve under `/build/src/**`, copy
|
||||
`config/` before the first Gradle invocation, invoke the correct `stageDockerJar` task with the
|
||||
existing release/revision properties, and copy only the fixed `build/docker/application.jar` path
|
||||
into the runtime stage.
|
||||
|
||||
- [ ] **Step 5: Move Redis Git evidence resolution to execution time**
|
||||
|
||||
Replace the eager `String` values with closures/providers invoked from evidence task actions:
|
||||
|
||||
```groovy
|
||||
Closure<Map<String, String>> resolveRedisSourceEvidence = {
|
||||
File gitMetadata = rootProject.file('../.git')
|
||||
if (!java.nio.file.Files.exists(
|
||||
gitMetadata.toPath(), java.nio.file.LinkOption.NOFOLLOW_LINKS)) {
|
||||
String attested = providers.gradleProperty('gitRevision')
|
||||
.orElse(providers.environmentVariable('GITHUB_SHA'))
|
||||
.orElse(providers.environmentVariable('GIT_SHA'))
|
||||
.getOrElse('')
|
||||
if (!(attested ==~ /[0-9a-f]{40}/)) {
|
||||
throw new GradleException(
|
||||
'Redis evidence requires an exact 40-character source revision.')
|
||||
}
|
||||
return [revision: attested, treeState: 'ATTESTED']
|
||||
}
|
||||
|
||||
String headFailure = 'Redis evidence failed to resolve checked-out Git HEAD.'
|
||||
def headExecution
|
||||
try {
|
||||
headExecution = providers.exec {
|
||||
commandLine 'git', 'rev-parse', 'HEAD'
|
||||
ignoreExitValue = true
|
||||
}
|
||||
if (headExecution.result.get().exitValue != 0) {
|
||||
throw new GradleException(headFailure)
|
||||
}
|
||||
} catch (GradleException exception) {
|
||||
if (exception.message == headFailure) {
|
||||
throw exception
|
||||
}
|
||||
throw new GradleException(headFailure, exception)
|
||||
}
|
||||
String checkedOut = headExecution.standardOutput.asText.getOrElse('').trim()
|
||||
if (!(checkedOut ==~ /[0-9a-f]{40}/)) {
|
||||
throw new GradleException(headFailure)
|
||||
}
|
||||
String supplied = providers.gradleProperty('gitRevision')
|
||||
.orElse(providers.environmentVariable('GITHUB_SHA'))
|
||||
.orElse(providers.environmentVariable('GIT_SHA'))
|
||||
.orElse(checkedOut)
|
||||
.getOrElse('')
|
||||
if (!(supplied ==~ /[0-9a-f]{40}/)) {
|
||||
throw new GradleException('Redis evidence requires an exact 40-character source revision.')
|
||||
}
|
||||
if (!checkedOut.isBlank() && supplied != checkedOut) {
|
||||
throw new GradleException('Redis evidence source revision does not match checked-out HEAD.')
|
||||
}
|
||||
|
||||
String statusFailure = 'Redis evidence failed to inspect checked-out Git status.'
|
||||
def statusExecution
|
||||
try {
|
||||
statusExecution = providers.exec {
|
||||
commandLine 'git', 'status', '--porcelain', '--untracked-files=normal'
|
||||
ignoreExitValue = true
|
||||
}
|
||||
if (statusExecution.result.get().exitValue != 0) {
|
||||
throw new GradleException(statusFailure)
|
||||
}
|
||||
} catch (GradleException exception) {
|
||||
if (exception.message == statusFailure) {
|
||||
throw exception
|
||||
}
|
||||
throw new GradleException(statusFailure, exception)
|
||||
}
|
||||
String treeState = statusExecution.standardOutput.asText.getOrElse('').isBlank()
|
||||
? 'CLEAN'
|
||||
: 'DIRTY'
|
||||
[revision: supplied, treeState: treeState]
|
||||
}
|
||||
```
|
||||
|
||||
Each evidence-producing root `doLast` and each leaf evidence test's root-suite `afterSuite` resolves
|
||||
this once and uses the returned values for all generated/validated artifacts. The resolver is
|
||||
exposed as `rootProject.ext.resolveRedisSourceEvidence`; eager scalar ext properties are removed.
|
||||
Non-evidence tasks never call the closure. Any repository-root `.git` filesystem entry is detected
|
||||
without following symbolic links, so a directory, worktree metadata file, or dangling symlink always
|
||||
selects the checkout branch. Both Git processes must start, exit zero, and return valid evidence
|
||||
before `CLEAN` or `DIRTY` can be emitted. `ATTESTED` is reserved for a truly absent `.git` entry in
|
||||
an explicitly Git-less build with an exact supplied revision; a Git execution failure must never
|
||||
fall back to it.
|
||||
|
||||
- [ ] **Step 6: Verify GREEN without `.git` and verify evidence mismatch failure**
|
||||
|
||||
Run the focused contract test, `./gradlew help` in the Git-less fixture with a 40-character
|
||||
`gitRevision`, and one Redis evidence task in the real checkout. The Git-less help invocation must
|
||||
pass; a Git-less Redis evidence task with a short revision must fail with the named message. Separate
|
||||
self-contained fixtures must cover a `.git` directory whose `rev-parse` fails, a `.git` worktree file
|
||||
whose `status` fails, and a dangling `.git` symlink whose Git invocation fails. Each fixture must
|
||||
assert the corresponding named fail-closed diagnostic instead of accepting a generic non-zero exit.
|
||||
|
||||
- [ ] **Step 7: Run actual Docker smoke when Docker is available**
|
||||
|
||||
Run both image builds with `--no-cache`. If Docker is unavailable, record the exact blocker and leave these commands as remaining risk; do not claim Docker success from string tests.
|
||||
|
||||
- [ ] **Step 8: Record diff evidence without committing**
|
||||
|
||||
Run `git diff --check` and `git status --short`.
|
||||
|
||||
### Task 4: Complete SpotBugs Auxiliary Classpaths and Remove the Gradle 10 Warning
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/build.gradle:208-360`
|
||||
- Modify: `src/build.gradle:1760-1795`
|
||||
- Test/verify: app-bootstrap redisComposition, inbound GraphQL main, inbound gRPC main SpotBugs tasks
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: every leaf's `SourceSetContainer` and the SpotBugs task named for each source set.
|
||||
- Produces: each SpotBugs task's `auxClassPaths` containing `sourceSet.runtimeClasspath - sourceSet.output` and a required XML report whose analysis errors/missing classes are checked after execution; `verifyApplicationCoreDependencyPurity` uses a configuration-time `Project` reference and declares its execution-time configuration traversal incompatible with the configuration cache.
|
||||
|
||||
- [ ] **Step 1: Capture the failing static-analysis evidence**
|
||||
|
||||
Run clean focused SpotBugs tasks and save output. Expected RED messages name Spring Session, `io.micrometer.context.ContextSnapshot`, and protobuf types as classes needed for analysis.
|
||||
|
||||
- [ ] **Step 2: Capture the Gradle 10 deprecation RED**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
cd src
|
||||
./gradlew verifyApplicationCoreDependencyPurity --warning-mode=fail --console=plain
|
||||
```
|
||||
|
||||
Expected: FAIL on execution-time `Task.project` access.
|
||||
|
||||
- [ ] **Step 3: Configure source-set-derived auxiliary classpaths**
|
||||
|
||||
After applying SpotBugs in each leaf, configure:
|
||||
|
||||
```groovy
|
||||
sourceSets.configureEach { sourceSet ->
|
||||
String taskName = "spotbugs${sourceSet.name.capitalize()}"
|
||||
tasks.named(taskName, com.github.spotbugs.snom.SpotBugsTask) {
|
||||
auxClassPaths.from(sourceSet.runtimeClasspath - sourceSet.output)
|
||||
def xmlAnalysisReport = reports.maybeCreate('xml')
|
||||
xmlAnalysisReport.required.set(true)
|
||||
doLast {
|
||||
List<String> analysisFailures =
|
||||
spotBugsAnalysisFailures(xmlAnalysisReport.outputLocation.get().asFile)
|
||||
if (!analysisFailures.isEmpty()) {
|
||||
throw new GradleException(
|
||||
"${path}: SpotBugs analysis incomplete:\n " +
|
||||
analysisFailures.join('\n '))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Do not add compile/runtime dependencies solely for SpotBugs. The XML parser fails on a missing or
|
||||
malformed report, malformed `Errors` counts, any `MissingClass`, and any analysis `Error`; ordinary
|
||||
`BugInstance` findings remain governed by the existing main/test severity policy. Wire an
|
||||
executable `verifySpotBugsAnalysisFailureContract` fixture into every leaf `check` so clean and
|
||||
advisory-bug-only reports pass while missing-class and analysis-error reports fail.
|
||||
|
||||
- [ ] **Step 4: Remove execution-time project access**
|
||||
|
||||
Resolve `Project applicationCoreProject = project(':application-core')` before registering
|
||||
`verifyApplicationCoreDependencyPurity`; capture that variable in `doLast` instead of calling
|
||||
`project(...)` from the task action. Because the action still traverses project configurations at
|
||||
execution time, declare
|
||||
`notCompatibleWithConfigurationCache('Inspects project configurations at execution time')` rather
|
||||
than making an unsupported compatibility claim.
|
||||
|
||||
- [ ] **Step 5: Verify GREEN**
|
||||
|
||||
Run `verifySpotBugsAnalysisFailureContract`, the three clean focused SpotBugs tasks, and
|
||||
`verifyApplicationCoreDependencyPurity --warning-mode=fail`. Expected: exit 0, XML
|
||||
`Errors errors="0" missingClasses="0"`, and no missing-analysis-class/deprecation output.
|
||||
|
||||
- [ ] **Step 6: Run release-hygiene aggregate verification**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
cd src
|
||||
./gradlew clean check :app-bootstrap:sampleOffTest verifyPublicPathSnapshot verifyDependencyLocks --no-daemon --console=plain --warning-mode=fail
|
||||
cd ..
|
||||
bash .github/scripts/verify-gate-matrix.sh
|
||||
bash .github/scripts/verify-gradle-wrapper.sh .
|
||||
```
|
||||
|
||||
Expected: every command exits 0; no skipped mandatory gate, missing SpotBugs class, or Gradle deprecation.
|
||||
|
||||
- [ ] **Step 7: Record final diff evidence without committing**
|
||||
|
||||
Run `git diff --check`, `git diff --stat`, and `git status --short`. Dispatch the complete diff for architecture/spec and code-quality review.
|
||||
|
||||
## Plan Self-Review
|
||||
|
||||
- Spec coverage: every release-hygiene design decision maps to Tasks 1-4.
|
||||
- Type consistency: both executable modules expose the same `stageDockerJar` task and output path; Redis evidence uses one `Map<String,String>` resolver contract.
|
||||
- Architecture: no production dependency edge changes are required.
|
||||
- Test discipline: each behavior has a named failing command or executable mutation fixture before implementation.
|
||||
- Commit policy: all generic commit steps are replaced with diff/status evidence.
|
||||
@@ -0,0 +1,73 @@
|
||||
# Client-Safe Error Boundary Implementation Plan
|
||||
|
||||
> **Execution:** Follow `superpowers:test-driven-development`; request an independent code review
|
||||
> before advancing to the next P1 batch.
|
||||
|
||||
**Goal:** Ensure public HTTP error envelopes contain only allowlisted messages and bounded safe
|
||||
metadata, never raw exceptions or request values.
|
||||
|
||||
**Architecture:** The inbound web adapter maps operational codes to fixed public messages. The
|
||||
sample consumer owns a parallel domain-code mapping. Exception diagnostics stay behind the
|
||||
transport boundary.
|
||||
|
||||
**Tech Stack:** Java 21, Spring Boot 4.0.0, JUnit 6/JUnit Jupiter, AssertJ, MockMvc.
|
||||
|
||||
## Constraints
|
||||
|
||||
- Preserve all completed P0 and verification-purity changes in the dirty worktree.
|
||||
- Preserve every error code/status/category/retryable value.
|
||||
- Preserve safe protocol details and required headers.
|
||||
- Do not leak request DTOs or transport types into application/domain.
|
||||
- Do not stage, commit, amend, or push.
|
||||
|
||||
### Task 1: Operational Handler RED Contracts
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/adapter/inbound/web/src/test/java/dev/caskeleton/adapter/inbound/web/error/GlobalExceptionHandlerTest.java`
|
||||
- Modify: `src/adapter/inbound/web/src/test/java/dev/caskeleton/adapter/inbound/web/error/TransportErrorHandlingTest.java`
|
||||
- Create: `src/adapter/inbound/web/src/test/java/dev/caskeleton/adapter/inbound/web/error/NoResourceFoundErrorHandlingTest.java`
|
||||
|
||||
- [x] Add secret-sentinel tests for mapping, illegal argument, adapter disabled, authentication,
|
||||
authorization, precondition, pagination, and cursor exceptions.
|
||||
- [x] Add validation tests proving rejected values, interpolated/default messages, and iterable
|
||||
keys/indices are absent while normalized fields plus allowlisted reason codes/fixed messages remain.
|
||||
- [x] Add transport tests proving raw request URLs and content-type values are not echoed.
|
||||
- [x] Add a real MVC resource-resolver test for a sentinel-bearing static-resource 404.
|
||||
- [x] Run the focused tests and record RED against the current raw-message implementation (30 tests, 9 expected failures).
|
||||
|
||||
### Task 2: Operational Allowlist Implementation
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/adapter/inbound/web/src/main/java/dev/caskeleton/adapter/inbound/web/error/ClientSafeErrorMessages.java`
|
||||
- Create: `src/adapter/inbound/web/src/main/java/dev/caskeleton/adapter/inbound/web/error/ClientSafeValidationDetails.java`
|
||||
- Modify: `src/adapter/inbound/web/src/main/java/dev/caskeleton/adapter/inbound/web/error/GlobalExceptionHandler.java`
|
||||
- Modify: `src/adapter/inbound/web/README.md`
|
||||
|
||||
- [x] Add code-specific fixed operational messages with a safe category fallback.
|
||||
- [x] Replace every public `ex.getMessage()`/rejected-value/raw-URL path.
|
||||
- [x] Discard validation message/value data, normalize field paths, strip iterable keys/indices, and
|
||||
emit only allowlisted reason codes with fixed messages.
|
||||
- [x] Route both `NoHandlerFoundException` and `NoResourceFoundException` through the same safe 404 envelope.
|
||||
- [x] Retain safe field/reason/expected-type/supported-method/media-type details and `Allow`.
|
||||
- [x] Run the operational/transport tests and confirm GREEN.
|
||||
|
||||
### Task 3: Sample Domain RED and Implementation
|
||||
|
||||
**Files:**
|
||||
- Create: `src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/adapter/inbound/web/error/PortfolioClientSafeErrorMessages.java`
|
||||
- Modify: `src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/adapter/inbound/web/error/DomainExceptionHandler.java`
|
||||
- Modify: `src/sample-portfolio/src/test/java/dev/caskeleton/sample/portfolio/adapter/inbound/web/error/DomainExceptionHandlerTest.java`
|
||||
|
||||
- [x] Add ID/title/reason sentinel tests and confirm RED (4 expected failures).
|
||||
- [x] Map every `PortfolioErrorCode` to fixed public text and use it from the advice.
|
||||
- [x] Confirm code/status/category remain unchanged and sentinels are absent.
|
||||
|
||||
### Task 4: Focused and Architecture Verification
|
||||
|
||||
- [x] Run `./gradlew :adapter:inbound:web:test --console=plain`.
|
||||
- [x] Run `./gradlew :sample-portfolio:test --console=plain`.
|
||||
- [x] Run focused Spotless/Checkstyle/SpotBugs tasks for both modules.
|
||||
- [x] Run `./gradlew verifyCleanArchitectureDependencies --console=plain`.
|
||||
- [x] Run `git diff --check` and request an independent read-only review.
|
||||
- [x] Apply the independent review findings and receive a no-Critical/no-Important code re-review;
|
||||
align this design/plan with the final validation and resource-404 contract.
|
||||
@@ -0,0 +1,80 @@
|
||||
# Conditional Inbound Transport Boundary Implementation Plan
|
||||
|
||||
> **Execution:** Apply TDD independently per transport, then run exact no-skip qualification and an
|
||||
> independent read-only review before beginning P2 cleanup.
|
||||
|
||||
**Goal:** Make GraphQL, gRPC, and WebSocket opt-in status truthful, fail closed on unsafe activation,
|
||||
and release-blocked by real protocol evidence without adding them to the default runtime.
|
||||
|
||||
### Task 1: Runtime Membership and Opt-In Composition
|
||||
|
||||
**Files:** `src/config/architecture/modules.json`, `src/settings.gradle`, `src/build.gradle`,
|
||||
`src/app-bootstrap/build.gradle`, app-bootstrap conditional transport tests
|
||||
|
||||
- [ ] Add and fail-closed validate exact `runtime_memberships` for all 19 leaves.
|
||||
- [ ] Compare registry membership to both composition roots' direct production project edges.
|
||||
- [ ] Add an isolated conditional-transport test classpath containing all three opt-in leaves.
|
||||
- [ ] Prove the default graphs omit them and the explicit qualification graph contains them.
|
||||
|
||||
### Task 2: gRPC Safe Activation and Wire Errors
|
||||
|
||||
**Files:** `src/adapter/inbound/grpc/**`
|
||||
|
||||
- [ ] Add RED tests for disabled bean/listener absence and safe property defaults/validation.
|
||||
- [ ] Add real Netty feature RPC tests for auth success/failure and reflection disabled.
|
||||
- [ ] Add RED tests for throw, `onError(ApiErrorCarrier)`, and raw status sentinel paths.
|
||||
- [ ] Implement loopback-only explicit insecure mode, required feature authentication policy, and
|
||||
`ServerCall.close` sanitization.
|
||||
- [ ] Update dependencies, locks, README, and CLAUDE truthfully.
|
||||
|
||||
### Task 3: GraphQL Real HTTP Boundary
|
||||
|
||||
**Files:** `src/adapter/inbound/graphql/**`
|
||||
|
||||
- [ ] Add random-port HTTP tests for auth, CORS, GraphiQL/introspection policy, and health.
|
||||
- [ ] Add carrier/unknown exception sentinels and assert absence from the complete JSON response.
|
||||
- [ ] Change production resolver/config only where the RED wire contract proves necessary.
|
||||
- [ ] Update dependencies, locks, README, and CLAUDE truthfully.
|
||||
|
||||
### Task 4: WebSocket Safe Activation and Wire Boundary
|
||||
|
||||
**Files:** `src/adapter/inbound/websocket/**`
|
||||
|
||||
- [ ] Add RED settings/disabled-context tests and real STOMP origin/auth/subscription tests.
|
||||
- [ ] Add RED broker-send and ERROR-frame sentinel tests.
|
||||
- [ ] Add RED no-projection/no-broadcast plus safe projection broadcast tests.
|
||||
- [ ] Implement disabled default, validated settings, inbound authorization, safe error handler, and
|
||||
explicit primitive projection allowlist.
|
||||
- [ ] Update dependencies, locks, README, and CLAUDE truthfully.
|
||||
|
||||
### Task 5: Exact No-Skip Release Gate
|
||||
|
||||
**Files:** `src/build.gradle`, `.github/workflows/ci-quality-gates.yml`,
|
||||
`.github/ci-gate-matrix.yml`, `.github/scripts/verify-gate-matrix.sh`, wrapper manifest contract
|
||||
|
||||
- [ ] Register exact per-transport Test lanes with no-match/no-discovery/zero-skip enforcement.
|
||||
- [ ] Register the aggregate `conditionalTransportQualification` task.
|
||||
- [ ] Invoke it explicitly from the release-blocking quality job and add the gate-matrix record.
|
||||
- [ ] Add semantic tests that fail if any required lane or workflow invocation disappears.
|
||||
|
||||
### Task 6: Verification and Review
|
||||
|
||||
- [ ] Run each leaf `check`, exact qualification, app-bootstrap composition contract, dependency
|
||||
locks, env keys, architecture, public path, wrapper validation, and `git diff --check`.
|
||||
- [ ] Run full `test`/`check` in proportion to the cross-cutting registry/build changes.
|
||||
- [ ] Request independent read-only review; resolve all Critical/Important findings.
|
||||
- [ ] Capture the batch in the LLM Wiki before final completion reporting.
|
||||
|
||||
### Explicit P2 Deferral
|
||||
|
||||
- GraphQL feature schema, field auth, cost/depth, persisted queries, DataLoader, subscriptions.
|
||||
- gRPC TLS/mTLS, external bind, proto compatibility, deadlines, streaming/backpressure.
|
||||
- WebSocket broker relay, multi-node delivery, resume/replay, backpressure, versioned feature catalog.
|
||||
- Transport dashboards, SLO alerts, and provider/ingress qualification.
|
||||
# Implementation status
|
||||
|
||||
- Completed on 2026-08-02.
|
||||
- Verified by `conditionalTransportQualification`: GraphQL 8, gRPC 15, WebSocket 5,
|
||||
composition 1; skipped 0.
|
||||
- Verified by the real CI gate-matrix validator and focused bypass regression tests.
|
||||
- Independent review result: READY, Critical 0 / Important 0 / Minor 0.
|
||||
@@ -0,0 +1,268 @@
|
||||
# P2 Verification Governance Refactoring Plan
|
||||
|
||||
## Batch 1 — strict owner-local qualification
|
||||
|
||||
- [x] Add TestKit RED cases for empty source sets, missing FQCNs, disabled-only tests, and one valid
|
||||
test.
|
||||
- [x] Add the shared strict qualification convention.
|
||||
- [x] Move conditional transport and Messaging task registration from root to owner projects.
|
||||
- [x] Adopt the convention for object-storage, Poster migration, and composition qualifications.
|
||||
- [x] Keep root tasks as absolute-path aggregators and verify all evidence XML.
|
||||
- [x] Run focused TestKit, every migrated qualification lane, locks, and independent review.
|
||||
|
||||
Evidence: eight TestKit cases passed fresh; conditional transport ran 8/15/5/1 tests and Messaging
|
||||
ran 15/6/4/29/28 tests with zero skips. All dependency locks passed. Object-storage and Poster
|
||||
required-class preflights passed; protected AWS and Docker-backed full lanes remain environment-
|
||||
qualified. Independent review closed with no remaining Critical, Important, or Minor findings.
|
||||
|
||||
## Batch 2 — tracked contract resources hard-fail
|
||||
|
||||
- [x] Add RED tests proving absent tracked files/directories fail instead of aborting.
|
||||
- [x] Add `RepositoryContractResources` and inject the canonical repository root.
|
||||
- [x] Replace stale tracked-resource assumptions in the contract corpus.
|
||||
- [x] Preserve assumptions only for genuinely optional external infrastructure.
|
||||
- [x] Run focused representative contracts, scan for stale skip language, and run app-bootstrap
|
||||
`check`.
|
||||
|
||||
Evidence (2026-08-02): the fail-closed repository resolver is covered by 11 boundary tests;
|
||||
Runbook coverage and lock-classification contracts passed with zero skips. Independent review found
|
||||
and closed both direct-link and directory-enumeration symlink escapes. A fresh
|
||||
`./gradlew :app-bootstrap:check --no-daemon --console=plain` passed (77 tasks; 18 executed, 59
|
||||
up-to-date), and the final Batch 2 review reported zero Critical, Important, or Minor findings.
|
||||
|
||||
## Batch 3 — real gate-matrix mutation tests
|
||||
|
||||
- [x] Add temporary-fixture tests that execute the shell validator itself.
|
||||
- [x] Make the validator accept a repository-root argument without changing default CI behavior.
|
||||
- [x] Delete the duplicated Java command parser.
|
||||
- [x] Cover deceptive names, suppression flags, missing/duplicate gates, and missing task wiring.
|
||||
- [x] Run the focused contract, real repository validator, and wrapper verifier.
|
||||
|
||||
Evidence (2026-08-02): the initial focused RED compiled and reported seven failing contracts against
|
||||
the old validator. Independent review found arbitrary project-qualified task matching, shorthand
|
||||
step parsing, generic `name:` registration, relocated-script guard evidence, unsafe custom refs,
|
||||
missing `check` wiring evidence, and process-tree cleanup gaps; each was closed with a regression
|
||||
test or bounded cleanup. A final regex-boundary audit also closed custom-task and plugin-ref ERE
|
||||
injection with literal-safe grammars and fixed-string plugin lookup. The final focused contract
|
||||
passed all 16 tests using bounded
|
||||
`ProcessBuilder` execution of the real shell script. `bash .github/scripts/verify-gate-matrix.sh`
|
||||
passed with 27 gates (26 verified and one explicitly delegated),
|
||||
`bash .github/scripts/verify-gradle-wrapper.sh .` passed, `bash -n` and
|
||||
`:app-bootstrap:spotlessJavaCheck` passed, and `git diff --check` reported no whitespace errors.
|
||||
|
||||
## Batch 4 — Redis manifest JSON Schema conformance
|
||||
|
||||
- [x] Add invalid-manifest RED fixtures for bounds, patterns, required fields, and extra fields.
|
||||
- [x] Validate the canonical schema and all manifests with Draft 2020-12 semantics.
|
||||
- [x] Retain Java-catalog equality checks for cross-resource invariants.
|
||||
- [x] Run the focused schema test, cache-redis `check`, and dependency-lock verification.
|
||||
|
||||
Evidence (2026-08-02): the initial focused RED compile failed on the deliberately missing
|
||||
`RedisProgramManifestSchemaValidator` (six `cannot find symbol` errors). NetworkNT 3.0.2 now
|
||||
validates the canonical schema against its bundled Draft 2020-12 meta-schema and validates the
|
||||
exact six closed manifests under strict parsing/configuration. Mutation coverage exercises
|
||||
additional properties, type, required, enum, minimum/maximum, pattern, duplicate JSON keys, and
|
||||
an independent cross-resource duplicate-program-id Java invariant. The first GREEN attempt exposed
|
||||
that the canonical ACL pattern rejected the existing `SCRIPT|LOAD` command form; the pattern was
|
||||
narrowly relaxed before independent review identified that it also admitted dangerous commands.
|
||||
A second RED run failed exactly two tests because the schema had no exact allowlist and accepted
|
||||
`FLUSHALL`, `CONFIG|SET`, and `MODULE|LOAD`. The six canonical manifests contain 265 ACL command
|
||||
occurrences and exactly 37 unique commands; `aclCommands.items` now uses that exact enum so adding
|
||||
a command requires an explicit schema change. Review coverage also rejects a trailing manifest
|
||||
JSON token and a duplicate schema key on the compile path, and pins invalid meta-schema diagnostics
|
||||
to `/type:type`. Final verification passed:
|
||||
`./gradlew :adapter:outbound:cache-redis:test --tests '*RedisProgramManifestContractTest' --console=plain`
|
||||
(12 tests), `./gradlew :adapter:outbound:cache-redis:test
|
||||
:adapter:outbound:cache-redis:spotlessJavaCheck --console=plain`,
|
||||
`./gradlew :adapter:outbound:cache-redis:verifyDependencyLocks
|
||||
:adapter:outbound:cache-redis:spotlessCheck --console=plain`, and
|
||||
`./gradlew :adapter:outbound:cache-redis:check --console=plain`. The owner lock gained only
|
||||
`com.networknt:json-schema-validator:3.0.2` and `com.ethlo.time:itu:1.14.0`; no
|
||||
`tools.jackson.dataformat:jackson-dataformat-yaml` entry is present. `git diff --check` passed.
|
||||
The configured owner `check` remained successful while its SpotBugs test report retained one
|
||||
pre-existing `DMI_RANDOM_USED_ONLY_ONCE` finding in `RedisPrimitiveRuntimeServiceTest`; the new
|
||||
schema validator and contract test introduced no SpotBugs finding.
|
||||
|
||||
## Batch 5 — registry and runbook governance
|
||||
|
||||
- [x] Enforce an exact catalog for every tracked registry, including object-storage readiness.
|
||||
- [ ] Resolve every stable `required_test` ID exactly once and reject dangling mappings.
|
||||
- [ ] Replace the Java runbook stub allowlist with owned, issue-linked, expiring debt data.
|
||||
- [ ] Clarify tracked registry ownership and private-wiki provenance.
|
||||
- [x] Run schema, object-storage readiness, runbook, app-bootstrap, and root checks. The checks
|
||||
exercise the mechanically enforceable catalog/containment rules; the three semantic migrations
|
||||
above remain explicitly blocked on project-owner evidence.
|
||||
|
||||
### Batch 5-A evidence — exact tracked registry catalog (2026-08-02)
|
||||
|
||||
The owner catalog now enumerates exactly eight regular, non-symlink direct children: seven
|
||||
universal contract registries plus the specialized object-storage readiness registry. The initial
|
||||
focused RED failed compilation on the deliberately absent `RegistryGovernanceCatalog` (13 symbol
|
||||
errors). A second exact-version mutation RED proved that numeric coercion admitted
|
||||
`schema_version: 1.5`; the implementation now requires the integer value `1`. Strict SnakeYAML
|
||||
safe construction disables duplicate keys and aliases, enforces exact root keys, a non-empty list
|
||||
of map rows, non-blank unique identities, the existing universal row policy, and the specialized
|
||||
owner delegation/provenance policy. Missing, unknown, non-regular, symlinked, malformed, duplicate,
|
||||
false-provenance, block-scalar spoofing, reordered-header, and fabricated-branch-header fixtures
|
||||
fail closed.
|
||||
|
||||
Gradle declares `docs/registries` as a relative-path-sensitive `:app-bootstrap:test` directory
|
||||
input. The object-storage owner declares its canonical readiness YAML as a relative-path-sensitive
|
||||
file input and passes its absolute path through `objectstorage.readiness.registry`; its leaf test no
|
||||
longer searches parent directories. The tracked specialized registry header is exactly four
|
||||
ordered leading comment lines containing only the factual repository and semantic owner Gradle
|
||||
paths and test FQCNs.
|
||||
|
||||
Fresh verification passed:
|
||||
|
||||
- `./gradlew :app-bootstrap:test --tests
|
||||
dev.caskeleton.bootstrap.contract.ContractRegistrySchemaGovernanceTest --console=plain`
|
||||
- `./gradlew :adapter:outbound:objectstorage:test --tests
|
||||
dev.caskeleton.adapter.outbound.objectstorage.readiness.ObjectStorageReadinessRegistryTest
|
||||
--console=plain`
|
||||
- `./gradlew :app-bootstrap:test --console=plain` (38 tasks; 2 executed)
|
||||
- `./gradlew :app-bootstrap:check --console=plain` (77 tasks; 21 executed)
|
||||
- `./gradlew :app-bootstrap:spotlessJavaCheck
|
||||
:adapter:outbound:objectstorage:spotlessJavaCheck --console=plain`
|
||||
- `git diff --check`, an exact direct-child regular-file audit, the owner-path `jq` audit, and
|
||||
`yq eval 'true' docs/registries/*.yaml` (eight parsed documents)
|
||||
|
||||
### Batch 5-C partial containment evidence — legacy runbook stub debt (2026-08-02)
|
||||
|
||||
This is bounded containment, not completion of the owned, issue-linked, expiring debt-ledger item
|
||||
above. The Java set is now named `LEGACY_STUB_DEBT`, contains exactly the 43 current
|
||||
`status: stub` runbooks, and is checked bidirectionally against canonical tracked runbook files.
|
||||
The stale `migration-failed.md` entry was removed because that runbook is already active. Active,
|
||||
missing, template, and newly introduced stub drift now fail the same exact-set contract. Messages
|
||||
and the runbook template forbid adding new legacy allowlist entries and direct maintainers to
|
||||
complete the runbook or adopt the future governed ledger.
|
||||
|
||||
The focused RED failed only because `migration-failed.md` was an unexpected legacy-debt element.
|
||||
After the containment change, the focused Runbook contract passed with 6 tests, zero failures, and
|
||||
zero skips. Fresh verification also passed `:app-bootstrap:spotlessJavaCheck` and
|
||||
`:app-bootstrap:check` (77 tasks; 18 executed, 59 up-to-date). Owner, issue, start/sunset,
|
||||
expiry enforcement, and the private-wiki provenance migration remain deliberately incomplete and
|
||||
the corresponding Batch 5 checkboxes remain open.
|
||||
|
||||
### Batch 5-B/C unresolved semantic migrations audit (2026-08-02)
|
||||
|
||||
These items are intentionally not marked complete. The seven universal registries contain 324
|
||||
non-reference `required_test` occurrences and 216 unique IDs. There is no tracked selector
|
||||
catalog, no Gradle declaration containing those IDs, and no ID that can currently be proven to
|
||||
resolve to one exact module/task/class/method selector. Exact Java test-source literals cover only
|
||||
17 IDs (45 occurrences, 40 in comments/Javadocs); 199 IDs have no exact source literal. Creating
|
||||
216 selectors from namespaces or historical branch labels would manufacture execution evidence,
|
||||
so the exact-linkage gate requires semantic owner confirmation or new tests before it can be
|
||||
enabled.
|
||||
|
||||
The runbook corpus contains 43 stub documents, all with response owner `oncall` but no accountable
|
||||
debt owner, real issue, approved expiry, or bounded debt window. The seven legacy registries contain
|
||||
30 distinct `owner_branch` labels, none resolving to a current local/remote Git ref, while their
|
||||
private-wiki paths are absent from a fresh clone. The repository files are now protected as the
|
||||
tracked artifacts, but current owner IDs, historical-label migration, CODEOWNERS identities,
|
||||
runbook expiry dates, the `INTERNAL_ERROR` reverse-link decision, and the four umbrella-runbook
|
||||
retention decisions require real project-owner input. Placeholder owners, issues, selectors, and
|
||||
sunsets were not added to make the checks pass.
|
||||
|
||||
## Batch 6 — bounded P2 cleanup
|
||||
|
||||
- [x] Extend link-check triggers and scan scope to module README/CLAUDE documents.
|
||||
- [x] Make Poster migration gate labels version-neutral while preserving externally stable job IDs.
|
||||
- [x] Replace fixed HTTP timeout sleeps with deterministic latch-controlled handlers.
|
||||
- [x] Separate sample-off compile evidence from its minimal runtime proof if exact required tests can
|
||||
be established without weakening coverage.
|
||||
- [x] Run focused docs, CI, HTTP client, sample-off, and wrapper checks.
|
||||
|
||||
Batch 6 link/Poster evidence: test-first changes made the two focused app-bootstrap contracts fail
|
||||
only for the absent module documentation scope and the legacy Poster V7 internal gate ID. The same
|
||||
contracts then passed with exact pull/push/lychee scope, all 27 gate IDs, and the stable external
|
||||
`poster-image-v7-migration` workflow job plus `posterImageMigrationTest` task mapping. The full
|
||||
`DeveloperExperienceContractTest` and `ConditionalTransportQualificationContractTest` classes
|
||||
passed, `posterImageMigrationTest` produced 4 tests with zero skips, and both the 27-entry gate
|
||||
validator and Gradle wrapper verifier passed. The complete sorted six-workflow SHA-256 lock was
|
||||
refreshed after review; app-bootstrap Java and sample-portfolio Spotless checks also passed. An
|
||||
independent Batch 6 link/Poster read-only review found no Critical, Important, or Minor issues.
|
||||
|
||||
Batch 6 HTTP evidence: the focused synchronization contract first failed on exactly five fixed
|
||||
sleeps across `OutboundHttpClientTest` (one), `OutboundHttpClientDeadlineTest` (one), and
|
||||
`OutboundCallExecutorTest` (three). The HTTP handlers now signal `requestStarted`, await a bounded
|
||||
`releaseResponse` latch, and are released in the caller's `finally` after the timeout result and
|
||||
classification assertions. Executor workers now block on a bounded latch interruption point, with
|
||||
the existing started/interrupted evidence and caller cleanup preserved. The four focused classes
|
||||
passed 25 tests with zero failures, errors, or skips. A 3-second read-timeout mutation failed when
|
||||
the handler's 1-second HTTP 204 fallback completed successfully, proving that the test cannot pass
|
||||
via the separate 5-second logical deadline. The full owner `test` passed, and
|
||||
`:adapter:outbound:httpclient:check` passed 29 tasks (16 executed, 13 up-to-date), including
|
||||
Spotless, Checkstyle, SpotBugs, architecture dependencies, and environment-key verification. No
|
||||
production source changed. Independent re-review found no remaining Critical, Important, or Minor
|
||||
issues and found no cleanup leak or deadlock race.
|
||||
|
||||
Batch 6 sample-off evidence: the focused build contract first failed because the dedicated source
|
||||
directory, compile lifecycle task, strict registration, and required FQCN did not exist. The
|
||||
`sampleOffTest` source set now compiles all 204 ordinary test sources plus the dedicated contract
|
||||
without `sample-portfolio`, while `sampleOffCompile` exposes that complete compile proof separately.
|
||||
The externally stable `sampleOffTest` task is registered through the shared strict qualification
|
||||
convention and executes only `SampleOffClasspathContractTest`; fresh XML reported exactly 1 test,
|
||||
0 skipped, 0 failures, and 0 errors. The existing eight strict-convention functional contracts
|
||||
passed, including missing-class, no-discovery, skip, and stale-evidence fail-closed cases. The
|
||||
focused build contract, `sampleOffCompile`, gate-matrix validator, wrapper verifier, dependency-lock
|
||||
verification, Spotless, and the full `:app-bootstrap:check` also passed; the full check completed 78
|
||||
tasks (23 executed, 55 up-to-date). This is focused/owner evidence; the repository-wide Batch 6
|
||||
aggregate is recorded below.
|
||||
|
||||
Batch 6 repository evidence (2026-08-02): the real gate-matrix validator passed all 27 entries
|
||||
(26 locally verified and the protected AWS lane explicitly delegated-pending), the Gradle-wrapper
|
||||
contract passed, `bash -n .github/scripts/verify-gate-matrix.sh` passed, all eight tracked registry
|
||||
YAML documents parsed, the Redis Draft 2020-12 schema parsed as JSON, and `git diff --check`
|
||||
reported no whitespace errors. The first repository `check` exposed a 503 in the first
|
||||
`JwtJwksSecurityFilterIntegrationTest` request while static-analysis workers were running. The
|
||||
single test passed in isolation, identifying a test-fixture scheduling race rather than a JWT
|
||||
classification mismatch. The embedded OIDC server now owns a dedicated single daemon executor and
|
||||
shuts it down in `close()`; the full eight-test security-boundary lane plus Checkstyle and Spotless
|
||||
passed, and a fresh repository `check` subsequently passed with the same boundary lane included.
|
||||
|
||||
## Final verification and capture
|
||||
|
||||
- [x] Run full Gradle tests/checks and all repository validators.
|
||||
- [x] Request an independent P2 code review, resolve actionable findings, and record semantic
|
||||
blockers separately.
|
||||
- [x] Update the LLM Wiki branch note and any honest derived raw documents.
|
||||
|
||||
Fresh aggregate evidence (2026-08-02):
|
||||
|
||||
- `./gradlew test --no-daemon --console=plain` — successful in 4m 24s (86 tasks).
|
||||
- `./gradlew check --no-daemon --console=plain` — first run failed only on the OIDC test-fixture
|
||||
race above; after the bounded fixture correction, successful in 4m 35s (260 tasks).
|
||||
- Final post-review `./gradlew check --no-daemon --console=plain` — successful in 10m 33s
|
||||
(260 tasks; 76 executed, 184 up-to-date). It regenerated the SampleRemoval result after the
|
||||
source edit: 5 tests, zero skipped/failures/errors.
|
||||
- `./gradlew verifyCleanArchitectureDependencies verifyRuntimeModuleMembership
|
||||
verifyDependencyLocks verifyPublicPathSnapshot verifyEnvKeys --no-daemon --console=plain` —
|
||||
successful (23 tasks); all 19 leaf locks passed and two runtime compositions matched the registry.
|
||||
- Real gate-matrix, wrapper, shell syntax, Redis JSON, registry YAML, and diff validators — all
|
||||
successful; the protected AWS qualification remains explicitly delegated to its environment.
|
||||
- Final `verifyDependencyLocks` rerun — successful in 24s with all 19 leaf tasks executed. The
|
||||
tracked-file assumption audit now reports only four Docker/Testcontainers integration
|
||||
assumptions; no registry or repository-contract assumption remains.
|
||||
|
||||
LLM Wiki capture evidence (2026-08-02): `raw/branch-notes/main.md` records the integrated P1/P2
|
||||
implementation, decisions, validation commands, failures, evidence grades, and unresolved semantic
|
||||
migrations. It links bidirectionally to one resolved error note, one interview-prep note, and one
|
||||
blog-topic note. The vault's targeted structure lint passed all three derived documents. The branch
|
||||
note passed its content, frontmatter, required-section, and wikilink checks but retained one explicit
|
||||
`NAMING_VIOLATION`: repository policy requires `<branch-name>.md` (`main.md`) while the vault naming
|
||||
rule permits only `feature|fix|chore|experiment-` branch-note prefixes. Neither policy was silently
|
||||
weakened; the exact conflict is the recorded capture-validation blocker.
|
||||
|
||||
Independent aggregate review evidence (2026-08-02): the first pass reported zero critical,
|
||||
three important, and two minor findings. Wiki capture closed the capture-pending finding; the two
|
||||
remaining important items were reclassified as the three project-semantic blockers already kept
|
||||
open in Batch 5. The two minor code findings were corrected with an exact test-fixture-only
|
||||
GraphQL SpotBugs exclusion and registry-derived scanning of all 18 production leaves in
|
||||
`SampleRemovalSmokeContractTest`. A follow-up audit also found and removed the last tracked-file
|
||||
assumption/upward-directory search in `PortfolioErrorCodeRegistryMappingTest`, replacing it with a
|
||||
canonical repository-root property, relative Gradle input, and missing-root/symlink-escape
|
||||
fail-closed checks. The re-review found no new code defect; its only completion-evidence concern
|
||||
was a stale SampleRemoval XML, addressed by the final repository `check` after these corrections.
|
||||
The reviewer retained only the Wiki naming-policy disclosure and this Batch 5 checkbox wording as
|
||||
minor documentation findings; both are now explicit here and in the branch note.
|
||||
@@ -0,0 +1,72 @@
|
||||
# Redis Session HTTP Boundary Implementation Plan
|
||||
|
||||
> **Execution:** Follow test-driven development and request an independent read-only review before
|
||||
> advancing to the remaining P1 work.
|
||||
|
||||
**Goal:** Prove browser-session security persists and fails closed across the real Spring Session ↔
|
||||
Redis composition, without silent skips.
|
||||
|
||||
**Architecture:** The app-bootstrap composition test reuses its existing Redis test source set and
|
||||
dependencies. It assembles inbound-web and cache-redis without adding a forbidden leaf-to-leaf edge.
|
||||
|
||||
**Tech Stack:** Java 21, Spring Boot 4.0.0, Spring Security 7, Spring Session 4, Testcontainers 2,
|
||||
Redis 7.4 digest-pinned image, MockMvc, Gradle 9.
|
||||
|
||||
### Task 1: Explicit Docker No-Skip Gate
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/app-bootstrap/build.gradle`
|
||||
|
||||
- [x] Exclude `redis-session-http` from ordinary `redisCompositionTest`.
|
||||
- [x] Register `redisSessionHttpIntegrationTest` over the same source output/classpath with tag
|
||||
inclusion, no-discovery failure, no-skip root-suite guard, UTC, rerun, and image-registry property.
|
||||
- [x] Keep the Docker task outside ordinary `check`; reuse Spring Session 4.0.0 and lock only the
|
||||
added `redisCompositionTestCompileClasspath` configuration.
|
||||
|
||||
### Task 2: Real Session HTTP RED Contract
|
||||
|
||||
**Files:**
|
||||
- Create: `src/app-bootstrap/src/redisCompositionTest/java/dev/caskeleton/adapter/outbound/cache/redis/RedisSessionHttpBoundaryIntegrationTest.java`
|
||||
|
||||
- [x] Load and validate the approved digest-pinned Redis image; explicitly start the container.
|
||||
- [x] Generate ephemeral TLS/ACL/password/HMAC material and assemble canonical SESSION-role
|
||||
configuration with full hostname verification and explicit trust.
|
||||
- [x] Cross CSRF, login, Spring Session filter, primitive snapshot, and hardened cookie creation.
|
||||
- [x] Close context A and prove context B restores the authenticated principal from Redis.
|
||||
- [x] Prove logout/tombstone rejects the old cookie and a stale repository save.
|
||||
- [x] Stop Redis during lookup and prove fail-closed controller behavior with fixed diagnostics.
|
||||
- [x] Record and resolve RED composition mismatches: response-commit session creation and framework
|
||||
request-cache serialization.
|
||||
|
||||
### Task 3: CI Release Gate
|
||||
|
||||
**Files:**
|
||||
- Modify: `.github/workflows/ci-quality-gates.yml`
|
||||
|
||||
- [x] Add `:app-bootstrap:redisSessionHttpIntegrationTest` to the existing `redis-standalone` job.
|
||||
- [x] Keep the existing required gate identity and matrix dependency unchanged.
|
||||
|
||||
### Task 4: Verification and Review
|
||||
|
||||
- [x] Run the explicit HTTP task and existing app-bootstrap Redis composition task.
|
||||
- [x] Run the selected cache-redis session capability lane, dependency locks, env keys, architecture,
|
||||
public-path snapshot, static analysis, and `git diff --check`.
|
||||
- [x] Request an independent read-only review and resolve all Critical/Important findings.
|
||||
|
||||
### Verification Evidence
|
||||
|
||||
- `:app-bootstrap:redisSessionHttpIntegrationTest`: 1 test, 0 skipped, GREEN.
|
||||
- `:adapter:outbound:cache-redis:redisSessionCapabilityTest`: GREEN with sanitized evidence.
|
||||
- `:adapter:inbound:web:check`: unit/contract/static analysis and 13 no-skip JWT/CORS boundary
|
||||
tests GREEN.
|
||||
- `:app-bootstrap:check :app-bootstrap:redisCompositionTest`: 640 bootstrap tests (6 pre-existing
|
||||
conditional Docker skips in the ordinary suite, not used as this gate's evidence), TestKit
|
||||
contracts, 14 Redis composition tests, Checkstyle, SpotBugs, and Spotless GREEN.
|
||||
- `verifyDependencyLocks verifyEnvKeys verifyCleanArchitectureDependencies
|
||||
verifyPublicPathSnapshot`: GREEN for all 19 registered leaves.
|
||||
- Review RED: final context reconciliation could retain the authentication saved at response commit;
|
||||
host TLS/ACL material permissions were too broad; the CI task lacked a semantic workflow assertion.
|
||||
- Review fixes: authoritative final empty/replacement context tests went RED then GREEN, async start
|
||||
defers commit-hook persistence, host material is `0700`/`0600` and copied selectively into the
|
||||
fixture, and the blocking Redis job is now asserted directly.
|
||||
- Independent re-review: Critical 0, Important 0, Minor 0; batch READY.
|
||||
@@ -0,0 +1,79 @@
|
||||
# Verification Purity Refactoring Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Make stale-JAR and public-path verification strictly read-only while preserving explicit cleanup/update workflows.
|
||||
|
||||
**Architecture:** Extract only these two root Gradle concerns into applied scripts so the production tasks can be exercised by isolated Gradle TestKit fixtures. Verification tasks only observe and fail; `clean*` and `update*` tasks are the sole writers.
|
||||
|
||||
**Tech Stack:** Java 21, Gradle 9.0.0 Groovy DSL, Gradle TestKit, JUnit 5, AssertJ.
|
||||
|
||||
## Global Constraints
|
||||
|
||||
- Preserve all existing P0 changes in the dirty worktree.
|
||||
- Preserve the 19-leaf registry and every production project dependency edge.
|
||||
- Normal archive tasks and every `verify*` task must be read-only.
|
||||
- `updatePublicPathSnapshot` requires `-PapprovePublicPathChange`.
|
||||
- Agents do not stage, commit, amend, or push.
|
||||
|
||||
---
|
||||
|
||||
### Task 1: Add Functional RED Contracts
|
||||
|
||||
**Files:**
|
||||
- Create: `src/app-bootstrap/src/functionalTest/java/dev/caskeleton/bootstrap/contract/BuildVerificationPurityContractTest.java`
|
||||
- Modify: `src/app-bootstrap/build.gradle`
|
||||
- Modify: `src/app-bootstrap/gradle.lockfile`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: production scripts at `src/gradle/archive-hygiene.gradle` and `src/gradle/public-path-snapshot.gradle`.
|
||||
- Produces: functional tests that execute real Gradle tasks and assert filesystem side effects.
|
||||
|
||||
- [x] Add an isolated `functionalTest` source set/task and its `functionalTestImplementation gradleTestKit()` dependency so Gradle's SLF4J provider cannot pollute ordinary tests.
|
||||
- [x] Add a nested temporary archive fixture with root + `family:module` projects. Apply the production archive script, pre-create a stale traceable JAR and a nonmatching JAR, run `:family:module:jar`, `verifyNoStaleTraceableJars`, and `cleanStaleTraceableJars`, and assert exact preservation/deletion plus the full task-path diagnostic.
|
||||
- [x] Add a temporary public-path fixture. Apply the production public-path script and assert missing/drifted snapshots are not written, the verifier rejects `-PapprovePublicPathChange`, and only the approved updater writes canonical content.
|
||||
- [x] Confirm the contracts RED before the two production scripts exist. The first RED run used the ordinary test source set; after it exposed Gradle TestKit's SLF4J provider collision, move the contract and TestKit dependency to isolated `functionalTest` configurations and add their strict lock state.
|
||||
|
||||
### Task 2: Separate Archive Verification from Cleanup
|
||||
|
||||
**Files:**
|
||||
- Create: `src/gradle/archive-hygiene.gradle`
|
||||
- Modify: `src/build.gradle`
|
||||
|
||||
**Interfaces:**
|
||||
- Produces: root tasks `verifyNoStaleTraceableJars` and `cleanStaleTraceableJars` with no dependency between them.
|
||||
|
||||
- [x] Move traceable archive matching/discovery and both root tasks into the applied script.
|
||||
- [x] Remove the stale-deleting `doFirst` from every `Jar` task while retaining manifest metadata.
|
||||
- [x] Apply the script before leaf `check` dependencies are configured; task actions discover leaf JAR tasks at execution time.
|
||||
- [x] Explicitly declare both archive tasks configuration-cache incompatible because their actions inspect subproject task models.
|
||||
- [x] Run the focused functional test and confirm archive cases are GREEN.
|
||||
|
||||
### Task 3: Separate Public-Path Verification from Update
|
||||
|
||||
**Files:**
|
||||
- Create: `src/gradle/public-path-snapshot.gradle`
|
||||
- Modify: `src/build.gradle`
|
||||
- Modify: `src/README.md`
|
||||
- Modify: `docs/security/public-paths-snapshot.txt`
|
||||
|
||||
**Interfaces:**
|
||||
- Produces: read-only `verifyPublicPathSnapshot` and explicitly mutating `updatePublicPathSnapshot`.
|
||||
|
||||
- [x] Centralize canonical snapshot rendering in the script.
|
||||
- [x] Make verification fail on missing env, missing snapshot, drift, and use of the approval property without any writes.
|
||||
- [x] Make update require `-PapprovePublicPathChange`, create the parent directory, and write canonical content.
|
||||
- [x] Replace documentation and snapshot instructions with `updatePublicPathSnapshot -PapprovePublicPathChange`.
|
||||
- [x] Run the focused functional test and confirm all public-path cases are GREEN.
|
||||
|
||||
### Task 4: Focused and Architecture Verification
|
||||
|
||||
**Files:** none beyond Tasks 1-3.
|
||||
|
||||
- [x] Run `./gradlew :app-bootstrap:functionalTest --tests '*BuildVerificationPurityContractTest' --console=plain`.
|
||||
- [x] Run `./gradlew :app-bootstrap:test --console=plain`; 640 ordinary tests pass after TestKit isolation (6 skipped), alongside the 9 functional contracts.
|
||||
- [x] Run `./gradlew :app-bootstrap:verifyDependencyLocks --console=plain`.
|
||||
- [x] Run `./gradlew :app-bootstrap:spotlessJavaCheck :app-bootstrap:checkstyleFunctionalTest :app-bootstrap:spotbugsFunctionalTest --console=plain`.
|
||||
- [x] Run `./gradlew verifyNoStaleTraceableJars verifyPublicPathSnapshot --console=plain` and confirm both are read-only and pass on the current baseline.
|
||||
- [x] Run `./gradlew verifyCleanArchitectureDependencies --console=plain`.
|
||||
- [x] Run `git diff --check` and record `git status --short` without staging or committing.
|
||||
@@ -0,0 +1,387 @@
|
||||
# Warning-Zero Build Refactoring Implementation Plan
|
||||
|
||||
> **For Codex:** REQUIRED SUB-SKILLS: use `superpowers:subagent-driven-development` for the
|
||||
> independent owner-leaf batches, `superpowers:test-driven-development` for behavior changes,
|
||||
> `superpowers:systematic-debugging` for any failure, and
|
||||
> `superpowers:verification-before-completion` before reporting success.
|
||||
|
||||
**Goal:** Remove the audited compiler/static-analysis/test-output warning debt, preserve the approved
|
||||
legacy compatibility boundaries, and make the blocking build fail on any future warning.
|
||||
|
||||
**Architecture:** Fix behavior in the owning leaf, preserve identity/framework/compatibility seams
|
||||
with the narrowest justified suppressions, migrate deprecated provider APIs in their outbound leaf,
|
||||
then enable root Gradle/CI gates only after all focused tasks are clean. No dependency edge or runtime
|
||||
membership changes are permitted. The 19-leaf registry remains the dependency SSOT.
|
||||
|
||||
**Tech Stack:** Java 21, Spring Boot 4.0.0, Gradle multi-project build, JUnit 5, AssertJ, Mockito,
|
||||
Error Prone, Checkstyle, SpotBugs, Jackson 3.0.2, Lettuce 6.8.1, AWS SDK v2, Testcontainers 2.
|
||||
|
||||
**Approved design:**
|
||||
`docs/superpowers/specs/2026-08-02-warning-zero-build-design.md`
|
||||
|
||||
**Repository constraints:** The worktree already contains user/P0/P1/P2 changes. Preserve them,
|
||||
never reset or rewrite unrelated files, and do not stage, commit, amend, or push. Agent tasks must
|
||||
edit only their assigned files and report overlaps before proceeding.
|
||||
|
||||
## Task 1: Freeze warning evidence and add behavior regressions
|
||||
|
||||
**Owner leaves:** `adapter-inbound-web`, `adapter-outbound-notification`, `sample-portfolio`,
|
||||
`app-bootstrap`
|
||||
|
||||
**Files:**
|
||||
|
||||
- Add: `src/adapter/inbound/web/src/test/java/dev/caskeleton/adapter/inbound/web/auth/JwtToAuthenticatedPrincipalConverterTest.java`
|
||||
- Modify: `src/adapter/inbound/web/src/test/java/dev/caskeleton/adapter/inbound/web/conditional/ETagsTest.java`
|
||||
- Modify: `src/adapter/outbound/notification/src/test/java/dev/caskeleton/adapter/outbound/notification/core/RoutingNotifierTest.java`
|
||||
- Modify: `src/sample-portfolio/src/test/java/dev/caskeleton/sample/portfolio/adapter/outbound/repostats/RepoStatsAclMapperTest.java`
|
||||
- Modify: `src/app-bootstrap/src/test/java/dev/caskeleton/bootstrap/async/AsyncGracefulShutdownBehaviorTest.java`
|
||||
|
||||
**Steps:**
|
||||
|
||||
1. Add Turkish-default-locale regressions for JWT role uppercasing, notification route-key
|
||||
lowercasing, and repository ACL lowercasing. Snapshot `Locale.getDefault()`, set
|
||||
`Locale.forLanguageTag("tr-TR")`, and restore it in `finally`.
|
||||
2. Add RED ETag cases for `"opaque,tag"`, weak `W/"opaque,tag"` inside a mixed list, malformed
|
||||
unclosed quotes, wildcard, blank, stale, and ordinary multiple values.
|
||||
3. Add a RED async case proving an exception raised in the submitted action reaches the test through
|
||||
`Future.get()`.
|
||||
4. Run the exact focused tests. Confirm the new locale/ETag cases fail for the intended reason; the
|
||||
async change uses the existing `FutureReturnValueIgnored` compile diagnostic as its RED contract:
|
||||
|
||||
```bash
|
||||
./gradlew :adapter:inbound:web:test --tests '*JwtToAuthenticatedPrincipalConverterTest' --tests '*ETag*' --console=plain
|
||||
./gradlew :adapter:outbound:notification:test --tests '*RoutingNotifier*' --console=plain
|
||||
./gradlew :sample-portfolio:test --tests '*RepoStatsAclMapper*' --console=plain
|
||||
./gradlew :app-bootstrap:test --tests '*AsyncGracefulShutdownBehaviorTest' --console=plain
|
||||
```
|
||||
|
||||
5. Do not change production code in this task; retain the behavior-test failures and compile warning
|
||||
as the TDD/static-analysis baseline.
|
||||
|
||||
## Task 2: Correct locale, ETag, async, cleanup, and host-default behavior
|
||||
|
||||
**Owner leaves:** `adapter-inbound-web`, `adapter-outbound-notification`, `sample-portfolio`,
|
||||
`app-bootstrap`, `application-core`, `shared-contract`, `adapter-outbound-fileserver`,
|
||||
`adapter-outbound-httpclient`, `adapter-outbound-identifier`
|
||||
|
||||
**Production files:**
|
||||
|
||||
- Modify: `src/adapter/inbound/web/src/main/java/dev/caskeleton/adapter/inbound/web/auth/JwtToAuthenticatedPrincipalConverter.java`
|
||||
- Modify: `src/adapter/inbound/web/src/main/java/dev/caskeleton/adapter/inbound/web/conditional/ETags.java`
|
||||
- Modify: `src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/core/RoutingNotifier.java`
|
||||
- Modify: `src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/adapter/outbound/repostats/RepoStatsAclMapper.java`
|
||||
|
||||
**Test/mechanical files:**
|
||||
|
||||
- Modify the nine audited implicit-charset sites in `CursorCodecTest`,
|
||||
`RedisTrustMaterialProviderTest`, `OutboundHttpClientTest`,
|
||||
`HmacUserPrincipalPseudonymizerTest`, `StreamingResponseBodyAllowedFixture`, and
|
||||
`IdempotencyExecutorTest`.
|
||||
- Modify the remaining audited test-only locale sites in `JwtDecoderConfigTest`,
|
||||
`OutboundHttpClientTest`, `WorkLogReservedIntegrationEventMapperJsonTest`, `WorkLogIdTest`, and
|
||||
`TraceParentTest`.
|
||||
- Modify: `src/sample-portfolio/src/test/java/dev/caskeleton/sample/portfolio/domain/worklog/WorkLogTest.java`
|
||||
- Modify the four outbox cleanup classes under
|
||||
`src/app-bootstrap/src/test/java/dev/caskeleton/bootstrap/integration/outbox/`.
|
||||
- Modify: `src/adapter/outbound/fileserver/src/test/java/dev/caskeleton/adapter/outbound/fileserver/FilesystemCsvExportAdapterTest.java`
|
||||
|
||||
**Steps:**
|
||||
|
||||
1. Use `Locale.ROOT` at the three production identifier sites and at audited test comparisons.
|
||||
2. Replace `ETags` delimiter splitting with a quote-aware scanner. Split only on commas outside
|
||||
quoted opaque tags; malformed quoting yields no match. Keep wildcard and weak-tag semantics.
|
||||
3. Retain and observe the async `Future<?>`; unwrap `ExecutionException` only as required by the
|
||||
test's existing assertion contract.
|
||||
4. Replace empty cleanup catches with propagation or `IllegalStateException`/`UncheckedIOException`
|
||||
preserving the original cause.
|
||||
5. Replace implicit charset calls with `StandardCharsets.UTF_8`; replace `LocalDate.now()` test data
|
||||
with the fixed intended date or an explicit UTC clock.
|
||||
6. Convert byte-identical readability literals to text blocks and verify the exact expected strings.
|
||||
7. Run the focused tests from Task 1 and the affected owner test suites:
|
||||
|
||||
```bash
|
||||
./gradlew :application-core:test :shared-contract:test :adapter:inbound:web:test \
|
||||
:adapter:outbound:notification:test :adapter:outbound:fileserver:test \
|
||||
:adapter:outbound:httpclient:test :adapter:outbound:identifier:test \
|
||||
:sample-portfolio:test :app-bootstrap:test --console=plain
|
||||
```
|
||||
|
||||
## Task 3: Preserve Redis invariants and migrate Lettuce calls
|
||||
|
||||
**Owner leaf:** `adapter-outbound-cache-redis`
|
||||
|
||||
**Files:**
|
||||
|
||||
- Modify: `src/adapter/outbound/cache-redis/src/main/java/dev/caskeleton/adapter/outbound/cache/redis/RedisPrimitiveInvocation.java`
|
||||
- Modify: `src/adapter/outbound/cache-redis/src/main/java/dev/caskeleton/adapter/outbound/cache/redis/RedisTopologyCommandRuntime.java`
|
||||
- Modify: `src/adapter/outbound/cache-redis/src/main/java/dev/caskeleton/adapter/outbound/cache/redis/RedisVersionedSession.java`
|
||||
- Modify: `src/adapter/outbound/cache-redis/src/main/java/dev/caskeleton/adapter/outbound/cache/redis/VersionedRedisSessionStore.java`
|
||||
- Add: `src/adapter/outbound/cache-redis/src/test/java/dev/caskeleton/adapter/outbound/cache/redis/RedisPrimitiveInvocationTest.java`
|
||||
- Add: `src/adapter/outbound/cache-redis/src/test/java/dev/caskeleton/adapter/outbound/cache/redis/VersionedRedisSessionStoreTest.java`
|
||||
- Modify: `src/adapter/outbound/cache-redis/src/test/java/dev/caskeleton/adapter/outbound/cache/redis/LettuceRedisRuntimeServiceTest.java`
|
||||
- Modify: `src/adapter/outbound/cache-redis/src/test/java/dev/caskeleton/adapter/outbound/cache/redis/RedisVersionedSessionRepositoryTest.java`
|
||||
- Modify: `src/adapter/outbound/cache-redis/src/test/java/dev/caskeleton/adapter/outbound/cache/redis/RedisPrimitiveRuntimeServiceTest.java`
|
||||
|
||||
**Steps:**
|
||||
|
||||
1. Add characterization regressions proving a value-equal descriptor from a different catalog is
|
||||
rejected and the four session array records copy constructor inputs and accessor outputs. These
|
||||
should pass before implementation because they justify preserving the invariants; the compiler
|
||||
warnings are the RED executable contract for the suppression/migration work.
|
||||
2. Keep descriptor reference equality and add constructor-only
|
||||
`@SuppressWarnings("ReferenceEquality")` with an invariant rationale.
|
||||
3. Qualify every ambiguous nested `ExpectedKind` reference with its enclosing record.
|
||||
4. Keep Spring Session's `<T> T getAttribute(String)` signature and add method-only
|
||||
`TypeParameterUnusedInFormals` suppression.
|
||||
5. Preserve defensive copying for the four `VersionedRedisSessionStore` array records; apply exact
|
||||
`ArrayRecordComponent` suppressions to those records and the private test fake only.
|
||||
6. Convert canonical finite score strings to `BigDecimal`, build inclusive Lettuce `Range` values,
|
||||
and use typed `zcount` and `zrangebyscoreWithScores(..., Limit.create(...))` overloads. Extend the
|
||||
runtime proxy test to prove both overloads and their offset/count arguments.
|
||||
7. Replace one-shot `new SecureRandom()` with one static final instance.
|
||||
8. Run:
|
||||
|
||||
```bash
|
||||
./gradlew :adapter:outbound:cache-redis:test --console=plain
|
||||
./gradlew :adapter:outbound:cache-redis:compileJava \
|
||||
:adapter:outbound:cache-redis:compileTestJava --rerun-tasks --console=plain
|
||||
./gradlew :adapter:outbound:cache-redis:spotbugsTest --rerun-tasks --console=plain
|
||||
```
|
||||
|
||||
## Task 4: Preserve HTTP retry and notification ciphertext invariants
|
||||
|
||||
**Owner leaves:** `adapter-outbound-httpclient`, `adapter-outbound-persistence-jpa`
|
||||
|
||||
**Files:**
|
||||
|
||||
- Modify: `src/adapter/outbound/httpclient/src/main/java/dev/caskeleton/adapter/outbound/httpclient/OutboundRetryPolicy.java`
|
||||
- Modify: `src/adapter/outbound/httpclient/src/test/groovy/dev/caskeleton/adapter/outbound/httpclient/OutboundRetryPolicySpec.groovy`
|
||||
- Modify: `src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/notification/crypto/NotificationCiphertext.java`
|
||||
- Modify: `src/adapter/outbound/persistence-jpa/src/test/java/dev/caskeleton/adapter/outbound/persistence/notification/crypto/NotificationPayloadCryptoTest.java`
|
||||
|
||||
**Steps:**
|
||||
|
||||
1. Add a characterization test using two `OutboundRetryPolicy` instances on one thread: policy A
|
||||
context must not be visible to policy B, and `endCall()` must clear the owning context. It should
|
||||
pass before implementation and justifies preserving the instance field; the compile warning is
|
||||
the RED contract.
|
||||
2. Keep the instance `ThreadLocal`; add field-only `ThreadLocalUsage` suppression with the isolation
|
||||
reason.
|
||||
3. Add/strengthen tests proving `NotificationCiphertext` clones nonce/ciphertext inputs and
|
||||
accessors, compares arrays by content, hashes consistently, and never exposes bytes in
|
||||
`toString()`.
|
||||
4. Keep the record API and add exact record-level `ArrayRecordComponent` suppression.
|
||||
5. Run:
|
||||
|
||||
```bash
|
||||
./gradlew :adapter:outbound:httpclient:test --console=plain
|
||||
./gradlew :adapter:outbound:persistence-jpa:test --console=plain
|
||||
```
|
||||
|
||||
## Task 5: Migrate Jackson 3 messaging APIs
|
||||
|
||||
**Owner leaf:** `adapter-outbound-messaging`
|
||||
|
||||
**Files:**
|
||||
|
||||
- Modify: `src/adapter/outbound/messaging/src/main/java/dev/caskeleton/adapter/outbound/messaging/schema/LocalJsonSchemaRegistry.java`
|
||||
- Modify: `src/adapter/outbound/messaging/src/main/java/dev/caskeleton/adapter/outbound/messaging/envelope/DeterministicEnvelopeWriter.java`
|
||||
- Modify: `src/adapter/outbound/messaging/src/test/java/dev/caskeleton/adapter/outbound/messaging/envelope/LocalJsonSchemaRegistryTest.java`
|
||||
- Modify: `src/adapter/outbound/messaging/src/test/java/dev/caskeleton/adapter/outbound/messaging/envelope/JsonSchemaIntegrationEventEncoderTest.java`
|
||||
|
||||
**Steps:**
|
||||
|
||||
1. Extend existing tests to freeze text-node validation and canonical envelope bytes.
|
||||
2. Replace `isTextual()`/`textValue()` with `isString()`/`stringValue()`.
|
||||
3. Replace `createGenerator(output)` with
|
||||
`createGenerator(ObjectWriteContext.empty(), output, JsonEncoding.UTF8)`.
|
||||
4. Run:
|
||||
|
||||
```bash
|
||||
./gradlew :adapter:outbound:messaging:test --console=plain
|
||||
./gradlew :adapter:outbound:messaging:compileJava --rerun-tasks --console=plain
|
||||
```
|
||||
|
||||
## Task 6: Preserve legacy object storage and migrate provider APIs
|
||||
|
||||
**Owner leaves:** `application-core`, `adapter-outbound-objectstorage`, `sample-portfolio`,
|
||||
`app-bootstrap` architecture tests
|
||||
|
||||
**Files:**
|
||||
|
||||
- Modify: `src/application-core/src/main/java/dev/caskeleton/application/storage/ObjectStoragePort.java`
|
||||
- Modify the six Java files under
|
||||
`src/application-core/src/main/java/dev/caskeleton/application/storage/migration/`.
|
||||
- Modify: `src/adapter/outbound/objectstorage/src/main/java/dev/caskeleton/adapter/outbound/objectstorage/FilesystemObjectStorageAdapter.java`
|
||||
- Modify: `src/adapter/outbound/objectstorage/src/main/java/dev/caskeleton/adapter/outbound/objectstorage/S3ObjectStorageAdapter.java`
|
||||
- Modify: `src/adapter/outbound/objectstorage/src/main/java/dev/caskeleton/adapter/outbound/objectstorage/maintenance/LegacyObjectInspector.java`
|
||||
- Modify: `src/adapter/outbound/objectstorage/src/main/java/dev/caskeleton/adapter/outbound/objectstorage/maintenance/LegacyObjectAdoptionService.java`
|
||||
- Modify: `src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/application/poster/UploadPosterImageUseCase.java`
|
||||
- Modify: `src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/application/poster/migration/AdoptLegacyPosterImageUseCase.java`
|
||||
- Modify: `src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/bootstrap/objectstorage/PosterImageApiConfig.java`
|
||||
- Modify: `src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/adapter/inbound/web/controller/LegacyPosterImageController.java`
|
||||
- Modify: `src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/adapter/inbound/web/mapper/PosterWebMapper.java`
|
||||
- Modify: `src/application-core/src/test/java/dev/caskeleton/application/objectstorage/ObjectStorageArchitectureContractTest.java`
|
||||
- Modify: `src/adapter/outbound/objectstorage/src/main/java/dev/caskeleton/adapter/outbound/objectstorage/s3/S3AsyncClientFactory.java`
|
||||
- Modify: `src/adapter/outbound/objectstorage/src/objectStorageMinioFaultTest/java/dev/caskeleton/adapter/outbound/objectstorage/qualification/MinioManagedObjectFaultTest.java`
|
||||
- Modify: `src/adapter/outbound/objectstorage/build.gradle`
|
||||
- Modify: `src/adapter/outbound/objectstorage/gradle.lockfile` only if the toxiproxy dependency graph changes.
|
||||
- Modify audited URL, Mockito varargs, range parser, text-block, and legacy characterization tests.
|
||||
|
||||
**Steps:**
|
||||
|
||||
1. Add/retain lifecycle tests: `ObjectStoragePort`, `StoredObject`, and adapter-owned
|
||||
`ObjectStorageSettings` remain `forRemoval=true`; migration types remain deprecated but are no
|
||||
longer `forRemoval`.
|
||||
2. Change the six migration mechanism types plus `AdoptLegacyPosterImageUseCase` to plain
|
||||
`@Deprecated`. Add only exact `deprecation` suppressions at adoption implementation/configuration
|
||||
consumers.
|
||||
3. Add only the exact `removal` suppressions named by the design to legacy implementations,
|
||||
controller/mapper/wiring, characterization classes, and single receipt methods.
|
||||
4. Replace AWS `RetryPolicy`/old equal-jitter API with `StandardRetryStrategy`, half-jitter
|
||||
exponential backoff, exact max attempts, and `retryStrategy(...)`. Assert normal/throttling
|
||||
configuration in `S3AsyncClientFactoryTest`.
|
||||
5. Keep the existing `org.testcontainers:testcontainers-toxiproxy` dependency, switch to its
|
||||
Testcontainers 2 package, and use `ToxiproxyClient`/`Proxy` against an explicitly exposed proxy
|
||||
port. Preserve cut/restore MinIO semantics; update the leaf lock only if resolution actually
|
||||
changes.
|
||||
6. Replace `new URL(String)` with `URI.create(...).toURL()`.
|
||||
7. Replace Mockito's two-value varargs `thenReturn` with two chained single-value stubs.
|
||||
8. Replace test-only range splitting with an asserted single-hyphen boundary; keep fingerprint
|
||||
literal bytes identical when converting to a text block.
|
||||
9. Run:
|
||||
|
||||
```bash
|
||||
./gradlew :application-core:test :adapter:outbound:objectstorage:test \
|
||||
:sample-portfolio:test --console=plain
|
||||
./gradlew :adapter:outbound:objectstorage:check --console=plain
|
||||
./gradlew :app-bootstrap:test --tests '*CleanArchitectureTest' --console=plain
|
||||
./gradlew verifyDependencyLocks --console=plain
|
||||
```
|
||||
|
||||
10. If Docker is available, run the MinIO fault source-set task. If unavailable, record the exact
|
||||
environmental blocker; never suppress its deprecation to claim success.
|
||||
|
||||
## Task 7: Remove remaining mechanical Error Prone warnings
|
||||
|
||||
**Owner leaves:** `application-core`, `app-bootstrap`, `sample-portfolio`, and the exact test leaves
|
||||
from the audit inventory
|
||||
|
||||
**Files:**
|
||||
|
||||
- Modify: `IdempotencyExecutor.java`, `IdempotencySettings.java`,
|
||||
`SampleIdempotencySettings.java`, and matching tests.
|
||||
- Modify: `TracingSampleRateResolver.java` and `TestTaxonomyArchitectureTest.java`.
|
||||
- Modify: `CleanArchitectureTest.java`, `ManagementActuatorSecurityContractTest.java`,
|
||||
`ProblemDetailDisabledConfigTest.java`, and the serialization violation fixture.
|
||||
- Modify: `CreateWorkLogOutboxTest.java`, `WorkLogUseCasesTest.java`, and the remaining exact sample
|
||||
test warning locations.
|
||||
|
||||
**Steps:**
|
||||
|
||||
1. Replace five `Duration.ofHours(72)` sites with `Duration.ofDays(3)`.
|
||||
2. Add the missing Javadoc summary and render annotation names as `{@code @WebMvcTest}`.
|
||||
3. Add all 16 missing `@Override` annotations.
|
||||
4. Replace Boolean wrapper comparison with the direct literal/assertion form.
|
||||
5. Preserve the forbidden `new BigDecimal(double/float)` bytecode and add method-only
|
||||
`BigDecimalLiteralDouble` suppressions with fixture rationale.
|
||||
6. Replace the three test-only one-argument splits without changing each grammar:
|
||||
limit-bearing CSV handling, equivalent mapping-path scanning, and exact byte-range parsing.
|
||||
7. Run affected owner tests and rerun all compile tasks with Error Prone:
|
||||
|
||||
```bash
|
||||
./gradlew :application-core:test :app-bootstrap:test :sample-portfolio:test --console=plain
|
||||
./gradlew compileJava compileTestJava --rerun-tasks --console=plain
|
||||
```
|
||||
|
||||
## Task 8: Capture Redis lab expected failures and configure clean test JVMs
|
||||
|
||||
**Files:**
|
||||
|
||||
- Modify: `infra/redis-lab/test/redis-lab-contract.sh`
|
||||
- Add: `src/gradle/test-jvm-agents.gradle`
|
||||
- Modify: `src/build.gradle`
|
||||
|
||||
**Steps:**
|
||||
|
||||
1. Change `assert_fails` to capture stdout/stderr per invocation, require non-zero status, assert the
|
||||
exact expected diagnostic with no extra lines, and print capture only on mismatch.
|
||||
2. Run `bash -n infra/redis-lab/test/redis-lab-contract.sh`, then run the real Redis lab Gradle/shell
|
||||
contract and verify successful output contains no leaked `redis-lab:` child diagnostics.
|
||||
3. Add a dedicated `mockitoAgent` configuration per Java test project and a relocatable
|
||||
`CommandLineArgumentProvider` in `src/gradle/test-jvm-agents.gradle`. Require exactly one
|
||||
`mockito-core` jar and emit `-javaagent:<absolute jar>` plus test-only `-Xshare:off`.
|
||||
4. Apply the script once from the root build and wire every ordinary/custom `Test` task without
|
||||
changing production JVM arguments.
|
||||
5. Run representative Mockito-heavy app-bootstrap, Redis, object-storage, and messaging tests and
|
||||
verify no self-attachment/CDS warning is printed.
|
||||
|
||||
## Task 9: Enable warning-zero blocking gates
|
||||
|
||||
**Files:**
|
||||
|
||||
- Modify: `src/build.gradle`
|
||||
- Modify: `src/app-bootstrap/build.gradle`
|
||||
- Modify: `.github/workflows/ci-quality-gates.yml`
|
||||
|
||||
**Steps:**
|
||||
|
||||
1. First run every `JavaCompile` task with `-Xlint:deprecation` and `-Xlint:unchecked`; resolve every
|
||||
remaining diagnostic at the exact source owner.
|
||||
2. Add `-Werror`, `-Xlint:deprecation`, and `-Xlint:unchecked` to every leaf `JavaCompile` task while
|
||||
retaining Error Prone.
|
||||
3. Remove root `checkstyleTest` and `spotbugsTest` `ignoreFailures=true`.
|
||||
4. Remove app-bootstrap `sampleOffTest`, `functionalTest`, and `conditionalTransportTest`
|
||||
Checkstyle/SpotBugs ignore overrides. Keep only `quarantineTest` non-blocking.
|
||||
5. Add `--warning-mode=fail` to the blocking `quality-gates` Gradle invocation.
|
||||
6. Run:
|
||||
|
||||
```bash
|
||||
./gradlew checkstyleTest spotbugsTest --rerun-tasks --console=plain
|
||||
./gradlew check --warning-mode=fail --no-daemon --console=plain
|
||||
```
|
||||
|
||||
## Task 10: Fresh repository verification, review, and Wiki capture
|
||||
|
||||
**Files:**
|
||||
|
||||
- Modify: `docs/superpowers/plans/2026-08-02-warning-zero-build-refactoring.md` only if execution
|
||||
evidence exposes a plan correction.
|
||||
- Modify external Wiki capture:
|
||||
`/home/donghyeon/workspace/ai-tool/llm-wiki-private/raw/branch-notes/main.md`
|
||||
and `raw/errors/build-success-warning-debt-2026-08-02.md`.
|
||||
|
||||
**Steps:**
|
||||
|
||||
1. Run owner-focused tests for every changed leaf.
|
||||
2. Run repository verification from `src/`:
|
||||
|
||||
```bash
|
||||
./gradlew test --no-daemon --console=plain
|
||||
./gradlew check --no-daemon --console=plain
|
||||
./gradlew build --warning-mode=fail --no-daemon --console=plain
|
||||
./gradlew clean build --warning-mode=all --no-daemon --console=plain
|
||||
./gradlew verifyCleanArchitectureDependencies verifyRuntimeModuleMembership \
|
||||
verifyDependencyLocks verifyPublicPathSnapshot verifyEnvKeys \
|
||||
--no-daemon --console=plain
|
||||
```
|
||||
|
||||
3. Verify the gate matrix, wrapper, shell syntax, XML findings/skips, and diff:
|
||||
|
||||
```bash
|
||||
bash .github/scripts/verify-gate-matrix.sh
|
||||
bash .github/scripts/verify-gradle-wrapper.sh .
|
||||
bash -n infra/redis-lab/test/redis-lab-contract.sh
|
||||
git diff --check
|
||||
```
|
||||
|
||||
4. Scan the fresh build log for `warning:`, deprecated/unchecked `Note:`, SpotBugs non-zero output,
|
||||
OpenJDK/CDS warnings, Mockito self-attachment, and leaked expected-negative Redis diagnostics.
|
||||
5. Confirm the skipped-test XML inventory is exactly the five approved optional-adapter contract
|
||||
cases and no qualification source set skipped.
|
||||
6. Dispatch independent code review over behavior fixes, legacy/provider migrations, and
|
||||
Gradle/test-noise gates. Apply only evidence-backed findings and rerun affected/full gates.
|
||||
7. Update the mandatory Wiki branch/error notes with changed files, commands, results, suppression
|
||||
inventory, blocked environment-only qualifications, and evidence grade. Run per-file Wiki lint;
|
||||
retain the known `main.md` naming-policy conflict without weakening either policy.
|
||||
8. Report success only if the clean build is exit zero and the final log is warning/noise clean.
|
||||
@@ -0,0 +1,56 @@
|
||||
# Web Security Boundary Implementation Plan
|
||||
|
||||
> **Execution:** Follow test-driven development and request an independent read-only review before
|
||||
> advancing to Redis session/CSRF.
|
||||
|
||||
**Goal:** Make JWT/JWKS and CORS filter-boundary behavior hermetic, release-blocking, and impossible
|
||||
to skip silently.
|
||||
|
||||
**Architecture:** Tests remain in inbound-web, use only existing dependencies, and cross the real
|
||||
Spring Security filter chain. A tagged Gradle task isolates them from the ordinary unit suite.
|
||||
|
||||
**Tech Stack:** Java 21, Spring Boot 4.0.0, Spring Security 7, Nimbus JOSE JWT, JDK HttpServer,
|
||||
MockMvc, Gradle 9.
|
||||
|
||||
### Task 1: Dedicated No-Skip Test Gate
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/adapter/inbound/web/build.gradle`
|
||||
|
||||
- [x] Register `webSecurityBoundaryTest` over `sourceSets.test` with tag inclusion, no-discovery
|
||||
failure, no up-to-date reuse, UTC, and a root-suite skipped-count guard.
|
||||
- [x] Exclude `security-boundary` from ordinary `test` and require the dedicated task from `check`.
|
||||
- [x] Confirm 13 tagged tests are discovered with zero skips and no dependency/lock entry is added.
|
||||
|
||||
### Task 2: JWT/JWKS RED Contracts
|
||||
|
||||
**Files:**
|
||||
- Create: `src/adapter/inbound/web/src/test/java/dev/caskeleton/adapter/inbound/web/auth/JwtJwksSecurityFilterIntegrationTest.java`
|
||||
|
||||
- [x] Add a loopback OIDC discovery/JWKS server with request counters and deterministic 503 mode.
|
||||
- [x] Add RS256 token generation using ephemeral keys and conspicuous secret sentinels.
|
||||
- [x] Prove lazy startup and valid bearer-to-principal conversion.
|
||||
- [x] Prove exact expiry, issuer, audience, signature, unknown-kid, and JWKS-outage envelopes/headers.
|
||||
- [x] Prove same-context recovery after a first-request JWKS 503 and prove mismatched discovery
|
||||
metadata reaches the safe 500 `INTERNAL_AUTH_MISCONFIGURATION` filter boundary.
|
||||
- [x] Run the dedicated task and record RED: unknown kid was classified as signature failure and a
|
||||
first-request JWKS 503 escaped as `JwtDecoderInitializationException`/`AuthenticationServiceException`.
|
||||
|
||||
### Task 3: CORS RED Contracts
|
||||
|
||||
**Files:**
|
||||
- Create: `src/adapter/inbound/web/src/test/java/dev/caskeleton/adapter/inbound/web/auth/CorsSecurityFilterIntegrationTest.java`
|
||||
|
||||
- [x] Prove approved credentialed preflight bypasses bearer authentication and emits exact headers.
|
||||
- [x] Prove denied origin, disabled CORS, wildcard-without-credentials, and approved actual-origin behavior.
|
||||
- [x] Assert bounded `Vary` behavior and no reflection of an unapproved sentinel origin.
|
||||
- [x] Run the dedicated task: all five CORS filter-boundary contracts passed without production changes.
|
||||
|
||||
### Task 4: Minimal Production Fixes and Verification
|
||||
|
||||
- [x] If RED exposes a production mismatch, change only the owning classifier/security configuration
|
||||
and keep stable error-code/header contracts intact.
|
||||
- [x] Run `webSecurityBoundaryTest`, ordinary inbound-web `test`, module static analysis, `check`,
|
||||
dependency-lock verification, architecture verification, and `git diff --check`.
|
||||
- [x] Request an independent read-only review; add the requested same-context recovery and non-I/O
|
||||
initialization-failure contracts, and bind the loopback server to an explicit IPv4 address.
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,838 @@
|
||||
# Redis Wrapper and Typed API — repository adaptation and delivery status
|
||||
|
||||
- **Design:** `docs/superpowers/specs/2026-08-07-redis-wrapper-typed-api-design.md`
|
||||
- **Plan:** `docs/superpowers/plans/2026-08-07-redis-wrapper-typed-api-implementation-plan.md`
|
||||
- **Status date:** 2026-08-07
|
||||
- **All 27 tasks delivered.** Sections 13–24 record what each one decided and what the topology
|
||||
lanes found; `docs/redis/support-matrix.md` records which test produced which evidence.
|
||||
|
||||
---
|
||||
|
||||
## 1. Why the structure differs from the plan
|
||||
|
||||
The design and plan were written without the target repository attached, so they assume a
|
||||
`backend-skeleton/` root with twelve standalone Gradle projects under `modules/redis/`, Kotlin DSL
|
||||
build files, and the `io.backend.skeleton.redis` package root. The package README anticipates exactly
|
||||
this and instructs the implementer to keep the structural contract while conforming to whatever
|
||||
stronger rules the real repository already enforces.
|
||||
|
||||
This repository has three such rules, and all of them outrank the plan's file layout:
|
||||
|
||||
1. `src/config/architecture/modules.json` is a fail-closed registry of **exactly 19 leaf modules**,
|
||||
re-validated by `src/settings.gradle` on every configuration. Adding twelve Gradle projects would
|
||||
violate HARD-STOP condition 5 in `AGENTS.md`.
|
||||
2. The build is Groovy DSL with `dependencyLocking(STRICT)`, so the plan's `libs.versions.toml`
|
||||
entries and its Spring Data Redis 4.1 / Lettuce 7.6 pins cannot be introduced without regenerating
|
||||
lock state. The repository is on Spring Boot 4.0.0 with **Lettuce 6.8.1**.
|
||||
3. The package root is `dev.caskeleton`, not `io.backend.skeleton`.
|
||||
|
||||
The SDK therefore lives inside the already-registered `adapter:outbound:cache-redis` leaf, and each
|
||||
designed module is a package. What the separate Gradle projects would have enforced —
|
||||
dependency direction and driver containment — is enforced instead by
|
||||
`RedisSdkModuleBoundaryTest`, which reads the source tree and fails on a forbidden import.
|
||||
|
||||
### Module mapping
|
||||
|
||||
| Design module | Package under `dev.caskeleton.adapter.outbound.cache.redis.sdk` |
|
||||
| --- | --- |
|
||||
| `redis-core-api` | `api`, `api.key`, `api.codec`, `api.command`, `api.error`, `api.operations`, `api.reactive` |
|
||||
| `redis-core-lettuce` | `lettuce.codec`, `lettuce.command`, `lettuce.connection`, `lettuce.observability` |
|
||||
| `redis-spring-boot-starter` | `config` |
|
||||
| `redis-cluster` | `cluster` |
|
||||
| `redis-programmability` | `programmability` |
|
||||
| `redis-raw-gateway` | `raw` |
|
||||
| `redis-admin-plane` | `admin` |
|
||||
| `extensions/*` | `extensions.json`, `extensions.search`, `extensions.timeseries`, `extensions.probabilistic` |
|
||||
| `redis-testkit` | `src/test` and the existing `redisTest` source set |
|
||||
|
||||
### Other adaptations, and the reason for each
|
||||
|
||||
| Plan says | Repository does | Why |
|
||||
| --- | --- | --- |
|
||||
| `backend.redis.*` properties | `ca-skeleton.capabilities.redis-sdk.*` | Matches the existing capability property namespace and avoids colliding with `app.cache.redis`. |
|
||||
| `RedisEnvelope` is a record with a `byte[]` component | Value class with the same accessors | ErrorProne `ArrayRecordComponent` is a blocking check in this build. |
|
||||
| Jackson-based YAML policy loader | Explicit strict reader for a closed YAML subset | No Jackson or SnakeYAML on the main compile classpath, and a general YAML engine would accept anchors, merges, and duplicate keys inside a security policy file. |
|
||||
| `VersionedJsonCodec` maps objects reflectively | Frames a versioned JSON envelope around a caller-supplied `RedisPayloadCodec` | Same guarantee — schema id, version, size ceiling, hard failure on an unknown version — without an object mapper the module cannot depend on. |
|
||||
| Each task ends with `git commit` | No commits | `AGENTS.md` commit policy is `human-only`. |
|
||||
| Gradle tasks `redis72Test` … `cluster82Test` | Not registered | They belong to Task 8's testkit half and Task 26; both need Docker-backed Testcontainers, which Milestone A does not reach. |
|
||||
|
||||
---
|
||||
|
||||
## 2. Task status
|
||||
|
||||
| Task | Title | Status |
|
||||
| --- | --- | --- |
|
||||
| 1 | Module graph and shared quality rules | **Done** as a package graph plus `RedisSdkModuleBoundaryTest` |
|
||||
| 2 | Command policy catalog and metadata diff | **Done** |
|
||||
| 3 | Version, topology, risk, permit, budget models | **Done** |
|
||||
| 4 | Key namespace and slot-safe typed keys | **Done** |
|
||||
| 5 | Codec registry and versioned envelope | **Done** |
|
||||
| 6 | Stable error model and ambiguous execution | **Done** |
|
||||
| 7 | Sync and reactive public API with parity test | **Done** |
|
||||
| 8 | Properties, capability probe, connection isolation, permit authority | **Done** except the Testcontainers topology environments and their Gradle tasks |
|
||||
| 9 | Policy-aware executor and observability | **Done** |
|
||||
| 10 | String and Key/TTL operations, blocking and reactive | **Done** against the in-memory gateway; no real-server evidence |
|
||||
| 11 | Hash operations and the 7.4 field-TTL version gate | **Done** against the in-memory gateway; no real-server evidence |
|
||||
| 12 | Set and Sorted Set operations, blocking and reactive | **Done** against the in-memory gateway; no real-server evidence |
|
||||
| 13 | List operations and the bounded blocking lane | **Done** against the in-memory gateway; no real-server evidence |
|
||||
| 14 | Bitmap, bitfield, HyperLogLog, and geospatial operations | **Done** against the in-memory gateway; no real-server evidence |
|
||||
| 15 | Batch and pipeline | **Done** against the in-memory gateway; no real-server evidence |
|
||||
| 16 | Stream | **Done** against the in-memory gateway, including the Redis 8.2 deletion capability; `XNACK` (8.8) deferred, see §13 |
|
||||
| 17 | Pub/Sub and sharded Pub/Sub | **Done** against the in-memory bus; no real-server evidence |
|
||||
| 18–19 | Sentinel failover certainty, Cluster slot/redirect/topology | **Done** as pure logic with unit evidence; the fault-injection lane is Task 26 |
|
||||
| 21 | Registered scripts and functions | **Done** against the in-memory gateway; no real-server evidence |
|
||||
| 20 | Transactions | **Done**, with the fixture reworked to defer inside a MULTI window, see §24 |
|
||||
| 22 | Approved raw gateway | **Done** against the in-memory gateway; no real-server evidence |
|
||||
| 23 | Isolated admin plane | **Done** against the in-memory gateway; no real-server evidence |
|
||||
| 24–25 | JSON, Search, Time Series, Probabilistic extensions | **Done** against the in-memory gateway; no real-module evidence |
|
||||
| 26–27 | Topology/fault/ACL/performance harness, CI matrix and docs gates | **Done** — all three lanes have produced evidence on 7.4, see §21–§23 |
|
||||
|
||||
`RedisSdkModuleBoundaryTest.NOT_YET_IMPLEMENTED_MODULES` is the machine-checked version of the
|
||||
"not started" rows: the test fails if a listed package appears without the list being updated, and
|
||||
fails if an unlisted one is missing.
|
||||
|
||||
---
|
||||
|
||||
## 3. What Milestone A actually guarantees
|
||||
|
||||
- Every command the SDK will ever run is classified in
|
||||
`src/main/resources/redis-sdk/redis-command-policy.yml`. An unclassified command is refused by
|
||||
`RedisCommandCatalog`, so a Redis upgrade cannot make a new command reachable by default.
|
||||
- `KEYS`, `FLUSHALL`, `FLUSHDB`, `SHUTDOWN`, `DEBUG`, `EVAL`, `CONFIG SET`, and the deprecated
|
||||
command names are `BLOCKED` with ACL account `NONE`.
|
||||
- R2 commands cannot execute without both an issued permit and an `OperationBudget`, and a permit the
|
||||
caller implemented itself fails provenance verification.
|
||||
- Sync and reactive typed API surfaces are mechanically proven to be in parity.
|
||||
- Metric and trace tags are a closed low-cardinality set with no key, field, member, or value in it.
|
||||
- A write that timed out is reported as `RedisAmbiguousExecutionException` with `retryable=false`,
|
||||
and `RedisFailureMetadata` rejects the retryable-and-ambiguous combination at construction.
|
||||
|
||||
## 4. What Milestone A does not guarantee
|
||||
|
||||
- No command has been executed against a real Redis server by this work. Every test is a unit or
|
||||
contract test over fakes; the contract suites the plan defines for Tasks 10–17 do not exist yet.
|
||||
- The typed operation interfaces have no implementation, so `RedisOperations` cannot be wired into a
|
||||
Spring context yet. `RedisSdkSettings` is bound but no bean registration reads it.
|
||||
- Cluster slot calculation is a caller-supplied function; the CRC16 implementation is Task 19.
|
||||
## 5. Cleanup of everything the design does not specify
|
||||
|
||||
The leaf previously carried five Redis capabilities that this design does not describe — semantic
|
||||
cache, session, request-replay idempotency, soft lease, and edge rate limit — together with their
|
||||
evidence and readiness governance. All of it is removed, so the Redis surface is now exactly the
|
||||
SDK.
|
||||
|
||||
| Removed | Scale |
|
||||
| --- | --- |
|
||||
| `cache-redis` non-SDK sources, tests, Lua programs, and the `redisTest` evidence source set | 188 main + 105 test + 18 evidence Java files, 52 resources |
|
||||
| `cache-redis/build.gradle` | 626 lines → 22; ~50 evidence/readiness lanes gone |
|
||||
| `app-bootstrap` Redis wiring, health contributor, material providers, `redisCompositionTest` source set | 15 files plus its Gradle tasks and configurations |
|
||||
| `application-core/src/redisPolicyContractTest` | 1 file plus its source set |
|
||||
| Root `build.gradle` Redis readiness/evidence/CI-matrix governance | 1,420 lines |
|
||||
| `config/redis/`, `gradle/redis-test-images.properties`, `infra/redis-lab/`, `.github/workflows/redis-production-readiness.yml` | removed |
|
||||
| `ci-quality-gates.yml` / `ci-gate-matrix.yml` | `redis-standalone` job retargeted to `redis-sdk` |
|
||||
|
||||
Kept deliberately: `shared-contract`'s `EdgeRateLimitPort` and its provider-neutral contract test.
|
||||
It is a rate-limit port, not a Redis type, and the design's exclusion list covers business policy
|
||||
rather than application ports.
|
||||
|
||||
Verified after the cleanup: `./gradlew test`, `verifyCleanArchitectureDependencies`,
|
||||
`verifyEnvKeys`, `verifyDependencyLocks` all pass; `verify-gate-matrix.sh` reports 27 gates OK.
|
||||
Dependency locks were regenerated for every module.
|
||||
|
||||
## 6. Task 10 — decisions a reviewer should check
|
||||
|
||||
The string and key/TTL operations landed in `sdk.lettuce.operations`, which is the package form of
|
||||
the plan's `redis-core-lettuce/.../lettuce/operations`. Five things differ from a literal reading of
|
||||
the plan, each for a stated reason.
|
||||
|
||||
| Decision | Why |
|
||||
| --- | --- |
|
||||
| A narrow `RedisCommandGateway` seam sits between the typed operations and Lettuce; `LettuceRedisCommandGateway` is the only class that touches the driver. | The plan's contract suites run on Testcontainers, which this environment has no lane for. The seam lets the whole policy path — catalog, permit provenance, budget, admission order, decode — be proven deterministically, and it keeps driver containment real rather than asserted. It is not a substitute for the real-server evidence Task 26 owns. |
|
||||
| Where design section 10 gives an R2 method only a permit (`multiGet`, `delete`, `unlink`, `rename`, `scan`) or only a budget (`append`, `getRange`, `setRange`), the SDK fills the missing half. | `CommandPolicyGuard` requires both for every R2 command. The caller-supplied half always wins; the other comes from `RedisOperationLimits` or a permit the SDK itself holds. Without this, half the designed R2 surface could not be admitted at all. |
|
||||
| Increment-with-initial-TTL runs a registered Lua script, and the SDK loads that script itself inside the guarded `EVALSHA` invocation. | Redis 7.2–8.2 has no `INCR` variant carrying an expiry, and both two-command sequences leak a permanent counter on a crash. The `SCRIPT LOAD` that resolves the digest is therefore *not* separately admitted by the guard — it travels under the `EVALSHA` admission with the same `registered-script` permit and script budget. Proper script registration is Task 20/21's `programmability` module; this is the narrowest thing that makes the operation correct in the meantime. |
|
||||
| No `INCREX` version-gated path. | No shipped Redis version has the command, so it is in neither the policy catalog nor `RedisCapability`. Adding a gate for a command that does not exist would be untestable. |
|
||||
| `expire`/`expireAt` report `ABSENT` only when the condition was `ALWAYS`. | Redis answers `0` both for a missing key and for an unmet condition. With `ALWAYS` the only possible cause is a missing key; with any other condition the SDK reports `CONDITION_NOT_MET` rather than guessing. A non-positive TTL is refused outright instead of silently deleting the key. |
|
||||
|
||||
Coverage: 30 new tests (`RedisValueOperationsContractTest`, `RedisKeyOperationsContractTest`) over
|
||||
permit provenance, budget ceilings, atomic counter creation, script reload after `NOSCRIPT`,
|
||||
namespace-bounded paging, and blocking/reactive agreement. `lettuce/operations` is registered in
|
||||
`RedisSdkModuleBoundaryTest.DESIGNED_MODULES`.
|
||||
|
||||
## 7. Task 11 — the field-TTL version gate
|
||||
|
||||
The gate design section 10.2 asks for is applied in two independent places, because either one alone
|
||||
is weaker than it looks.
|
||||
|
||||
- `LettuceRedisHashFieldExpirationOperations.ifSupported(...)` returns empty below Redis 7.4, so a
|
||||
composition root has nothing to inject and a caller cannot hold the API at all. This is the
|
||||
"bean is absent on 7.2" property the plan's Redis 7.2 test asserts.
|
||||
- `HEXPIRE`, `HPEXPIRE`, `HPERSIST`, `HTTL`, and `HPTTL` carry `minimum-version: "7.4"` in the policy
|
||||
catalog, so `CommandPolicyGuard` refuses them on an older server even for a hand-built instance.
|
||||
`guardRefusesFieldExpiryOnAnOlderServer` proves that second layer by forcing an instance into
|
||||
existence against a 7.2 server and watching the guard reject it.
|
||||
|
||||
`entries` is R2 with a caller-supplied permit *and* budget, exactly as designed — it is the one hash
|
||||
method the design gives both, so nothing is filled in for it. `HSCAN` gets the Task 10 treatment: an
|
||||
SDK `cursor-scan` permit and a budget derived from the requested page, because
|
||||
`scan(HashKey, ScanRequest)` carries neither. `HGETALL` and `HSCAN` replies are measured against the
|
||||
budget before decoding, so an oversized hash is refused rather than materialised.
|
||||
|
||||
One Lettuce accommodation is worth knowing about: its only batched `HSET` takes a `Map`, which for a
|
||||
`byte[]`-keyed connection means identity hashing. The seam therefore passes two positional lists and
|
||||
`LettuceRedisCommandGateway.hashPutAll` is the single place that builds the map — never reading from
|
||||
it, only iterating — with the ErrorProne check suppressed there and nowhere else.
|
||||
|
||||
## 8. Task 12 — the range commands are encoded, not borrowed
|
||||
|
||||
Design section 10.5 requires `rangeByScore`, `rangeByLex`, and a descending `rangeByRank`. Lettuce
|
||||
6.8 has no typed `ZRANGE ... BYSCORE / BYLEX / REV`; its only typed paths are the deprecated
|
||||
`ZRANGEBYSCORE`, `ZREVRANGEBYSCORE`, `ZRANGEBYLEX`, `ZREVRANGEBYLEX`, and `ZREVRANGE`.
|
||||
|
||||
Those five stay `BLOCKED`, exactly like `SETNX`, `GETSET`, `HMSET`, `RPOPLPUSH`, and `GEORADIUS`.
|
||||
`LettuceRedisCommandGateway` encodes the modern command itself — `ZRANGE key min max
|
||||
BYSCORE|BYLEX [REV] LIMIT offset count [WITHSCORES]` — through Lettuce's typed `dispatch` with a
|
||||
fixed `CommandType.ZRANGE`, a fixed output, and arguments built from the already-rendered key. Every
|
||||
range read therefore declares `ZRANGE` to the guard and sends `ZRANGE` on the wire, so the ACL
|
||||
account and the catalog drift gate stay aligned with reality.
|
||||
|
||||
This is not the forbidden raw-command surface: there is no method anywhere that accepts a command
|
||||
name, and the encoding lives in the one class that is already allowed to know the driver.
|
||||
|
||||
Everything else in Task 12 follows Task 10's rules. `SMEMBERS` has no method at all — the API offers
|
||||
`scan` or permit-and-budget set algebra, and a test asserts no whole-set reader exists.
|
||||
`SRANDMEMBER`, `SSCAN`, and `ZSCAN` get an SDK permit plus a derived budget because their signatures
|
||||
carry neither; `SMOVE` takes the caller's multi-key permit; `SDIFF`/`SINTER`/`SUNION` and every range
|
||||
read take both from the caller, and the reply is measured against the budget before it is decoded.
|
||||
|
||||
## 9. Task 13 — the blocking lane
|
||||
|
||||
`LettuceRedisBlockingListOperations` takes its own `RedisCommandGateway`, which the composition root
|
||||
binds to a connection borrowed from `RedisConnectionKind.BLOCKING`. That parameter is the structural
|
||||
form of design section 10.3's "separate bean, dedicated pool": a command that occupies its
|
||||
connection until the server answers cannot be issued down the lane ordinary traffic shares, and the
|
||||
type system is what stops it rather than a convention.
|
||||
|
||||
An unbounded wait is impossible on three independent levels: the request always declares its block,
|
||||
`ListOperationRequests` refuses a non-positive one before building anything, and
|
||||
`CommandPolicyGuard` refuses a block above the configured ceiling and sets the client timeout to the
|
||||
block plus `TimeoutProfile.BLOCKING_MARGIN`. All three are asserted.
|
||||
|
||||
`BLMOVE` needs both authorisations and the design gives the caller only one, so the caller's
|
||||
multi-key permit is verified in the operations layer while the SDK supplies the `blocking-pop`
|
||||
permit the guard demands. Crossing two keys and occupying a connection are separate decisions and
|
||||
the caller must still hold the first.
|
||||
|
||||
## 10. Task 14 — the ceiling that matters
|
||||
|
||||
A single `SETBIT` at an arbitrary offset allocates the whole prefix, so an unchecked offset is a
|
||||
memory-exhaustion primitive rather than a write. `RedisOperationLimits.maxBitmapOffset` bounds every
|
||||
bit offset — `GETBIT`, `SETBIT`, and each `BITFIELD` subcommand — before a command is built, and a
|
||||
negative offset is refused outright.
|
||||
|
||||
`BITOP`, `PFCOUNT`, `PFMERGE`, and `GEOSEARCHSTORE` take the caller's multi-key permit; `BITCOUNT`,
|
||||
`BITPOS`, `BITFIELD`, and `GEOSEARCH` take the caller's budget with an SDK permit. A geo search is
|
||||
bounded three ways — its own `count`, the collection ceiling, and the caller's budget measured
|
||||
against the reply before decoding.
|
||||
|
||||
## 11. Task 15 — the two decisions the design left open
|
||||
|
||||
`RedisBatch` exposes only `size()`, `keys()`, and `requestBytes()`, so it is opaque: nothing in the
|
||||
public contract lets a caller put commands into one. The SDK therefore owns both the concrete batch
|
||||
and the only way to fill it, and two questions had to be answered.
|
||||
|
||||
**What the builder covers.** `LettuceRedisBatch.Builder` covers the string, key, and hash surfaces
|
||||
rather than mirroring all ~80 typed methods. Those are what pipelining is actually used for, each
|
||||
extra method is one delegating line onto the existing request factories, and widening it later is
|
||||
mechanical rather than a redesign. A batch built anywhere else is refused.
|
||||
|
||||
**Whether R2 commands may be batched.** They may, carrying their own permit and budget exactly as
|
||||
they do alone. `BatchOptions` has no permit field, so the alternative was an R1-only batch — which
|
||||
would have blocked the case where saving a round trip matters most. Both ceilings apply and the
|
||||
smaller wins: the guard refuses an item that broke its own budget before the batch ceiling is even
|
||||
checked.
|
||||
|
||||
Four properties are enforced rather than documented. Every item is admitted **before** any command
|
||||
is sent, so one refused item cancels the batch instead of leaving it half-applied. Input index is
|
||||
result index, failure or not. Items fail independently — `hasPartialFailure` is the caller's signal,
|
||||
not an exception. And there is no retry path in the class at all, so a failed write is never
|
||||
re-sent.
|
||||
|
||||
## 12. Task 17 — a subscription is not a command
|
||||
|
||||
Publishing goes through the guard like anything else. Subscribing does not: it has no reply to bound
|
||||
and no timeout to apply, it occupies its connection for as long as it lives, and it therefore has
|
||||
its own seam — `RedisPubSubGateway`, bound to a connection borrowed from
|
||||
`RedisConnectionKind.PUBSUB`. A long-lived listener can never sit on the lane ordinary commands use.
|
||||
|
||||
What the guard would have checked is checked in `PubSubOperationRequests` instead: every channel and
|
||||
pattern must belong to the process namespace, an empty subscription is refused, and a pattern
|
||||
subscription demands the `pattern-subscribe` permit because the server decides how much a pattern
|
||||
matches.
|
||||
|
||||
Lifecycle is the part that leaks if it is only documented. The blocking API returns an
|
||||
`AutoCloseable` `Subscription`; the reactive API returns a `Flux` whose cancellation closes the
|
||||
driver handle. Both are asserted against a bus that reports how many subscriptions are still open,
|
||||
so an abandoned subscriber releasing its connection is a test, not a claim.
|
||||
|
||||
Sharded Pub/Sub is gated exactly like per-field expiry: `ifSupported` yields nothing below Redis
|
||||
7.0, and `SPUBLISH` carries the same minimum in the catalog so the guard refuses it independently.
|
||||
|
||||
### Still outstanding
|
||||
|
||||
`application.yml`, `.env`, and `docs/registries/env-keys.yaml` still carry the property blocks of
|
||||
the five removed capabilities. They bind nothing and the build is green with them present, but they
|
||||
are dead configuration and should go in the same sweep that removes the corresponding capability
|
||||
sections.
|
||||
|
||||
## 13. Task 16 — a stream entry, a payload field, and one command that had to be encoded
|
||||
|
||||
**One payload field.** `StreamKey<V>` carries exactly one payload codec and `StreamRecord<V>`
|
||||
exactly one value, so the SDK writes exactly one field, named `payload` in
|
||||
`StreamOperationRequests` and nowhere else. An entry that comes back with any other shape is
|
||||
refused rather than half-decoded: a foreign producer's record is an anomaly the caller has to see,
|
||||
not something to silently truncate into a `StreamRecord`.
|
||||
|
||||
**`XREAD` forced a catalog distinction.** `BLPOP` has no non-blocking form, so a request that omits
|
||||
its block is a defect. `XREAD` does have one — the same command name is an ordinary bounded read
|
||||
without `BLOCK`. The catalog previously modelled only "blocking", which would have meant either
|
||||
rejecting every non-blocking stream read or excusing the stream reads from the rule that nothing
|
||||
waits forever. Both were wrong, so `optional-block` was added to the policy schema and
|
||||
`RedisCommandPolicy.requiresServerBlock()` now separates the two. `XREAD` and `XREADGROUP` are the
|
||||
only commands that carry it. The blocking bean still takes a non-nullable `Duration`, and the guard
|
||||
still refuses a non-positive block or one over the configured ceiling.
|
||||
|
||||
**A group read has exactly two legal offsets.** `NewForGroup` and `PendingForConsumer` are accepted;
|
||||
`After` and `Latest` are refused. Reading a group from an arbitrary identifier would hand a consumer
|
||||
entries the group already distributed elsewhere without moving the pending list — a duplicate
|
||||
delivery the caller did not ask for. The mirror rule holds for the group-free read, which refuses
|
||||
the two group offsets.
|
||||
|
||||
**`XAUTOCLAIM` is encoded, not borrowed.** Lettuce's typed `xautoclaim` returns `ClaimedMessages`,
|
||||
which drops the third reply element: the identifiers that were pending but no longer exist in the
|
||||
stream. `ClaimResult.deletedIds` is part of the SDK contract precisely because a consumer that
|
||||
cannot see that list keeps sweeping the same tombstones forever. The command is therefore built in
|
||||
`LettuceRedisCommandGateway` with `NestedMultiOutput`, the same precedent set by the sorted-set
|
||||
ranges in §8 — the command declared to the guard is still the command on the wire, and no method
|
||||
accepts a command name.
|
||||
|
||||
**Permits and budgets.** `XTRIM` runs under `bounded-collection-write`, the ranges under
|
||||
`bounded-collection-read`, both reads under the new `stream-read` policy, and `XPENDING`/`XAUTOCLAIM`
|
||||
under `stream-recovery`. None of the design's stream signatures carry a caller permit, so all four
|
||||
are SDK permits; the caller-supplied bound is the mandatory `count`, which becomes both the guard's
|
||||
budget and the ceiling checked against `maxCollectionElements`. There is no "read the whole stream"
|
||||
call that can be written against this API.
|
||||
|
||||
**Redis 8.2 deletion landed; 8.8 `XNACK` did not.** `XACKDEL`/`XDELEX` are behind
|
||||
`LettuceRedisStreamDeletionOperations.ifSupported(...)`, gated exactly like hash field expiry — the
|
||||
capability probe decides whether a bean exists, and the catalog's 8.2 minimum refuses a hand-built
|
||||
one. `XNACK` is deliberately not implemented: the pinned Lettuce 6.8.2 has no typed form for it, and
|
||||
unlike `XAUTOCLAIM` its wire format cannot be verified against a driver or a released server, so
|
||||
encoding it by hand would be inventing a protocol rather than adapting one. The capability, the
|
||||
catalog entry, and the 8.8 minimum stay in place; the bean is the only missing piece and should be
|
||||
added when the command is available in the driver or in a released server.
|
||||
|
||||
## 14. Tasks 18–19 — the parts that do not need a cluster to be true
|
||||
|
||||
Both tasks are specified against real Sentinel and Cluster environments, which this repository does
|
||||
not yet have a lane for. What landed is the half that is decidable without one, and it is the half
|
||||
the rest of the SDK depends on.
|
||||
|
||||
**The slot calculator is a pre-flight check, not a redirect handler.** `RedisSlotCalculator`
|
||||
computes CRC-16/XMODEM over the hash tag exactly as Redis does, so `CommandPolicyGuard` can refuse a
|
||||
cross-slot multi-key command before it is written. A server-side `CROSSSLOT` would arrive after the
|
||||
request left the process, which is precisely the outcome the guard exists to prevent. The seam was
|
||||
already there — the guard has always taken a `ToIntFunction<String>` — so this task filled it rather
|
||||
than changing the pipeline. The published slots for `foo`, `bar`, and `hello` are asserted, so a
|
||||
regression in the checksum shows up as a wrong number rather than as a cluster that quietly
|
||||
mis-routes.
|
||||
|
||||
**An empty tag is not a tag.** `{}` hashes the whole key, matching Redis, and that is tested,
|
||||
because the alternative — hashing an empty string — would collapse every such key onto one slot.
|
||||
|
||||
**A cluster scan is not a snapshot, and `ClusterScanCursor` refuses to pretend otherwise.** A sweep
|
||||
is complete only when every primary has *answered* with a zero cursor; a primary that was never
|
||||
asked counts as unfinished. Reporting completion after skipping a shard would let a caller conclude
|
||||
a key does not exist when a whole shard was never looked at.
|
||||
|
||||
**Redirect counting separates two different incidents.** A trickle of `MOVED` means the client's
|
||||
topology is stale; `ASK` and `TRYAGAIN` mean a resharding is in progress. The driver follows both
|
||||
transparently, so neither is visible to a caller — `ClusterTopologyObserver` is what makes them
|
||||
visible to an operator, and it accepts slot numbers and node identifiers only, never a key.
|
||||
|
||||
**`ExecutionCertainty` is the failover decision made explicit.** "The server refused it" and "the
|
||||
connection died after the command was written" look identical to a caller and have opposite
|
||||
consequences. `SentinelFailoverObserver.classify` returns `SAFE_TO_RETRY_FAILURE` only when the
|
||||
command provably never reached the server; anything written and unanswered is `AMBIGUOUS_FAILURE`,
|
||||
and `allowsAutomaticRetry` then defers to the command policy's `retry-safe` flag. A non-idempotent
|
||||
write is therefore never resent by the pipeline, and each one is counted so an operator knows how
|
||||
many need reconciling.
|
||||
|
||||
**The reconnect queue is bounded on purpose.** An unbounded queue turns a thirty-second promotion
|
||||
into a thirty-second backlog that lands at once on a freshly promoted primary. Refusals past the
|
||||
bound are counted so the bound can be tuned from evidence rather than guessed.
|
||||
|
||||
**What is still owed:** the fault-injection evidence. Nothing here proves how Lettuce actually
|
||||
behaves during a promotion or a resharding — that is a real-topology lane and belongs to Task 26.
|
||||
These types are the classification and accounting that lane will assert against.
|
||||
|
||||
## 15. Task 21 — scripts are a deployment artefact, and Task 20 is blocked on the fixture
|
||||
|
||||
**Nothing accepts a script body at call time.** `EVAL` is blocked in the command policy, so the only
|
||||
reachable path is `EVALSHA` of a digest that `RedisScriptRegistry` obtained from a `SCRIPT LOAD` of
|
||||
a reviewed `RegisteredRedisScript`. A script assembled from request data has the blast radius of the
|
||||
whole keyspace; making registration a deployment step is what turns "we only run reviewed scripts"
|
||||
from a convention into a structural property.
|
||||
|
||||
**Keys are declared, and that is what makes them checkable.** Every key goes into the request's key
|
||||
list, so a script is namespace-checked and same-slot-checked exactly like any other multi-key
|
||||
command. `RedisArgument` is a distinct type from a key for the same reason: a key smuggled through
|
||||
`ARGV` would bypass both checks, and having the two be different types is what makes that a compile
|
||||
problem rather than a review problem.
|
||||
|
||||
**A registered script returns one bulk reply.** That is a contract, not a limitation of
|
||||
`RedisResultDecoder`. A nested Lua table forces the SDK to guess how deep the reply is and how each
|
||||
level is typed, which is the ambiguity a typed API exists to remove. Encode the result and decode it
|
||||
in the decoder.
|
||||
|
||||
**`NOSCRIPT` is the one automatic retry in the SDK.** The server rejects the call before running
|
||||
anything, so reloading and re-issuing once repeats nothing. It is not a retry of an ambiguous write,
|
||||
and no other failure is retried on this path.
|
||||
|
||||
**Functions are callable, not loadable.** `FUNCTION LOAD` is `ADMIN_ONLY` in the catalog and belongs
|
||||
to the admin plane, so `RedisFunctionOperations` has no method that introduces server-side code.
|
||||
`RegisteredRedisFunction` carries the library's semantic version because a library replaced under
|
||||
the same name changes behaviour with no signal at the call site. A function declared read-only is
|
||||
issued as `FCALL_RO`, which lets the server refuse a wrong declaration — worth more than the replica
|
||||
routing it also buys.
|
||||
|
||||
**Task 20 is deliberately not half-done.** `WATCH`/`MULTI`/`EXEC` is implementable against Lettuce —
|
||||
after `MULTI` the command futures complete when `EXEC` runs — but proving it needs a fixture that
|
||||
models that deferral. The current `InMemoryRedisCommandGateway` completes every future eagerly, so a
|
||||
transaction written against it would apply its writes *before* the `WATCH` conflict was detected: the
|
||||
fixture would report a correct-looking conflict while the effects had already landed. A fake that
|
||||
lies about atomicity is worse than no fake, so the transaction work is deferred until the fixture
|
||||
grows a deferral model (or the real-server lane from Task 26 exists), rather than being landed
|
||||
against a fixture that cannot falsify it.
|
||||
|
||||
## 16. Task 22 — the escape hatch, and why it is not an escape
|
||||
|
||||
The raw gateway exists because a few commands have no typed form worth building, not because
|
||||
arbitrary command execution is acceptable. Everything about its shape follows from that.
|
||||
|
||||
**Two independent gates, neither decided at request time.** A command must be classified
|
||||
`RAW_ONLY` in `redis-command-policy.yml` — the organization's decision about which commands may ever
|
||||
leave through this door — *and* the deployment must have registered an `ApprovedRawCommand` for it
|
||||
in `RawCommandApprovals`. Neither alone is enough. The approval carries the argument, request, and
|
||||
reply ceilings and the timeout, so widening what may be sent is a deployment change, not a call-site
|
||||
one.
|
||||
|
||||
**The token is bound to its registry.** `RawCommandApprovals.issue` is the only source, and
|
||||
`verify` refuses a token from a different registry instance, a token issued for another policy, and
|
||||
an approval that is not byte-for-byte the registered one. That last check is the one that matters:
|
||||
without it a caller could present a widened copy of a real approval and keep the real policy id.
|
||||
|
||||
**Keys are parsed back, not taken on trust.** Arguments reach the gateway as opaque bytes, so the
|
||||
catalog's key specification locates the key positions and `RedisOperationContext.parseKey` — the
|
||||
same strict parse `SCAN` uses — turns each one back into a `QualifiedRedisKey`. A key outside the
|
||||
bound namespace or one that does not follow the key grammar is refused before anything is sent. A
|
||||
`movable` key specification cannot be checked without asking the server with `COMMAND
|
||||
GETKEYSANDFLAGS`, so it is refused at registration time; `SORT` and `SORT_RO` are therefore
|
||||
classified `RAW_ONLY` but not approvable until that lookup exists.
|
||||
|
||||
**Every RAW_ONLY command now names a permit policy.** The guard's rule is that an R2 command always
|
||||
states the policy that authorised it. The raw path used to be the one place that rule did not hold,
|
||||
so `raw-command` was added to the three `RAW_ONLY` entries and the gateway presents the SDK permit
|
||||
for it. The approval registry still decides *which* commands a deployment may send; the permit is
|
||||
what keeps the guard's invariant true on this path too.
|
||||
|
||||
**Everything else was already built.** Reachability, minimum version, risk refusal, and the timeout
|
||||
profile come from the catalog; namespace and same-slot from the guard; the audit record from the
|
||||
executor's observation, which carries the command family and latency and never a key or a value.
|
||||
The one new seam method, `sendApprovedRaw`, takes a `CommandId` rather than a string — by the time
|
||||
it is reached the identity has already been validated, classified, and matched to an approval.
|
||||
|
||||
## 17. Task 23 — the admin plane is defined by what it cannot do
|
||||
|
||||
Design section 14.2 lists what the admin plane must never reach. None of it is enforced by
|
||||
`RedisAdminOperations` omitting a method — omission is not enforcement, because the next person to
|
||||
add one would not notice. `FLUSHDB`, `FLUSHALL`, `SHUTDOWN`, `DEBUG`, `CONFIG SET`, `CONFIG REWRITE`,
|
||||
`CLIENT KILL`, `ACL SETUSER`, `ACL DELUSER`, `SLOWLOG RESET`, `LATENCY RESET`, `SCRIPT FLUSH`,
|
||||
`FUNCTION FLUSH`, and `MODULE UNLOAD` are all `BLOCKED` in the catalog, which means no path in the
|
||||
SDK can send them, and a test asserts that list rather than trusting it.
|
||||
|
||||
**Every diagnostic is checked against the catalog before it is built.** Not classified
|
||||
`ADMIN_ONLY`, or not read-only, and it is refused. That check is what stops a future addition to
|
||||
this class from quietly becoming a write.
|
||||
|
||||
**Replies are projected, not forwarded.** A slow log entry carries the command family and drops the
|
||||
arguments; a client entry carries id, age, idle, and last command and drops the peer address and the
|
||||
connection name. Both are read by an operator and end up in dashboards and tickets, and the dropped
|
||||
fields are exactly the caller and tenant identity that must not travel that way. The command family
|
||||
is enough to find a call site; an address is not needed to find a leaking pool.
|
||||
|
||||
**A key is still a key.** `MEMORY USAGE` takes a `QualifiedRedisKey` and goes through the guard, so
|
||||
an admin diagnostic cannot read a key outside the bound namespace. An absent key reports {@code -1},
|
||||
not zero, because "this key uses no memory" and "this key does not exist" are different answers.
|
||||
|
||||
**Separation is structural, not documentary.** The plane takes its own gateway, bound to the admin
|
||||
account's own connection, the same way the blocking operations take theirs. What that cannot enforce
|
||||
is that the deployment actually configured a separate ACL account — which is precisely why the
|
||||
dangerous commands are blocked catalog-wide rather than left to the credentials to prevent.
|
||||
|
||||
## 18. Tasks 24–25 — four extensions, one seam, and the checks the guard cannot do
|
||||
|
||||
All four extension families share `ExtensionCommandRunner`, so every extension command declares its
|
||||
key and is namespace- and slot-checked exactly like a classic one. Sharing the runner is also what
|
||||
stops them drifting apart on the parts that matter.
|
||||
|
||||
**The probe is the authority, the version is a pre-filter.** A managed Redis 8 with no module loaded
|
||||
reports the version and not the commands, so each bean is created through `ifSupported(...)` and a
|
||||
deployment without the module simply has no instance. Catalog minimums are the second gate, not the
|
||||
first.
|
||||
|
||||
**Bounds are in the types, not in a caller's discipline.** A `JsonPath` is validated against a
|
||||
narrow grammar — roots, members, indices, recursive descent — so a path assembled from request data
|
||||
cannot become `$` and replace a whole document. A `TimeSeriesSample` series is created with a
|
||||
retention or not at all; unlike a stream there is no per-append trim to fall back on. A `SearchQuery`
|
||||
carries its offset, page size, and timeout, so "read the whole index" cannot be written. Every
|
||||
probabilistic structure is reserved with an explicit error rate and capacity, because one created
|
||||
implicitly by its first write gets server defaults and saturates into answering "probably present"
|
||||
for everything.
|
||||
|
||||
**The interfaces say the answers are approximate.** `probablyContains`, `estimateCount`,
|
||||
`estimateQuantile` — a false-positive rate does not become a correctness bug because someone read a
|
||||
method called `contains`.
|
||||
|
||||
**Search is the one place the guard cannot help.** An `FT` command addresses an index, and an index
|
||||
is not a key, so there is no key on the request to namespace-check. The index name is therefore a
|
||||
validated type rendered with the process's namespace prefix by the operations class, and the key
|
||||
prefix an index covers is rendered the same way. An index can only be created over — and queried
|
||||
against — documents this process owns, and that rule lives in one method rather than in a review
|
||||
checklist. `FT.DROPINDEX` is `BLOCKED` for the whole SDK: dropping an index is a destructive
|
||||
operational action, and an accidental one is indistinguishable from a search that suddenly returns
|
||||
nothing.
|
||||
|
||||
**What is still owed:** evidence against real modules. Nothing here proves how RedisJSON, the query
|
||||
engine, Time Series, or the probabilistic structures actually reply — the fixture answers with what
|
||||
the design says they answer. That is Task 26's lane.
|
||||
|
||||
## 19. The "dead capability property blocks" item was wrong
|
||||
|
||||
Earlier notes in this delivery listed `app-bootstrap/src/main/resources/application.yml`, `src/.env`,
|
||||
and `docs/registries/env-keys.yaml` as carrying dead property blocks for five removed capabilities
|
||||
(cache, session, idempotency, lease, rate-limit), to be deleted together because `verifyEnvKeys` is
|
||||
fail-closed.
|
||||
|
||||
That is not true for at least three of them. `ca-skeleton.capabilities.rate-limit.provider`,
|
||||
`.idempotency.provider`, and `.lease.provider` are read at startup by
|
||||
`dev.caskeleton.bootstrap.runtime.SecretSourceValidator`, which refuses to start when a provider is
|
||||
selected without its HMAC secret, and `SecretSourceValidatorTest` covers all three. Deleting those
|
||||
blocks would remove a live startup check and break the test.
|
||||
|
||||
`app.rate-limit.*` is a separate, also live tree bound by `EdgeRateLimitTransportSettings` in
|
||||
`adapter:inbound:web`; it is not the same property as the capability selector above and the two must
|
||||
not be conflated.
|
||||
|
||||
The `ca-skeleton.capabilities.cache.canonical.*` and `ca-skeleton.security.redis-session.*` blocks
|
||||
have no binder that a source search finds, so they may genuinely be residue — but "no binder found"
|
||||
is not the same as "unused", and removing keys from a fail-closed three-file invariant on that basis
|
||||
is not a change worth making without auditing each key's consumers. No cleanup was performed.
|
||||
|
||||
## 20. Tasks 26–27 — the harness landed, the evidence did not
|
||||
|
||||
I previously described these two as blocked on a real server. That was wrong and worth correcting:
|
||||
the *evidence* needs servers, but the harness, the ACL accounts, the docs gates, and the CI wiring
|
||||
are all files, and they are now in the repository.
|
||||
|
||||
**What landed.**
|
||||
|
||||
- `infra/redis-sdk/{standalone,sentinel,cluster}/compose.yml` — three lanes, version-parameterised so
|
||||
one file serves every row of the support matrix. Sentinel runs three sentinels because a
|
||||
two-sentinel quorum cannot survive losing one, and a failover test that cannot lose a sentinel is
|
||||
not testing failover. Cluster runs six nodes so a promotion can be forced without losing a shard,
|
||||
and waits for slot assignment before tests start.
|
||||
- `infra/redis-sdk/acl/*.acl` — one account per `CommandAccess` level, each deliberately narrower
|
||||
than the SDK's own rules. The account is the last boundary and a permit never widens it, so a
|
||||
mistake in the SDK is still refused by the server.
|
||||
- `redisTopologyTest`, a Gradle lane tagged `redis-topology` and excluded from the default unit task.
|
||||
It **fails closed**: selecting it without host, port, and mode is a `GradleException`, and
|
||||
`RedisTopologyEndpoint` refuses to default to `localhost:6379`. A topology test that silently
|
||||
passes because it never connected is worse than not having one.
|
||||
- `docs/redis/support-matrix.md`, which `RedisSupportMatrixTest` parses. A package or a capability
|
||||
that is not listed fails the build, so stating the support level is part of shipping a module
|
||||
rather than a follow-up someone remembers. The certified-version table says "lane declared, not
|
||||
run" for all three topologies, and the test asserts that string — a certified version cannot be
|
||||
claimed from a lane that has never produced evidence.
|
||||
- `docs/redis/command-policy.md`, `operations.md`, `upgrade-guide.md`. The upgrade guide states why
|
||||
each check exists, not just that it is required: an unclassified command is refused, but a command
|
||||
whose risk changed upstream and is still classified R1 here is not; a rollback that leaves a
|
||||
process holding stale script digests produces `NOSCRIPT` on every scripted call.
|
||||
- `.github/workflows/redis-sdk-topology.yml`, manual-dispatch only, plus two new entries in
|
||||
`.github/ci-gate-matrix.yml` — the support matrix as a release-blocking contract test, and the
|
||||
topology evidence as explicitly `delegated-pending`. The gate count moved from 27 to 29.
|
||||
|
||||
**What did not land: the evidence.** No assertion in `RedisTopologyContractTest` yet exercises a
|
||||
promotion, a resharding, an ACL denial, or the guardrail datasets from the plan (1 MiB string,
|
||||
hundred-thousand-element collections, a million-entry trimmed stream, a five-hundred-command
|
||||
pipeline). Writing those assertions against a lane that has never been started would produce tests
|
||||
whose first run is also their first review, so the lane is fail-closed and the support matrix says
|
||||
plainly that nothing is certified. That is the honest state, and the harness is what makes closing
|
||||
it a bounded piece of work rather than a project.
|
||||
|
||||
## 21. The standalone lane ran, and it found five defects
|
||||
|
||||
The lane in `infra/redis-sdk/standalone` was started against Redis 7.4 and
|
||||
`RedisTopologyContractTest` now asserts, for every account in `infra/redis-sdk/acl`, that the
|
||||
`CommandAccess` level grants exactly what the command policy catalog says it may issue. Seven tests
|
||||
pass. Getting there required fixing five things that reading the files would never have surfaced:
|
||||
|
||||
1. **The ACL files did not load at all.** A Redis `aclfile` accepts nothing but complete `user`
|
||||
lines — no comments, no line continuations — and the server refused to start. The rationale moved
|
||||
to `infra/redis-sdk/acl/README.md`, and the four accounts are concatenated into
|
||||
`all-accounts.acl` because Redis takes one `aclfile`.
|
||||
2. **The advanced account granted `SMEMBERS` and `SORT`.** Both are `RAW_ONLY`, so they belong to the
|
||||
raw gateway account alone. This is the defect worth caring about: the ACL account is the last
|
||||
enforcement boundary and a permit never widens it, so an account wider than the catalog silently
|
||||
removes the second control the whole raw-gateway design rests on.
|
||||
3. **The ordinary account granted `SORT_RO`,** for the same reason.
|
||||
4. **The ordinary account could not run `PUBLISH`, `SUBSCRIBE`, or `PING`,** all classified `TYPED`.
|
||||
5. **The ordinary account could not run `MULTI`, `EXEC`, `UNWATCH`, or `DISCARD`,** also `TYPED`.
|
||||
|
||||
6. **The admin account was missing twelve read-only diagnostics** the catalog exposes: the `OBJECT`,
|
||||
`PUBSUB`, and `XINFO` subcommands, `FUNCTION LIST`/`STATS`, and `CLUSTER KEYSLOT`. Closing this
|
||||
also forced a decision: `FUNCTION LOAD` is `ADMIN_ONLY` but not read-only, and granting it to an
|
||||
account named `admin-readonly` would make the name a lie. Loading a library is a deployment
|
||||
action with its own credentials, so the assertion covers read-only `ADMIN_ONLY` commands only.
|
||||
|
||||
There was also a defect in the test itself, which is worth recording because it is the failure mode
|
||||
this kind of test usually dies of: `ACL DRYRUN` checks arity *before* permission, so probing a
|
||||
command with the wrong number of arguments answers "wrong number of arguments" for an account that
|
||||
would have been refused anyway. Reading that as a grant makes the test pass while the account is
|
||||
wrong. The probe now walks argument counts until the server actually answers the permission
|
||||
question. A second one followed it: a command the server does not carry answers "not found", and
|
||||
skipping that without checking the catalog's minimum version is how a real ACL gap hides behind a
|
||||
module that happens not to be installed. An absent command is now only tolerated when the catalog
|
||||
already says the server is too old for it.
|
||||
|
||||
`docs/redis/support-matrix.md` records standalone 7.4 as "ACL contract verified"; Sentinel and
|
||||
Cluster remain "lane declared, not run", and `RedisSupportMatrixTest` still asserts that string.
|
||||
|
||||
**Still owed on this task:** the guardrail datasets (1 MiB string, hundred-thousand-element
|
||||
collections, a million-entry trimmed stream, a five-hundred-command pipeline) and the fault
|
||||
injection — promotion on the Sentinel lane, resharding on the Cluster lane. Those are the assertions
|
||||
`ExecutionCertainty` and `RedisSlotCalculator` were built to be checked against.
|
||||
|
||||
## 22. The guardrail run found the first real SDK defect
|
||||
|
||||
`LiveRedisGuardrailTest` is the first thing that puts `LettuceRedisCommandGateway` under the SDK's
|
||||
own contracts against a live server. Everything before it ran against
|
||||
`InMemoryRedisCommandGateway`, which is a deterministic stand-in and answers what the design says it
|
||||
should — so an encoding or budgeting mistake could not show up there by construction.
|
||||
|
||||
It found one immediately, and it is a good example of the class of bug a fake cannot catch:
|
||||
|
||||
**The cursor-scan reply budget was sized to the requested `COUNT`.** Redis treats `COUNT` as a hint,
|
||||
not a limit: it walks whole hash buckets and listpack entries and returns what it found. A real
|
||||
`HSCAN` asked for 500 came back with 501, and the SDK rejected a perfectly correct reply — a refusal
|
||||
the caller can neither act on nor avoid. `RedisOperationContext.scanBudget` now accepts the
|
||||
configured scan ceiling plus a fixed overshoot allowance, which is still a bound: a server returning
|
||||
an order of magnitude more than it was asked for is refused. All four scan sites (key, hash, set,
|
||||
sorted set) use it.
|
||||
|
||||
The rest of the datasets passed unchanged: the 1 MiB value ceiling holds and one byte over never
|
||||
leaves the process; a hundred-thousand-field hash refuses `HGETALL` and is only reachable by cursor;
|
||||
a stream trimmed to 1,000 stays trimmed while twenty thousand entries are appended; a
|
||||
five-hundred-command batch reports every item positionally.
|
||||
|
||||
**Still owed:** Sentinel promotion and Cluster resharding. Those need their own lanes started, and
|
||||
they are where `ExecutionCertainty` and `RedisSlotCalculator` finally get checked against reality.
|
||||
|
||||
## 23. The Sentinel and Cluster lanes ran, and the worst defect was not in the code
|
||||
|
||||
Both remaining lanes now produce evidence. `docs/redis/support-matrix.md` records which test
|
||||
produced which, and `RedisSupportMatrixTest` no longer asserts the literal string
|
||||
`"lane declared, not run"` — that gate worked only until the lanes ran, and a gate that has to be
|
||||
deleted the moment it binds was never a gate. It now requires every evidence claim to name a test
|
||||
class that exists in the source tree, which is a rule that survives the lanes running.
|
||||
|
||||
### The harness had to be fixed before it could produce anything
|
||||
|
||||
Neither compose file could have worked. Both published no ports, and more importantly both would
|
||||
have advertised container-internal addresses: Sentinel answers `get-master-addr-by-name` with the
|
||||
address it monitors and the client dials that itself, and a cluster client reads `CLUSTER SHARDS`
|
||||
and connects to every node it names. On a bridge network a host client resolves a topology it cannot
|
||||
reach. Both lanes now use host networking with fixed ports, which is the only arrangement where the
|
||||
address the topology advertises is the address the client can use.
|
||||
|
||||
Three smaller harness defects went with it: the endpoint record assumed the declared address was a
|
||||
data node (on the Sentinel lane it is a sentinel, so ACL assertions were being asked of the
|
||||
sentinel's own accounts); the CI workflow passed `6379` for all three lanes; and `redisTopologyTest`
|
||||
was cacheable, so Gradle reported a previous run's verdict as the current one against a lane that
|
||||
had since been restarted and promoted. Lane selection is now derived from the declared mode
|
||||
(`redis-topology & lane-<mode>`) so a promotion test is never selected on a standalone lane and
|
||||
never silently skipped either.
|
||||
|
||||
### The finding: a superseded primary keeps acknowledging writes
|
||||
|
||||
This is the most serious thing this delivery has surfaced, and none of it is in the SDK's code.
|
||||
|
||||
Sentinel promoted the replica at `05:56:12.503` and did not demote the old primary until
|
||||
`05:56:23.529`. For those eleven seconds the client stayed connected to a primary that had already
|
||||
been replaced, wrote, and was told `+OK` **2,086 times**. Every one of those writes was discarded
|
||||
when the old primary resynced from the new one — the server's own log says so:
|
||||
`Partial resynchronization not accepted: Requested offset for second ID was 9897663, but I can reply
|
||||
up to 9731839`. Exactly **one** command failed in the whole run.
|
||||
|
||||
There is no client-side signal for this. The server answered, so the driver recorded a success, the
|
||||
SDK recorded `CONFIRMED_SUCCESS`, and the caller was told the write landed. A second run made the
|
||||
point harder: sixteen thousand attempts, **zero** exceptions, 2,086 acknowledged writes gone.
|
||||
|
||||
`SentinelFailoverObserver` counts *ambiguous* writes and its documentation called those "the ones an
|
||||
operator has to reconcile". That was wrong by three orders of magnitude — the writes that actually
|
||||
needed reconciling were the confirmed ones, and no counter on the client can be made to include
|
||||
them. The class now says so instead of implying it measures something it cannot.
|
||||
|
||||
What closes the window is server-side. Re-running the identical promotion with
|
||||
`min-replicas-to-write 1` and `min-replicas-max-lag 1` cut acknowledged-and-discarded writes from
|
||||
**2,086 to 1**: the orphaned primary refused 2,020 writes with `NOREPLICAS`, which the SDK already
|
||||
translates to a definite, non-ambiguous failure. Both settings are in the lane, and
|
||||
`acknowledgedWriteLossIsBounded` ties the tolerated loss to the configured lag window rather than to
|
||||
a magic number.
|
||||
|
||||
### The assertion immediately caught a second version of the same mistake
|
||||
|
||||
The first run with the setting passed. The second failed, with 2,099 lost writes — because the
|
||||
setting had been written into the `primary` service only. These two nodes swap roles on every
|
||||
failover, so a guardrail applied to whichever one happens to start as primary stops applying the
|
||||
moment the lane does the thing it exists to do. Both data nodes now take their whole configuration
|
||||
from one definition, which makes the asymmetry impossible to reintroduce. Three consecutive
|
||||
promotions in both directions since: 0, 0, and 1 acknowledged write lost.
|
||||
|
||||
### One real translator defect
|
||||
|
||||
The promotion closed the channel under an in-flight `RPUSH` and Lettuce raised a bare
|
||||
`RedisException`, which matched no branch of `LettuceExceptionTranslator` and fell through to a
|
||||
generic failure reported with `ambiguous=false` — that is, as a write that *definitely did not run*.
|
||||
Nothing about an unrecognised failure supports that claim, and a caller who believes it retries a
|
||||
non-idempotent write. The fallback now treats an unclassified write failure as ambiguous, which is
|
||||
the safe direction, and two unit tests pin both branches.
|
||||
|
||||
### Cluster: the arithmetic holds
|
||||
|
||||
`LiveRedisClusterTest` checked `RedisSlotCalculator` against `CLUSTER KEYSLOT` over a corpus built
|
||||
from the brace rules a hand-written implementation gets wrong — `{}`, `a{}b`, `foo{}{bar}`,
|
||||
`foo{{bar}}zap`, `foo{bar}{zap}`, `{`, `}`, `}{`, an unclosed brace, the empty key, and non-ASCII
|
||||
keys. No disagreements, and the result was reproduced independently against the server outside the
|
||||
test. The rendered-key invariant holds too: the slot the SDK computes from a tag alone equals the
|
||||
slot the server computes from the whole rendered key, which is what makes the two-step design sound.
|
||||
|
||||
Cross-slot refusal was checked in both directions, because a guard stricter than the cluster costs
|
||||
availability for nothing and a looser one sends requests that cannot succeed; the pair the guard
|
||||
refuses is the pair the server answers `CROSSSLOT` for. Redirects were observed rather than assumed:
|
||||
a `MOVED` names the slot the client computed, and a slot put into a real `MIGRATING`/`IMPORTING`
|
||||
state answers `ASK` for an absent key and `TRYAGAIN` for a multi-key request that straddles the
|
||||
migration. The lane restores the slot to `STABLE`, so a run leaves the cluster as it found it.
|
||||
|
||||
Nothing in `sdk.cluster` needed changing. That is worth recording as an outcome, not treated as the
|
||||
test having nothing to say: the calculator is the one piece of this SDK that silently degrades into
|
||||
wrong refusals and wrong admissions if it is off by one, and it is now checked rather than assumed.
|
||||
|
||||
### Where this leaves the task
|
||||
|
||||
| | |
|
||||
| --- | --- |
|
||||
| Unit | 288 tests, 0 failures |
|
||||
| Standalone lane | 14 tests, 0 failures |
|
||||
| Sentinel lane | 8 tests, 0 failures, three promotions in both directions |
|
||||
| Cluster lane | 14 tests, 0 failures |
|
||||
| `check` + architecture/env/public-path | green for `adapter:outbound:cache-redis` |
|
||||
| `verify-gate-matrix.sh` | 29 gates, 27 verified, 2 delegated-pending, OK |
|
||||
|
||||
Defects found and fixed across the whole evidence effort: six in the ACL accounts, two in the ACL
|
||||
test itself, one in the scan budget, four in the topology harness, one in the Sentinel lane's
|
||||
configuration, one in the exception translator, and one documentation claim that was wrong by three
|
||||
orders of magnitude.
|
||||
|
||||
`:app-bootstrap:test --tests '*CleanArchitectureTest'` passes. It briefly did not, on
|
||||
`NO_UUID_RANDOM_IN_CONTROLLER` in `application.fileserver.cleanup.CleanupItem` — untracked
|
||||
in-progress work from a different feature that was being edited while this evidence ran. The
|
||||
identifier factories have since moved to `CleanupRequest` and no direct `UUID.randomUUID` or
|
||||
`UuidCreator` call remains in `application-core`, so the rule is satisfied by the current sources
|
||||
rather than waived.
|
||||
|
||||
**Task 20 is the only implementation task left.**
|
||||
|
||||
## 24. Task 20 — the fixture had to learn to defer before the contract meant anything
|
||||
|
||||
Task 20 was deferred back at section 15 for a reason that turned out to be the whole task: the
|
||||
in-memory fixture executes every command the moment it is called, so a transaction written against
|
||||
it would have passed while proving the opposite of what it claimed. The writes would already have
|
||||
happened before the commit, and a watch conflict would have had nothing left to discard.
|
||||
|
||||
The controller chose the full option — every command available inside the window, and the fixture
|
||||
reworked to match — over a narrow hand-picked subset.
|
||||
|
||||
### Deferral is one property, not a hundred and eleven
|
||||
|
||||
`RedisCommandGateway` has 111 methods and every one of them returns a `CompletionStage`. That is not
|
||||
incidental: deferral is a property of the *connection*, so it can be implemented once rather than
|
||||
per command.
|
||||
|
||||
On the production side it costs nothing at all. Lettuce already defers everything issued after
|
||||
`MULTI` and completes those futures from the `EXEC` reply, so `LettuceRedisCommandGateway` needed no
|
||||
change to any existing method — only the five new seam methods (`watch`, `unwatch`,
|
||||
`beginTransaction`, `commitTransaction`, `discardTransaction`). `commitTransaction` returns a
|
||||
boolean rather than a list of results, because the per-command stages resolve themselves and the
|
||||
only thing `EXEC` alone can say is whether it ran.
|
||||
|
||||
On the test side, `DeferringRedisCommandGateway` is a `java.lang.reflect.Proxy` that records an
|
||||
invocation, hands back an unfinished future, and replays it against the fixture at commit — which is
|
||||
exactly when Redis runs it. The 1,996-line fixture was not edited for it. The consequence that
|
||||
matters: a command added to the seam later cannot forget to be transactional.
|
||||
|
||||
The one part that does need the data is the watch check, so that lives in the fixture. It hashes the
|
||||
watched key's current contents rather than incrementing a counter at each of the sixteen mutation
|
||||
sites — a counter is something a seventeenth mutation can silently fail to update, and a hash is not.
|
||||
|
||||
### What the contract refuses to let a caller do
|
||||
|
||||
`QueuedReply.value()` throws before the commit. The alternative — returning `null` or a zero for a
|
||||
command the server has only answered `+QUEUED` to — is the trap the type exists to remove.
|
||||
|
||||
`TransactionResult` reports exactly two outcomes, "executed" and "a watched key changed so nothing
|
||||
ran", and neither is a rollback. Redis has none: a command that fails at runtime inside `EXEC` does
|
||||
not undo the ones around it, and the proxy reproduces that faithfully by failing one future and
|
||||
leaving the rest alone.
|
||||
|
||||
`RedisTransactionQueue` is write-only, which is a contract rather than an unfinished surface. A read
|
||||
inside the window cannot be branched on — its reply does not exist until every command has already
|
||||
been chosen — so accepting one would only offer a way to write code that looks conditional and is
|
||||
not. Reads a transaction depends on belong before it, under `WATCH`.
|
||||
|
||||
Queued commands go through `QueueingRedisCommandExecutor`, which is `SyncRedisCommandExecutor` with
|
||||
the wait removed and *nothing else* changed. The same `CommandPolicyGuard` admits them, so namespace,
|
||||
slot, permit, and budget rules hold identically: a transaction is not a way around the guard, and a
|
||||
test asserts that a foreign-namespace key is refused inside a window exactly as it is outside one.
|
||||
|
||||
### Three defects the tests found
|
||||
|
||||
1. **A callback returning nothing crashed the transaction.** `Optional.of` on a null body result
|
||||
threw an NPE after a perfectly successful commit. A transaction with no interesting return value
|
||||
is entirely normal, so the result now carries an empty value for it and the invariant only forbids
|
||||
a value on a transaction that did not execute.
|
||||
2. **`RedisTransactionQueue.delete` could never succeed.** `DEL` is R2 in the catalog because it
|
||||
accepts any number of keys, so it needs a permit and a budget even when a transaction queues
|
||||
exactly one. The queue presents the SDK's own permit rather than making every caller thread one
|
||||
through for a single-key delete.
|
||||
3. **The first conflict test was contending with itself.** It wrote the watched key through the same
|
||||
gateway — that is, from inside the very window it was supposed to be contending with — so the
|
||||
write was queued rather than applied and the transaction timed out instead of conflicting. A
|
||||
competing writer has to come from another connection, and the test now has one. This is the kind
|
||||
of mistake that would have produced a green test if the fixture had not been deferring.
|
||||
|
||||
| | |
|
||||
| --- | --- |
|
||||
| Unit | 296 tests, 0 failures |
|
||||
| `check` | green for `adapter:outbound:cache-redis` |
|
||||
|
||||
**Every implementation task in the plan is now done.**
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,366 @@
|
||||
# Redis Optionality and Composition Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development
|
||||
> (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use
|
||||
> checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Make Redis genuinely optional at both ends — `APP_REDIS_ENABLED=false` loads, binds,
|
||||
validates and allocates nothing Redis-shaped, and `APP_REDIS_ENABLED=true` assembles a validated,
|
||||
fail-fast Redis runtime — and close the SDK correctness defects that must not be wired live.
|
||||
|
||||
**Architecture:** A single conditional composition root (`RedisSdkAutoConfiguration`) owns
|
||||
`RedisSdkSettings`, its validation, its secret/credential resolution, and its resource loading.
|
||||
Nothing Redis-shaped is registered by the global `@ConfigurationPropertiesScan`. Secret requirements
|
||||
move from the unconditional bootstrap list into that conditional owner. The SDK stays an
|
||||
implementation detail of the `adapter:outbound:cache-redis` leaf; provider-neutral semantic ports
|
||||
are re-implemented on top of it in a later phase.
|
||||
|
||||
**Tech Stack:** Java 21, Spring Boot 4.0.0, Lettuce, Gradle (fail-closed 19-leaf registry), JUnit 5,
|
||||
AssertJ, ArchUnit.
|
||||
|
||||
## Status — 2026-08-10
|
||||
|
||||
| Review item | State | Where |
|
||||
| --- | --- | --- |
|
||||
| P1 #1 optionality (settings/validation half) | done | `RedisSdkAutoConfiguration`, `RedisSdkSettings`, `RedisOptionalityContractTest` |
|
||||
| P1 #1 optionality (client/runtime half) | done | Phase D: `RedisTopologyClientFactory`, `RedisRuntimeOwner`, `RedisStartupProbe`, health contributors |
|
||||
| P1 #2 production Redis secrets | done | `SecretSourceValidator`, `RedisActivationValidator` |
|
||||
| P1 #3 env SSOT for the 34 settings | done | `env-keys.yaml`, `verifyEnvKeys` check E |
|
||||
| P1 #4 semantic adapters | 4 of 5 | rate-limit, lease, idempotency V2, cache done. **Session is blocked, not deferred**: no provider-neutral session contract exists in `application-core` or `shared-contract` — it was deleted with the previous generation and the bootstrap references it only by bean name. Restoring it is a contract design task, not a port implementation, and the review does not specify that contract. |
|
||||
| P1 #5 counter TTL | done | `AtomicCounterScripts` |
|
||||
| P1 #6 transaction slot (aggregate check) | done | `LettuceRedisTransactionOperations.AttemptSlot` |
|
||||
| P1 #6 transaction exclusive connection lease | done | typed `RedisLease` with `invalidate()`; the TRANSACTION lane is bounded and a poisoned connection is never pooled |
|
||||
| P1 #7 telemetry isolation | done | `NoThrowObservationSink`, all three executors |
|
||||
| P1 #8 topology lane fail-closed | done | `cache-redis/build.gradle` |
|
||||
| P1 #9 README three-state split | done | `cache-redis/README.md` |
|
||||
| TLS lane | done | `infra/redis-sdk/tls/compose.yml`, plaintext port off, certificates generated at start-up |
|
||||
| P1 #9 PR/nightly/RC release gates | done | `redis-sdk-topology.yml` PR/schedule/RC matrix + evidence artifacts; gate promoted from `delegated-pending` |
|
||||
| P1 #10 Netty floor | done | `ext['netty.version'] = '4.2.17.Final'`, all lockfiles |
|
||||
| Phase B3 orphan configuration removal | done | 4 blocks removed from `application.yml`, 33 `.env` keys dropped, registry rows deprecated |
|
||||
|
||||
### P2/P3 hardening
|
||||
|
||||
| Item | State | Where |
|
||||
| --- | --- | --- |
|
||||
| Multi-key permit dead branch | done | `CommandPolicyGuard.requirePermits`; set algebra and blocking list now present a multi-key permit |
|
||||
| Codec type safety | done | `RedisCodecRegistry` records the declared type and refuses a mismatched lookup |
|
||||
| Error metadata on decode failure | done | `RedisFailureMetadata.storedDataCorruption`, deployment mode threaded from the caller |
|
||||
| Pub/Sub codec per target | done | per-channel codec map; pattern subscriptions must agree on one codec |
|
||||
| Pub/Sub backpressure | done | `SubscriptionFlux` bounded buffer + explicit overflow policy, decode failure terminates |
|
||||
| Admin `CONFIG GET` | done | fixed allowlisted projection, secret-shaped values redacted, no caller pattern |
|
||||
| Reply budget | done (consolidated) | dead `CommandPolicyGuard.validateReply` removed; `RedisOperationContext.requireReplyWithinBudget` is the single authority |
|
||||
| Sentinel durability probe | done | `min-replicas-max-lag` now required alongside the replica count |
|
||||
| Missing raw allowlist resource | done | `RedisSdkAutoConfiguration` opens it at startup |
|
||||
| ACL fixture | done | `user default off`, fixture-only header, named-credential instructions |
|
||||
| Readiness false-green | done | `validate-group-membership: true`, group names only contributors that exist |
|
||||
| Dependency drift | done | unused `spring-data-redis`/`micrometer-core` removed, Reactor declared directly |
|
||||
| JSON framing | done | control characters escaped, schema identifier constrained by regex |
|
||||
| Connection lifecycle state machine | done | `RedisRuntimeOwner` `OPEN→DRAINING→CLOSED` |
|
||||
| Gateway/`CommandRequest` visibility | **open** | needs `sdk.programmability`, `sdk.raw`, `sdk.admin` and `sdk.extensions` to stop constructing requests directly; a package restructuring, not a rename |
|
||||
| Raw movable keys (`SORT BY/GET/STORE`) | done | `RawMovableKeys` settles SORT/SORT_RO locally including the STORE destination; BY/GET stay refused because their patterns cannot be namespace-checked, and an unknown option is a rejection rather than a guess |
|
||||
| Batch observed-aggregate reply bytes | done | `BatchExecution` accumulates measured replies and fails the item that crosses the ceiling |
|
||||
|
||||
Residual limitation on P1 #6: keys queued inside the callback are only knowable after `MULTI`, so
|
||||
the aggregate slot is enforced as each key becomes known — the offending command is refused before
|
||||
it is written and the window is discarded, rather than the whole attempt being refused before
|
||||
`WATCH`. Refusing before `WATCH` in every case needs a declared-keys transaction API, which Phase E
|
||||
would revisit anyway.
|
||||
|
||||
## Global Constraints
|
||||
|
||||
- Registry SSOT for module identity, Gradle paths and allowed edges is
|
||||
`src/config/architecture/modules.json`. Never infer a Gradle path.
|
||||
- Commit policy is `human-only`. Agents do not stage, commit, amend, or push.
|
||||
- `domain-core` must stay free of framework/transport/database/cloud dependencies.
|
||||
- `application-core` must never see an SDK type, a Redis key, a topology or a connection type.
|
||||
- Global Redis activation is exactly one switch: `APP_REDIS_ENABLED`. `APP_CACHE_REDIS_ENABLED`
|
||||
must not be a second master switch.
|
||||
- Every new `APP_*` key must land in all four places or `verifyEnvKeys` fails:
|
||||
`src/app-bootstrap/src/main/resources/application.yml`, `src/.env`,
|
||||
`docs/registries/env-keys.yaml`, and (when secret-classified)
|
||||
`docs/registries/secrets-classification.yaml`.
|
||||
- `SecretsClassificationRegistryTest` asserts `SecretSourceValidator.REQUIRED_PROD_SECRETS` matches
|
||||
`docs/registries/secrets-classification.yaml` 1:1. Changing one requires changing the other.
|
||||
- Netty floor: `4.2.16` or higher (CVE-2026-42577 epoll `<4.2.13`, CVE-2026-59901
|
||||
codec-compression `<4.2.16`).
|
||||
- Topology lane modes allowlist: exactly `STANDALONE`, `SENTINEL`, `CLUSTER`.
|
||||
- Verification commands run from `src/`.
|
||||
|
||||
## Current-state facts this plan is written against
|
||||
|
||||
Established by direct inspection on 2026-08-10, working tree (not HEAD):
|
||||
|
||||
- `CaSkeletonApplication` scans `dev.caskeleton.adapter` for `@ConfigurationProperties`, so
|
||||
`RedisSdkSettings` (`ca-skeleton.capabilities.redis-sdk`) is registered with Redis off.
|
||||
- `RedisSdkSettings.validate()` has no production caller.
|
||||
- The `cache-redis` leaf has **no** `@Bean`, `@Configuration`, or `@AutoConfiguration` in main
|
||||
source: nothing constructs a client, connection, gateway, or health contributor.
|
||||
- 240 tracked main-source files under `cache-redis` are deleted in the working tree; the SDK
|
||||
(~300 files under `…cache.redis.sdk`) is untracked. The semantic cache/session/idempotency/
|
||||
rate-limit/lease adapters are gone.
|
||||
- `ca-skeleton.providers.redis.*`, `ca-skeleton.capabilities.cache.*`, and
|
||||
`ca-skeleton.security.redis-session.*` in `application.yml` bind to **no** Java type — orphan
|
||||
configuration from the previous generation.
|
||||
- `SecretSourceValidator.REQUIRED_PROD_SECRETS` requires `APP_CACHE_REDIS_PASSWORD` and
|
||||
`APP_CACHE_REDIS_KEY_HMAC_SECRET` unconditionally in prod; the other Redis roles have
|
||||
conditional skips.
|
||||
- `verifyEnvKeys` compares only the three text sets (`.env`, `application.yml` placeholders,
|
||||
`env-keys.yaml`); it never reads `spring-configuration-metadata.json`, so a typed property with
|
||||
no env name passes.
|
||||
- `redisTopologyTest` builds its tag as `lane-${declaredMode}` from an unvalidated project
|
||||
property, with no mode allowlist and no positive test-count postcondition — an unknown mode
|
||||
selects zero tests and exits 0.
|
||||
- `src/app-bootstrap/gradle.lockfile` pins `io.netty:*:4.2.7.Final` on
|
||||
`productionRuntimeClasspath`, and still carries a `redisCompositionTestRuntimeClasspath`
|
||||
configuration whose source set no longer exists.
|
||||
|
||||
---
|
||||
|
||||
## Phase A — Redis optionality (P1 #1, #2) and the dead second switch
|
||||
|
||||
### Task A1: Remove the unconditional production Redis secret requirement
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/runtime/SecretSourceValidator.java`
|
||||
- Modify: `docs/registries/secrets-classification.yaml`
|
||||
- Test: `src/app-bootstrap/src/test/java/dev/caskeleton/bootstrap/runtime/SecretSourceValidatorTest.java`
|
||||
|
||||
**Interfaces:**
|
||||
- Produces: `SecretSourceValidator.REQUIRED_PROD_SECRETS` without any `APP_CACHE_REDIS_*` entry;
|
||||
`isCacheRedisMaterial(String)` + `isRedisGloballyEnabled()` private helpers gating every
|
||||
remaining Redis-prefixed secret on `app.redis.enabled`.
|
||||
|
||||
- [ ] **Step 1: Write the failing test** — prod profile, Redis off, no Redis secrets present,
|
||||
validator must not throw.
|
||||
- [ ] **Step 2: Run it and watch it fail** on the two cache secrets.
|
||||
- [ ] **Step 3: Gate every Redis secret on `app.redis.enabled` plus its role selector.**
|
||||
- [ ] **Step 4: Re-run the focused test class.**
|
||||
- [ ] **Step 5: Update `secrets-classification.yaml` `required_in_prod` metadata to match.**
|
||||
|
||||
### Task A2: Stop the global scan from registering `RedisSdkSettings`
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/CaSkeletonApplication.java`
|
||||
(exclude the SDK config package) **or** move `RedisSdkSettings` out of a scanned package —
|
||||
preferred: keep the class where it is and drop `@ConfigurationProperties` from it, binding it
|
||||
instead from the conditional configuration with `@ConfigurationProperties` on the `@Bean` method.
|
||||
- Test: new bootstrap contract test asserting zero `RedisSdkSettings` beans when
|
||||
`app.redis.enabled` is absent or false.
|
||||
|
||||
### Task A3: `RedisSdkAutoConfiguration` — the ON/OFF composition root
|
||||
|
||||
**Files:**
|
||||
- Create: `src/adapter/outbound/cache-redis/src/main/java/dev/caskeleton/adapter/outbound/cache/redis/sdk/config/RedisSdkAutoConfiguration.java`
|
||||
- Create: `src/adapter/outbound/cache-redis/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports`
|
||||
- Test: `…/sdk/config/RedisSdkAutoConfigurationTest.java` (ApplicationContextRunner)
|
||||
|
||||
Conditions: `@ConditionalOnProperty(prefix = "app.redis", name = "enabled", havingValue = "true")`.
|
||||
Inside: bind settings, call `validate()` and fail the context on `IllegalStateException`, log
|
||||
warnings, then (Phase D) build the topology client.
|
||||
|
||||
### Task A4: Retire `APP_CACHE_REDIS_ENABLED` as a second master switch
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/app-bootstrap/src/main/resources/application.yml` (add `app.redis.enabled`)
|
||||
- Modify: `src/.env`, `docs/registries/env-keys.yaml`
|
||||
|
||||
---
|
||||
|
||||
## Phase B — env SSOT migration (P1 #3)
|
||||
|
||||
### Task B1: Register `APP_REDIS_ENABLED` and the 34 SDK settings
|
||||
|
||||
Names are fixed by the review's env contract table. Each `env-keys.yaml` row carries
|
||||
`property`, `owner_module`, `type`, `default`, `secret`, `required_when`, and (where one exists)
|
||||
`deprecated_alias` + `removal_deadline`.
|
||||
|
||||
### Task B2: Extend `verifyEnvKeys` to read `spring-configuration-metadata.json`
|
||||
|
||||
Bidirectional: a typed `app.redis.*` property with no registry row fails; a registry row whose
|
||||
`property` matches no metadata entry fails.
|
||||
|
||||
### Task B3: Remove the orphan generations
|
||||
|
||||
Delete `ca-skeleton.providers.redis.*`, `ca-skeleton.capabilities.cache.*`, and
|
||||
`ca-skeleton.security.redis-session.*` from `application.yml` once a migration table records the
|
||||
old→new mapping; drop the now-orphaned `.env` keys; mark the registry rows deprecated rather than
|
||||
deleting their metadata.
|
||||
|
||||
---
|
||||
|
||||
## Phase C — SDK correctness (P1 #5, #6, #7)
|
||||
|
||||
### Task C1: Atomic counter must not add a TTL to a pre-existing persistent key
|
||||
|
||||
**Files:**
|
||||
- Modify: `…/sdk/lettuce/operations/AtomicCounterScripts.java`
|
||||
- Test: `…/sdk/lettuce/operations/AtomicCounterScriptsTest.java`
|
||||
|
||||
Both scripts must record existence **before** the increment and apply the initial expiry only when
|
||||
the key was absent:
|
||||
|
||||
```lua
|
||||
local existed = redis.call('EXISTS', KEYS[1])
|
||||
local value = redis.call('INCRBY', KEYS[1], ARGV[1])
|
||||
if existed == 0 then
|
||||
if ARGV[3] == 'AT' then
|
||||
redis.call('PEXPIREAT', KEYS[1], ARGV[2])
|
||||
else
|
||||
redis.call('PEXPIRE', KEYS[1], ARGV[2])
|
||||
end
|
||||
end
|
||||
return value
|
||||
```
|
||||
|
||||
### Task C2: Validate the transaction's whole key set against one slot
|
||||
|
||||
**Files:**
|
||||
- Modify: `…/sdk/programmability/LettuceRedisTransactionOperations.java`
|
||||
- Test: `…/sdk/programmability/LettuceRedisTransactionOperationsTest.java`
|
||||
|
||||
Collect watched + queued keys per attempt and validate the aggregate slot before `MULTI`, instead
|
||||
of validating the WATCH bundle and each queued write independently.
|
||||
|
||||
### Task C3: A throwing observation sink must not fail a successful command
|
||||
|
||||
**Files:**
|
||||
- Create: `…/sdk/lettuce/observability/NoThrowObservationSink.java`
|
||||
- Modify: `…/sdk/lettuce/command/SyncRedisCommandExecutor.java`
|
||||
- Modify: `…/sdk/lettuce/command/ReactiveRedisCommandExecutor.java`
|
||||
- Test: `…/sdk/lettuce/command/ObservationIsolationTest.java`
|
||||
|
||||
---
|
||||
|
||||
## Phase D — Runtime composition (P1 #4 prerequisite, deferred)
|
||||
|
||||
Topology strategy (standalone/sentinel/cluster), authentication/TLS, shared vs dedicated
|
||||
connection lanes, lifecycle owner, capability/durability probe, health contributors.
|
||||
|
||||
## Phase E — Semantic adapter restoration (P1 #4, deferred)
|
||||
|
||||
Re-implement the provider-neutral ports on top of the SDK: cache, session, idempotency V2,
|
||||
rate-limit, efficiency-only lease. This is the restoration of the 240 deleted files' behaviour and
|
||||
is the largest single body of work in this plan.
|
||||
|
||||
## Phase F — Release gates, evidence and dependencies (P1 #8, #9, #10)
|
||||
|
||||
### Task F1: `redisTopologyTest` fails closed
|
||||
|
||||
Mode allowlist, `failOnNoDiscoveredTests = true`, per-lane required tag/class presence, and a
|
||||
`>= 1` executed-test postcondition.
|
||||
|
||||
### Task F2: Netty floor `4.2.16`
|
||||
|
||||
Add a platform constraint, regenerate every lockfile, rerun the dependency scan.
|
||||
|
||||
### Task F3: README status split
|
||||
|
||||
`API implemented` / `Spring composition implemented` / `production-qualified` as three separate
|
||||
states.
|
||||
|
||||
## Phase G — P2/P3 hardening (deferred)
|
||||
|
||||
Gateway/request visibility, multi-key permit dead branch, connection lifecycle state machine,
|
||||
reply budgets, admin `CONFIG GET` projection, pub/sub codec mapping and backpressure, codec type
|
||||
safety, error metadata, raw movable keys, Sentinel durability probe, ACL fixture, readiness
|
||||
false-green, missing raw resource, dependency drift, JSON framing.
|
||||
|
||||
---
|
||||
|
||||
## Round 2 — the defects a real server found that this plan did not
|
||||
|
||||
Everything above was written before any of it had run against Redis. A second review started four
|
||||
Docker lanes, wired the production code to them, and found that several items marked done were
|
||||
done in the sense that the code existed, not in the sense that it worked. What follows is what that
|
||||
round changed, and what it changed because of.
|
||||
|
||||
### The readiness group could not start at all
|
||||
|
||||
`management.endpoint.health.group.readiness.include` named `redisRequired`, a contributor that only
|
||||
exists when a correctness role selected Redis. Boot validates group membership and does **not**
|
||||
tolerate a conditional member being absent, so every Redis-off and cache-only deployment failed at
|
||||
startup with `Included health contributor 'redisRequired' in group 'readiness' does not exist`. The
|
||||
comment in `application.yml` asserted the opposite.
|
||||
|
||||
The group now names only unconditional contributors, and
|
||||
`RedisReadinessGroupPostProcessor` appends `redisRequired` from `RedisCorrectnessRoles` — the same
|
||||
predicate the bean's `@Conditional` asks, so membership and existence cannot drift.
|
||||
`RedisReadinessGroupPostProcessorTest` boots a real Actuator context in each of the three shapes;
|
||||
putting the name back in the shipped file makes two of them fail exactly as production did.
|
||||
|
||||
### Redis on composed no capability
|
||||
|
||||
`APP_REDIS_ENABLED=true` produced a client, an owner and a health contributor. Every semantic port
|
||||
count was zero, so a deployment that selected `redis` for its rate limiter started, reported
|
||||
healthy, and had no rate limiter. `RedisCapabilityConfig` composes cache, rate limit, lease and the
|
||||
owner-safe idempotency store, each on its own selector.
|
||||
|
||||
The idempotency guard was also counting `application.idempotency.IdempotencyStorePortV2`, which no
|
||||
provider implements — the implemented contract is the one in `…idempotency.v2`. Selecting `redis`
|
||||
therefore required a bean nothing could supply. Driving the V2 store from an executor remains
|
||||
outstanding and is named as such rather than covered by a guard that cannot see it.
|
||||
|
||||
### Four key prefixes, and an ACL that matched none of them
|
||||
|
||||
Each capability joined its own `namespace-application` / `namespace-environment` pair in its own
|
||||
order, so the cache wrote `ca-skeleton:prod:…` while the ACL granted `~prod:*`. `CapabilityKeyspace`
|
||||
renders every capability below one `RedisNamespace`, and the per-capability namespace keys are
|
||||
deprecated.
|
||||
|
||||
The scripted capabilities also ran `EVALSHA` on the application account, which does not have it.
|
||||
Lanes now carry a `RedisCredentialRole`; the topology factory builds one client per configured
|
||||
role, so the `SCRIPT` lane authenticates as the advanced account and the account that reads a cache
|
||||
entry still cannot execute a script. `LiveRedisSemanticPortsTest` proves both directions against a
|
||||
real server.
|
||||
|
||||
### Cluster transactions were impossible, and multi-key WATCH was refused
|
||||
|
||||
`beginTransaction()` on a live cluster failed by design: every lane opened the slot-routing
|
||||
connection, which cannot own a window. `RedisTransactionRunner` derives a routing key and pins the
|
||||
lane to the node that owns the slot. Fixing that surfaced a second defect a cluster was not needed
|
||||
for — `watch()` presented no multi-key permit, so watching more than one key was rejected
|
||||
unconditionally, which is most optimistic transactions.
|
||||
|
||||
### The fixtures could not fail
|
||||
|
||||
Every ACL account was `nopass`, which accepts any password: every assertion about authentication
|
||||
passed for the same reason a typo would have. The accounts carry real passwords and a wrong one is
|
||||
now asserted to produce `WRONGPASS`. The cluster lane's readiness helper checked
|
||||
`CLUSTER INFO` unauthenticated, so it never matched, never exited, and `up --wait` returned while
|
||||
slots were still being assigned; a `ready` gate now blocks on `cluster_state:ok`.
|
||||
|
||||
### TLS was reachable only by hand
|
||||
|
||||
`tls` is a lane of `redisTopologyTest` and of the CI matrix. Trust material resolved with
|
||||
`new File(...)` broke `classpath:` references, and resolving it purely through the resource loader
|
||||
breaks mounted paths — both shapes are ordinary, and both are supported.
|
||||
|
||||
### Gates that could report success for a lane they did not run
|
||||
|
||||
`afterTest` fires for skipped tests too, so the "ran something" check could be satisfied by a run
|
||||
that skipped everything. Lanes now declare the classes they exist to run and a floor for the
|
||||
executed count, and a skipped test fails the run. `verifyEnvKeys` gained a check for registered
|
||||
keys that nothing reads — no typed property, no yaml reference, no `.env` entry, no Java consumer —
|
||||
which found eight orphaned Redis keys beyond the two the review named.
|
||||
|
||||
### Verified
|
||||
|
||||
| Lane | Result |
|
||||
| --- | --- |
|
||||
| standalone | 25 tests |
|
||||
| sentinel | 27 tests |
|
||||
| cluster | 29 tests, including a same-slot transaction and a cross-slot refusal |
|
||||
| tls | 4 tests, filesystem and classpath CA |
|
||||
|
||||
Repository: 3594 tests, 0 failures. `verifyCleanArchitectureDependencies`,
|
||||
`verifyPublicPathSnapshot`, `verifyEnvKeys`, `CleanArchitectureTest`, `verify-gate-matrix.sh`
|
||||
(37 gates) and `verify-gradle-wrapper.sh` all pass.
|
||||
|
||||
### Still open
|
||||
|
||||
- **Session port.** No provider-neutral session contract exists in `application-core` or
|
||||
`shared-contract`; it went with the previous generation. That is a contract to design, not a port
|
||||
to implement, and inventing one here would be guessing at its shape.
|
||||
- **V2 idempotency executor.** `IdempotencyExecutorV2` targets a contract no provider implements.
|
||||
- **Gateway / `CommandRequest` visibility.** Narrowing it is a package restructuring across
|
||||
`sdk.programmability`, `sdk.raw`, `sdk.admin` and `sdk.extensions`, not an access-modifier change.
|
||||
Reference in New Issue
Block a user