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.
|
||||
@@ -0,0 +1,191 @@
|
||||
> **SUPERSEDED — HISTORICAL PROVENANCE ONLY (2026-07-25):** The user-approved harness-free
|
||||
> Mode B amendment supersedes this design. Retain the body as historical provenance; it is not
|
||||
> executable instruction.
|
||||
|
||||
# Harness Policy Engine Refactoring Design
|
||||
|
||||
- **Date:** 2026-07-20
|
||||
- **Status:** Approved by user request
|
||||
- **Scope:** repository-local development harness (`.harness`, `.agents`, `.claude`, `.codex`, root/module guidance, Gradle module registry integration)
|
||||
- **Source:** user-provided “개발 하네스 분석·리뷰” plus repository evidence gathered on 2026-07-20
|
||||
|
||||
## 1. Problem Statement
|
||||
|
||||
The repository now has 19 nested Gradle leaf modules, but the write-time import gate,
|
||||
agent prompts, runner allowlist, and root guidance still contain parts of the previous flat
|
||||
module topology. Platform variants are copied manually, so commit policy and orchestration
|
||||
already differ between Claude, Codex, and Antigravity. Verdict validation checks a text
|
||||
summary but does not consistently require enum fields, non-negative counts, or arithmetic
|
||||
balance.
|
||||
|
||||
The harness must move from duplicated platform prompts to a small policy engine with one
|
||||
project manifest, deterministic renderers, strict validators, and platform adapters.
|
||||
|
||||
## 2. Goals
|
||||
|
||||
1. Make the actual nested Gradle topology a single machine-readable source of truth.
|
||||
2. Resolve a touched file to its nearest owning leaf module without assuming `src/<module>`.
|
||||
3. Generate write-time import policy and focused Gradle task validation from that registry.
|
||||
4. Validate machine verdicts with required fields, non-negative integers, arithmetic rules,
|
||||
upstream evidence, revision identity, and TDD red evidence for behavior changes.
|
||||
5. Materialize validated evidence as JSON artifacts that platform hooks can share.
|
||||
6. Render Claude, Codex, and Antigravity agent variants from one canonical source and fail
|
||||
parity checks when generated files drift.
|
||||
7. Use one human-only commit policy on every platform.
|
||||
8. Replace file-count and exhaustive-report rules with risk and review profiles.
|
||||
9. Add mutation and cross-platform static parity tests.
|
||||
|
||||
## 3. Non-Goals
|
||||
|
||||
- This change does not run authenticated end-to-end golden tasks inside all three external
|
||||
products. It supplies the deterministic fixtures and validators those runs will consume.
|
||||
- It does not add application features or alter production Java behavior.
|
||||
- It does not require PyYAML, jsonschema, Pydantic, or another runtime dependency. Harness
|
||||
data files use JSON syntax, which is valid YAML, and validators use Python stdlib only.
|
||||
- It does not make natural-language agent self-reports authoritative. Hooks convert accepted
|
||||
reports into evidence artifacts; validators remain authoritative.
|
||||
|
||||
## 4. Architecture
|
||||
|
||||
```text
|
||||
.harness/project/modules.yaml ──┬── Gradle settings/includes
|
||||
├── Gradle dependency verification
|
||||
├── owning-module resolver
|
||||
├── import gate
|
||||
└── Gradle command validator
|
||||
|
||||
.harness/agents/*.md + platforms.yaml
|
||||
└── render_agents.py
|
||||
├── .claude/agents/*.md
|
||||
├── .codex/agents/*.toml
|
||||
└── .agents/agents/*/agent.json
|
||||
|
||||
Claude hook ───────────────┐
|
||||
Antigravity hook adapter ──┼── verdict validator ── evidence JSON
|
||||
Codex validation command ──┘
|
||||
```
|
||||
|
||||
### 4.1 Project registry
|
||||
|
||||
`modules.yaml` contains, per leaf module:
|
||||
|
||||
- stable module id
|
||||
- repository-relative source path
|
||||
- Gradle path
|
||||
- role
|
||||
- Java package roots (informational and import-policy lookup only)
|
||||
- allowed project dependencies
|
||||
- focused test command
|
||||
- profiles/capabilities
|
||||
- owning `CLAUDE.md` when present
|
||||
- an intentional mutation import used by gate tests
|
||||
|
||||
`src/settings.gradle` reads the registry to declare projects. The
|
||||
`verifyCleanArchitectureDependencies` task reads the same registry instead of maintaining a
|
||||
second dependency map.
|
||||
|
||||
### 4.2 Owning-module resolution
|
||||
|
||||
Owner selection uses the longest filesystem-boundary match among registered leaf source
|
||||
paths. Package prefixes never decide ownership because `support` owns a broad
|
||||
`dev.caskeleton.adapter.outbound` package and sample code mirrors production packages.
|
||||
|
||||
Instruction discovery walks upward from the touched file and returns the nearest
|
||||
`CLAUDE.md`; if a leaf has none, root `CLAUDE.md` and `AGENTS.md` are the explicit fallback.
|
||||
|
||||
### 4.3 Import gate
|
||||
|
||||
The import gate first resolves the registered leaf module, then applies:
|
||||
|
||||
- dependency-derived sibling module isolation
|
||||
- role-specific framework rules for domain, application, inbound, outbound, persistence,
|
||||
identifier, shared-contract, bootstrap, and sample roles
|
||||
- global unsafe-pattern checks
|
||||
|
||||
All registered production modules receive a mutation test using their real nested source
|
||||
path. Sample-only exemptions are explicit registry data, not accidental regex misses.
|
||||
|
||||
### 4.4 Verdict and evidence
|
||||
|
||||
The canonical verdict schema requires `agent`, `verdict`, `task_id`, `revision`, and agent
|
||||
specific evidence. Non-blocked verdicts require every declared field. Counts are non-negative.
|
||||
Required equations include:
|
||||
|
||||
- spec totals balance
|
||||
- Gradle `run = passed + failed + skipped`
|
||||
- ready Gradle results include at least one command and no failed command
|
||||
- behavior-changing implementation requires at least one observed red test
|
||||
- quality-ready references validated architecture and spec artifacts
|
||||
|
||||
Claude's fenced `ca-verdict` remains a compatibility input, but its accepted form is converted
|
||||
to the same JSON evidence model. Missing or malformed payloads for detected CA agents fail
|
||||
closed. Evidence records include a source-message hash and current revision/diff identity.
|
||||
|
||||
### 4.5 Platform rendering and hook adapters
|
||||
|
||||
Canonical agent Markdown lives under `.harness/agents/`; platform metadata lives in
|
||||
`.harness/project/platforms.yaml`. Generated files carry `generated_from`, `source_hash`,
|
||||
`generator_version`, and `do_not_edit` metadata.
|
||||
|
||||
Antigravity gains a plugin `hooks.json` and a platform adapter using the documented camelCase
|
||||
stdin/stdout contract. Claude keeps its native hook entry points but calls the common library.
|
||||
Codex variants instruct the runner/reviewer to invoke the common validation command because
|
||||
the repository has no equivalent local lifecycle-hook registration surface.
|
||||
|
||||
### 4.6 Risk and review profiles
|
||||
|
||||
Risk is determined by change surface, not file count:
|
||||
|
||||
- high: security, migration/schema, public contract, module dependency, architecture rule,
|
||||
transaction/concurrency, CI/deployment
|
||||
- medium: behavior, multiple modules, external integration
|
||||
- low: docs/comments, test fixture, local refactor protected by characterization tests
|
||||
|
||||
Review profiles:
|
||||
|
||||
- `review-lite`: direct diff references; no saved report by default
|
||||
- `review-standard`: verify blocking citations; one report only when risk or findings justify it
|
||||
- `audit-deep`: verify all quotes and persist detailed findings
|
||||
- `regulated`: immutable evidence and full traceability
|
||||
|
||||
Option analysis uses a dependency DAG and at most 3–5 materially distinct alternatives.
|
||||
Counterarguments are required for judgment-dependent findings, not deterministic failures.
|
||||
|
||||
## 5. Commit Policy
|
||||
|
||||
All platforms use `human-only`. Implementers never stage or commit. Reviewers may inspect a
|
||||
working-tree diff before commit or an explicit immutable range after the human commits.
|
||||
|
||||
## 6. Verification Strategy
|
||||
|
||||
1. Stdlib unit tests for registry loading and owner resolution.
|
||||
2. Mutation tests for every registered production module path.
|
||||
3. Strict verdict negative tests: missing fields, negatives, arithmetic imbalance, missing
|
||||
upstream evidence, revision mismatch, and behavior change without red evidence.
|
||||
4. Golden renderer tests and `--check` parity validation.
|
||||
5. JSON validation of generated Antigravity hook and agent files.
|
||||
6. Gradle `projects`, architecture dependency verification, focused ArchUnit test, and full
|
||||
`check` after harness tests pass.
|
||||
|
||||
## 7. Migration and Compatibility
|
||||
|
||||
- Existing fenced verdicts remain parseable only when they satisfy the new required fields.
|
||||
- Generated platform files are overwritten only by the renderer and documented as generated.
|
||||
- Root and module guidance is updated to the registered nested topology.
|
||||
- Actual external cross-platform golden executions remain a follow-up; static parity and seeded
|
||||
mutation coverage become mandatory in this change.
|
||||
|
||||
## 8. Acceptance Criteria
|
||||
|
||||
- A seeded forbidden import under every nested production module is rejected.
|
||||
- No legacy flat adapter path remains in gate tests or agent task allowlists.
|
||||
- `settings.gradle`, dependency verification, import gate, and Gradle runner resolve the same
|
||||
19 leaf modules from `modules.yaml`.
|
||||
- Missing/negative/inconsistent ready verdicts fail validation.
|
||||
- Claude and Antigravity adapters invoke the shared validator; accepted verdicts produce JSON
|
||||
evidence.
|
||||
- Rendering followed by `--check` reports no platform drift.
|
||||
- Agent variants uniformly state human-only commit policy and risk-based orchestration.
|
||||
- `N!` enumeration, all-quote routine grep, file-count report splitting, and unconditional
|
||||
counterargument requirements are absent from active rules.
|
||||
- Harness tests and Gradle checks pass, or every unrun/failing command is reported with risk.
|
||||
@@ -0,0 +1,431 @@
|
||||
# Application Outbox Failure Reporting Refactoring Design
|
||||
|
||||
- **Date:** 2026-07-25
|
||||
- **Status:** Approved
|
||||
- **Scope:** `application-core` outbox failure reporting, its messaging adapter, bootstrap wiring,
|
||||
dependency purity enforcement, tests, and affected module documentation
|
||||
- **Source:** user-requested Clean Architecture refactoring review plus repository evidence gathered
|
||||
on 2026-07-25
|
||||
|
||||
## 1. Problem Statement
|
||||
|
||||
`application-core` declares `org.springframework.boot:spring-boot-starter`, although its production
|
||||
sources use no Spring type or annotation. The only external observability types in the module are
|
||||
`org.slf4j.Logger` and `org.slf4j.LoggerFactory` in
|
||||
`PublishPendingOutboxEventsUseCase`. The broad starter consequently places Boot autoconfiguration,
|
||||
Spring Context/AOP, Micrometer Observation, Logback, Log4j bridges, JUL bridges, and SnakeYAML on a
|
||||
core application classpath for two logging calls.
|
||||
|
||||
This contradicts the module's framework-free design statement and weakens the dependency direction
|
||||
the template is intended to teach. It also hides an important semantic distinction: the two log
|
||||
lines are not arbitrary diagnostic messages. They report confirmed `FAILED` and `DEAD` outbox state
|
||||
transitions that feed operational alerts and runbooks.
|
||||
|
||||
The build currently cannot resolve dependencies or run tests because `src/settings.gradle` fails
|
||||
when `.harness/project/modules.yaml` is absent. Harness registry recovery is therefore a prerequisite
|
||||
for implementation and verification, not part of this refactoring.
|
||||
|
||||
## 2. Evidence
|
||||
|
||||
- `src/application-core/build.gradle:14` declares `spring-boot-starter`.
|
||||
- `src/application-core/src/main/java/dev/caskeleton/application/outbox/PublishPendingOutboxEventsUseCase.java:17-18`
|
||||
imports the only production observability framework types in the module.
|
||||
- The same use case creates an SLF4J logger at lines 38-39 and emits the two failure records at
|
||||
lines 139-165.
|
||||
- `src/application-core/README.md:10-13` says the module is framework-free and accesses
|
||||
infrastructure only through `*Port` interfaces.
|
||||
- `src/application-core/CLAUDE.md:27-30` and `src/application-core/build.gradle:3-5` instead claim
|
||||
the starter is retained for optional `@Service` registration, although application-core contains
|
||||
no Spring stereotype.
|
||||
- The relay is manually constructed and must not be a Spring bean:
|
||||
`src/application-core/README.md:332-337`.
|
||||
- The outbox registry assigns `OUTBOX_PUBLISH_FAILED` and `OUTBOX_DEAD_LETTER` to the infrastructure
|
||||
owner layer and ERROR severity: `docs/registries/error-codes.yaml:724-749`.
|
||||
- The runbooks require structured `error.code`, `event_id`, `event_type`, and `correlation_id`
|
||||
fields: `docs/runbooks/outbox-publish-failed.md:17-27` and
|
||||
`docs/runbooks/outbox-dead-letter.md:17-24`.
|
||||
- The current project-dependency verifier inspects only `ProjectDependency` instances, so it cannot
|
||||
reject an external starter added to a core module: `src/build.gradle:612-619`.
|
||||
|
||||
## 3. Goals
|
||||
|
||||
1. Make `application-core` free of Spring, SLF4J, Logback, Log4j, JUL logging, and Micrometer types
|
||||
and main/test classpath dependencies.
|
||||
2. Express confirmed outbox publication failures as a typed application-owned outbound port.
|
||||
3. Keep report data safe by construction: no payload, idempotency key, arbitrary field map, log
|
||||
level, message template, or framework logger crosses the port.
|
||||
4. Implement structured failure reporting in `adapter:outbound:messaging`.
|
||||
5. Keep `app-bootstrap` limited to final wiring and runtime logging configuration.
|
||||
6. Preserve outbox state-machine behavior, transaction boundaries, per-event continuation, and
|
||||
at-least-once semantics.
|
||||
7. Ensure a reporting backend failure cannot change a persisted `FAILED`/`DEAD` outcome or stop the
|
||||
remaining relay batch.
|
||||
8. Remove the duplicate, misleading fail-open WARN currently emitted by the fail-closed outbox
|
||||
publisher.
|
||||
9. Add source- and dependency-level guardrails that prevent framework observability from returning
|
||||
to application-core.
|
||||
|
||||
## 4. Non-Goals
|
||||
|
||||
- This change does not redesign outbox claiming, FIFO ordering, retry backoff, in-flight recovery,
|
||||
or broker selection.
|
||||
- It does not turn operational failures into domain events or business audit records.
|
||||
- It does not deliver a failure report through the same broker/outbox path; that would recurse when
|
||||
the broker is the failing dependency.
|
||||
- It does not add a generic `LoggerPort`, severity API, string template API, or untyped field map.
|
||||
- It does not add a twentieth module or a general observability adapter family.
|
||||
- It does not recover the missing `.harness` policy registry; the implementation waits for that
|
||||
independently governed recovery.
|
||||
- It does not change public HTTP response contracts.
|
||||
|
||||
## 5. Semantic Classification
|
||||
|
||||
An outbox publish failure is an **application operational event**:
|
||||
|
||||
- the application state machine decides whether the confirmed result is retryable `FAILED` or
|
||||
terminal `DEAD`;
|
||||
- the persistence transition is authoritative;
|
||||
- an infrastructure adapter renders that fact as a structured operational record;
|
||||
- metrics and runbooks consume the result for operations.
|
||||
|
||||
It is not a business audit event. It has no actor/action audit semantics, is not retained as an
|
||||
immutable audit ledger, and must not be used as proof of a business transaction. It is also not a
|
||||
domain event: feeding it into the same outbox publisher would recursively fail.
|
||||
|
||||
## 6. Architecture
|
||||
|
||||
```text
|
||||
PublishPendingOutboxEventsUseCase
|
||||
│
|
||||
├── OutboxStorePort ──────────────> persistence adapter
|
||||
├── OutboxMessagePublishPort ─────> messaging publisher adapter
|
||||
└── OutboxRelayFailureReportPort ─> SLF4J structured reporter adapter
|
||||
(adapter:outbound:messaging)
|
||||
|
||||
app-bootstrap
|
||||
└── injects all three ports when manually constructing the relay use case
|
||||
```
|
||||
|
||||
Dependency direction remains:
|
||||
|
||||
```text
|
||||
app-bootstrap
|
||||
-> adapter:outbound:messaging
|
||||
-> application-core
|
||||
-> shared-contract
|
||||
-> domain-core
|
||||
```
|
||||
|
||||
`application-core` owns the port and safe report value. The messaging module owns the concrete
|
||||
rendering because its local responsibility explicitly includes outbox publication adaptation, it
|
||||
already depends on application/shared contracts, and it already has the SLF4J API. Bootstrap
|
||||
selects and injects the adapter but does not implement the port.
|
||||
|
||||
## 7. Application Contract
|
||||
|
||||
### 7.1 Port
|
||||
|
||||
```java
|
||||
@FunctionalInterface
|
||||
public interface OutboxRelayFailureReportPort {
|
||||
|
||||
/**
|
||||
* Attempts to report a confirmed FAILED or DEAD relay transition.
|
||||
*
|
||||
* <p>The implementation must not throw. The report is operational evidence, while the persisted
|
||||
* outbox state and returned relay outcome remain authoritative.
|
||||
*/
|
||||
void report(OutboxRelayFailureReport report);
|
||||
}
|
||||
```
|
||||
|
||||
The use case also contains a defensive non-throwing invocation boundary. This makes the invariant
|
||||
explicit even if a custom implementation violates the port contract.
|
||||
|
||||
### 7.2 Safe immutable report
|
||||
|
||||
```java
|
||||
public record OutboxRelayFailureReport(
|
||||
OperationalError code,
|
||||
String eventId,
|
||||
String eventType,
|
||||
String aggregateId,
|
||||
String correlationId,
|
||||
int attemptCount,
|
||||
Instant nextAttemptAt,
|
||||
RuntimeException cause) {
|
||||
|
||||
public static OutboxRelayFailureReport retryableFailure(
|
||||
String eventId,
|
||||
String eventType,
|
||||
String aggregateId,
|
||||
String correlationId,
|
||||
int attemptCount,
|
||||
Instant nextAttemptAt,
|
||||
RuntimeException cause);
|
||||
|
||||
public static OutboxRelayFailureReport deadLetter(
|
||||
String eventId,
|
||||
String eventType,
|
||||
String aggregateId,
|
||||
String correlationId,
|
||||
int attemptCount,
|
||||
RuntimeException cause);
|
||||
}
|
||||
```
|
||||
|
||||
Invariants:
|
||||
|
||||
- `code` is exactly `OUTBOX_PUBLISH_FAILED` or `OUTBOX_DEAD_LETTER`.
|
||||
- identifiers and type names are non-null and non-blank.
|
||||
- `attemptCount` is at least one.
|
||||
- `nextAttemptAt` is required for `OUTBOX_PUBLISH_FAILED` and absent for
|
||||
`OUTBOX_DEAD_LETTER`.
|
||||
- `cause` is required.
|
||||
- the record has no `payload`, `idempotencyKey`, logger, severity, template, or arbitrary map.
|
||||
|
||||
Static factories remove invalid combinations from ordinary call sites. The record deliberately
|
||||
accepts an allowlisted set of safe operational metadata plus the original cause rather than an
|
||||
`OutboxEvent`, whose full shape includes payload and idempotency data.
|
||||
|
||||
## 8. Relay Flow and Failure Semantics
|
||||
|
||||
The report attempt happens only after the corresponding write transaction succeeds:
|
||||
|
||||
```text
|
||||
publish throws
|
||||
├── retry remains
|
||||
│ ├── inWrite(markFailed) succeeds
|
||||
│ ├── attempt OUTBOX_PUBLISH_FAILED report
|
||||
│ └── return FAILED
|
||||
└── attempts exhausted
|
||||
├── inWrite(markDead) succeeds
|
||||
├── attempt OUTBOX_DEAD_LETTER report
|
||||
└── return DEAD
|
||||
```
|
||||
|
||||
Behavior matrix:
|
||||
|
||||
| Situation | Persistence | Report | Relay behavior |
|
||||
|---|---|---|---|
|
||||
| Publish succeeds, `markPublished` succeeds | `PUBLISHED` | none | return `PUBLISHED` |
|
||||
| Publish succeeds, `markPublished` fails | remains recoverable `IN_FLIGHT` | none | propagate store failure; scheduler retries later |
|
||||
| Publish fails, `markFailed` succeeds | `FAILED` | attempt retryable report | return `FAILED`; continue batch |
|
||||
| Publish fails, `markDead` succeeds | `DEAD` | attempt dead-letter report | return `DEAD`; continue batch |
|
||||
| Publish fails, status transition fails | no confirmed FAILED/DEAD transition | none | propagate store failure; do not emit a false report |
|
||||
| Reporter violates its contract and throws | already `FAILED` or `DEAD` | attempted | contain reporter exception; preserve outcome and continue batch |
|
||||
|
||||
The report is mandatory as an **attempt** after every confirmed failure transition. A production
|
||||
NOOP binding is forbidden. No logging system can guarantee durable emission, so metrics and
|
||||
persistence remain independent evidence when the logging backend itself is impaired.
|
||||
|
||||
## 9. Structured Logging Contract
|
||||
|
||||
The messaging adapter emits ERROR through SLF4J 2's fluent key-value API. The runtime Logstash
|
||||
encoder serializes key-value pairs as top-level JSON fields, while the message also carries a short
|
||||
safe summary for local pattern output.
|
||||
|
||||
Common fields:
|
||||
|
||||
- `error.code`
|
||||
- `error.category`
|
||||
- `dependency_name`
|
||||
- `dependency_type=messaging`
|
||||
- `outcome`
|
||||
- `event_id`
|
||||
- `event_type`
|
||||
- `aggregate_id`
|
||||
- `correlation_id`
|
||||
- `attempt_count`
|
||||
- `runbook_link`
|
||||
|
||||
Retry-only field:
|
||||
|
||||
- `next_attempt_at`
|
||||
|
||||
Mappings:
|
||||
|
||||
| Code | Outcome | Runbook |
|
||||
|---|---|---|
|
||||
| `OUTBOX_PUBLISH_FAILED` | `FAILED` | `runbook://outbox/publish-failed` |
|
||||
| `OUTBOX_DEAD_LETTER` | `DEAD` | `runbook://outbox/dead-letter` |
|
||||
|
||||
The throwable is attached as the log cause. Payload, idempotency key, message envelope, recipient,
|
||||
and arbitrary exception-derived key/value fields are never added. Existing runtime masking remains
|
||||
defense in depth rather than the primary privacy boundary.
|
||||
|
||||
## 10. Duplicate Logging Removal
|
||||
|
||||
`OutboxMessagePublishAdapter` currently uses `FailOpenDependencyLogger` for a fail-closed operation:
|
||||
it emits WARN and rethrows. That logger's documented meaning is an optional dependency failure where
|
||||
the use case still succeeds. The relay then emits a second ERROR after deciding `FAILED` or `DEAD`.
|
||||
|
||||
After this refactoring:
|
||||
|
||||
- `OutboundMessagePublisher` keeps `FailOpenDependencyLogger` because its contract is genuinely
|
||||
fail-open.
|
||||
- `OutboxMessagePublishAdapter` maps/sends and surfaces failures without logging.
|
||||
- `Slf4jOutboxRelayFailureReportAdapter` emits the single canonical ERROR after the application
|
||||
state transition succeeds.
|
||||
|
||||
This removes duplicate records and makes severity match the confirmed outcome.
|
||||
|
||||
## 11. Dependency Purity
|
||||
|
||||
### 11.1 Gradle declarations
|
||||
|
||||
`application-core` production declarations become:
|
||||
|
||||
```groovy
|
||||
dependencies {
|
||||
implementation project(':domain-core')
|
||||
implementation project(':shared-contract')
|
||||
}
|
||||
```
|
||||
|
||||
The root test baseline gives application-core JUnit Jupiter and AssertJ directly rather than Spring
|
||||
Boot test starters. Spring dependency-management may remain build tooling, but Spring/logging/
|
||||
Micrometer artifacts must not appear on application-core main or test compile/runtime classpaths.
|
||||
Static-analysis tool configurations are outside this classpath rule.
|
||||
|
||||
### 11.2 Gradle verification
|
||||
|
||||
A blocking `verifyApplicationCoreDependencyPurity` task checks both:
|
||||
|
||||
1. application-core production configurations contain no declared external module dependency;
|
||||
2. `compileClasspath`, `runtimeClasspath`, `testCompileClasspath`, and `testRuntimeClasspath`
|
||||
resolve no Spring, SLF4J, Logback, Log4j, or Micrometer component.
|
||||
|
||||
The task is wired into `:application-core:check`.
|
||||
|
||||
### 11.3 Source verification
|
||||
|
||||
ArchUnit adds an application logger/metrics ban covering:
|
||||
|
||||
- `org.slf4j..`
|
||||
- `java.util.logging..`
|
||||
- `ch.qos.logback..`
|
||||
- `org.apache.logging.log4j..`
|
||||
- `io.micrometer..`
|
||||
|
||||
An intentional application-package fixture proves the rule is not vacuous. The Gradle task remains
|
||||
necessary because ArchUnit cannot detect an unused starter that is merely present on the classpath.
|
||||
|
||||
## 12. Module Ownership Alternatives
|
||||
|
||||
### 12.1 Messaging adapter — selected
|
||||
|
||||
Advantages:
|
||||
|
||||
- highest cohesion with outbox publication failure;
|
||||
- existing application/shared/support and SLF4J dependencies;
|
||||
- no new project edge;
|
||||
- reusable by composition roots other than app-bootstrap;
|
||||
- preserves bootstrap as wiring rather than an adapter collection.
|
||||
|
||||
Counterargument: reporting is observability rather than broker transport. The selected design
|
||||
answers this by keeping the contract application-owned and the concrete class narrowly
|
||||
outbox-specific; generic logging support does not move into messaging.
|
||||
|
||||
### 12.2 Shared outbound support — rejected
|
||||
|
||||
Advantages:
|
||||
|
||||
- already owns reusable correlation and fail-open dependency logging;
|
||||
- would centralize logging backend calls.
|
||||
|
||||
Counterargument: the support module explicitly keeps feature-specific behavior in the owning leaf.
|
||||
Putting `FAILED`/`DEAD` outbox semantics there makes a low-level shared module feature-aware.
|
||||
Generalizing the interface would create the forbidden logger abstraction.
|
||||
|
||||
### 12.3 App bootstrap — viable fallback, not selected
|
||||
|
||||
Advantages:
|
||||
|
||||
- owns runtime logging bootstrap and final wiring;
|
||||
- already contains outbox metrics and structured Logstash usage.
|
||||
|
||||
Counterargument: each feature-specific reporter placed there expands the composition root into an
|
||||
adapter implementation module and prevents straightforward reuse by another composition root.
|
||||
|
||||
### 12.4 Direct `slf4j-api` in application-core — rejected
|
||||
|
||||
This is the smallest dependency diff and would remove Spring Boot transitive dependencies, but it
|
||||
retains framework coupling, contradicts the module rule, and tests formatting calls instead of
|
||||
application meaning.
|
||||
|
||||
### 12.5 Generic operational event publisher — deferred
|
||||
|
||||
A typed cross-feature operational event sink could become valuable when several application
|
||||
features need the same routing. Introducing it for two outbox outcomes is premature, risks an
|
||||
untyped field bag, and must never be implemented through the failing outbox broker.
|
||||
|
||||
## 13. Testing Strategy
|
||||
|
||||
### Application contract tests
|
||||
|
||||
- report factory happy paths and invariant rejection;
|
||||
- record component whitelist proving payload and idempotency key are absent;
|
||||
- transient failure reports only after `markFailed`;
|
||||
- dead-letter failure reports only after `markDead`;
|
||||
- transition failure emits no report;
|
||||
- successful publish and `markPublished` failure emit no failure report;
|
||||
- a throwing reporter does not change `FAILED`/`DEAD` result and does not stop later events.
|
||||
|
||||
### Messaging adapter tests
|
||||
|
||||
- exactly one ERROR record;
|
||||
- code/category/outcome/runbook mapping;
|
||||
- required snake_case identifiers and attempt fields;
|
||||
- retry-only `next_attempt_at`;
|
||||
- throwable preservation;
|
||||
- no payload or idempotency key;
|
||||
- reporter bean exists when messaging is disabled and when a broker is active;
|
||||
- outbox publisher propagates runtime and checked broker failures without emitting the old
|
||||
fail-open WARN.
|
||||
|
||||
### Architecture and Gradle tests
|
||||
|
||||
- intentional application logger fixture is rejected;
|
||||
- production application packages pass the new rule;
|
||||
- application-core dependency purity task passes only with clean main/test classpaths;
|
||||
- lock verification passes after regeneration.
|
||||
|
||||
### Regression tests
|
||||
|
||||
- focused application, messaging, and bootstrap tests;
|
||||
- outbox PostgreSQL lifecycle tests when Docker is available;
|
||||
- full `test` and `check`.
|
||||
|
||||
## 14. Migration Sequence
|
||||
|
||||
1. Recover and validate the harness module registry so Gradle can configure.
|
||||
2. Add red tests for safe report contracts and relay semantics.
|
||||
3. Add the application report value and port.
|
||||
4. Inject the port into the relay and make report failures outcome-neutral.
|
||||
5. Add red messaging adapter and wiring tests.
|
||||
6. Implement the structured messaging reporter.
|
||||
7. Remove fail-open logging from the fail-closed outbox publisher.
|
||||
8. Remove application-core Boot/Spring/logging dependencies and give it a pure test baseline.
|
||||
9. Add source and resolved-classpath purity guards.
|
||||
10. Regenerate dependency locks and update module/runbook documentation.
|
||||
11. Run focused, architecture, dependency, full test, and full check gates.
|
||||
12. Capture implementation evidence in the LLM Wiki as required by repository policy.
|
||||
|
||||
## 15. Acceptance Criteria
|
||||
|
||||
- No application-core main or test source imports Spring, SLF4J, Logback, Log4j, JUL logging, or
|
||||
Micrometer.
|
||||
- No forbidden framework artifact appears on application-core main/test compile/runtime
|
||||
classpaths.
|
||||
- `spring-boot-starter` is absent from `src/application-core/build.gradle`.
|
||||
- Every confirmed `FAILED`/`DEAD` transition attempts exactly one typed report.
|
||||
- A status-transition failure emits no success-like failure report.
|
||||
- A reporter exception cannot change a relay outcome or stop the next claimed event.
|
||||
- Production wiring contains exactly one non-NOOP `OutboxRelayFailureReportPort`.
|
||||
- Structured ERROR fields and runbook links match the documented registry conventions.
|
||||
- Payload and idempotency key cannot cross the report contract and do not appear in adapter logs.
|
||||
- The fail-closed publisher no longer uses `FailOpenDependencyLogger`.
|
||||
- Application logger ArchUnit mutation and dependency purity checks are blocking.
|
||||
- Focused tests, dependency locks, architecture checks, full tests, and `check` pass after harness
|
||||
registry recovery, or any environmental blocker is reported with its remaining risk.
|
||||
+87
@@ -0,0 +1,87 @@
|
||||
# Application Outbox Failure Reporting — Harness-Free Design
|
||||
|
||||
## Context
|
||||
|
||||
`application-core` currently carries Spring Boot and SLF4J only because
|
||||
`PublishPendingOutboxEventsUseCase` renders relay failures itself. That reverses the diagnostic
|
||||
dependency direction and also permits a duplicate WARN in `OutboxMessagePublishAdapter`.
|
||||
|
||||
This change is harness-free: `src/config/architecture/modules.json`, Gradle, ArchUnit, and focused
|
||||
module tests are the policy and evidence authorities. No `.harness` files or public paths change.
|
||||
|
||||
## Boundary
|
||||
|
||||
`application-core` owns a specific `OutboxRelayFailureReportPort` and an immutable
|
||||
`OutboxRelayFailureReport`. The report is an allowlist containing only:
|
||||
|
||||
- `OperationalError code`
|
||||
- event, aggregate, and correlation identifiers
|
||||
- event type, attempt count, optional next-attempt time
|
||||
- the originating `RuntimeException`
|
||||
|
||||
It never carries the payload, idempotency key, message template, severity, arbitrary fields, or the
|
||||
whole `OutboxEvent`. Factories and record invariants admit only retryable
|
||||
`OUTBOX_PUBLISH_FAILED` reports with a next-attempt time and terminal `OUTBOX_DEAD_LETTER` reports
|
||||
without one.
|
||||
|
||||
`adapter:outbound:messaging` owns `Slf4jOutboxRelayFailureReportAdapter`. It maps the typed report to
|
||||
one canonical SLF4J 2 fluent ERROR with fixed key names and runbook links. Bootstrap only wires the
|
||||
port.
|
||||
|
||||
## Ordering and Failure Semantics
|
||||
|
||||
The persisted FAILED or DEAD transition is authoritative:
|
||||
|
||||
1. broker publication fails;
|
||||
2. the application calculates the transition;
|
||||
3. the store transition succeeds inside `TransactionPort`;
|
||||
4. only then is the typed report emitted.
|
||||
|
||||
A transition failure propagates and emits no report. A reporter `RuntimeException` is contained by
|
||||
both the adapter and the use case, so it cannot change the relay outcome or prevent later events
|
||||
from running. Successful publication and `markPublished` failures emit no failure report.
|
||||
|
||||
There is no production no-op reporter. `MessagingConfig` always contributes exactly one reporter
|
||||
bean, using the configured broker name or `disabled` when blank. `OutboxMessagePublishAdapter`
|
||||
becomes mapping/send-only: runtime failures propagate, checked failures are wrapped with their
|
||||
cause, and it emits no success or failure log. The general `OutboundMessagePublisher` retains its
|
||||
existing fail-open dependency logging.
|
||||
|
||||
## Structured ERROR Contract
|
||||
|
||||
Every confirmed transition produces one ERROR with the common fields:
|
||||
|
||||
`error.code`, `error.category`, `dependency_name`, `dependency_type=messaging`, `outcome`,
|
||||
`event_id`, `event_type`, `aggregate_id`, `correlation_id`, `attempt_count`, and `runbook_link`.
|
||||
|
||||
Retryable failures additionally carry `next_attempt_at`. Mappings are:
|
||||
|
||||
| Code | Outcome | Runbook |
|
||||
| --- | --- | --- |
|
||||
| `OUTBOX_PUBLISH_FAILED` | `FAILED` | `runbook://outbox/publish-failed` |
|
||||
| `OUTBOX_DEAD_LETTER` | `DEAD` | `runbook://outbox/dead-letter` |
|
||||
|
||||
The originating exception is attached as the throwable. Payload, idempotency key, envelope data,
|
||||
message templates derived from the exception, and arbitrary exception fields are forbidden.
|
||||
The adapter's fail-open boundary also applies to invalid direct calls: `report(null)` must never
|
||||
throw. The focused structured-adapter test pins this behavior.
|
||||
|
||||
## Enforcement and Tests
|
||||
|
||||
- Value tests enforce invariants and reflectively pin the exact record component allowlist.
|
||||
- Relay tests pin transition-before-report ordering, no-report paths, exact cardinality, and
|
||||
reporter containment.
|
||||
- Messaging tests capture Logback events and pin level, fields, throwable, and unsafe-data absence.
|
||||
- `verifyApplicationCoreDependencyPurity` rejects non-project production declarations and forbidden
|
||||
Spring/logging/metrics groups on resolved application classpaths.
|
||||
- `APPLICATION_HAS_NO_DIAGNOSTIC_FRAMEWORK` bans SLF4J, JUL, Logback, Log4j, and Micrometer from
|
||||
the exact `dev.caskeleton.application..` scope. Its dedicated violation fixture also resides
|
||||
inside that scope, under `dev.caskeleton.application.architecture.violations`, proving the rule
|
||||
is non-vacuous.
|
||||
- `application-core` test dependencies are reduced to JUnit Jupiter and AssertJ; all other leaves
|
||||
keep the shared Spring Boot test baseline.
|
||||
|
||||
## Scope
|
||||
|
||||
No public path, CI workflow, module-registry edge, payload shape, outbox persistence schema, or
|
||||
general publisher logging behavior changes. Agents do not stage, commit, amend, or push.
|
||||
@@ -0,0 +1,476 @@
|
||||
# CI Control Plane Recovery Design
|
||||
|
||||
- **Date:** 2026-07-25
|
||||
- **Status:** Approved
|
||||
- **Scope:** repository control-plane recovery, Gitea Actions enablement, Gradle/CI gates,
|
||||
container build context, dependency automation, and documentation parity
|
||||
- **Depends on:**
|
||||
[`2026-07-20-harness-policy-engine-design.md`](2026-07-20-harness-policy-engine-design.md)
|
||||
- **Current revision audited:** `821fe00c323b5335980f271c7ee47b92ac2168f2`
|
||||
- **Task-packet state:** unavailable. `.harness/validators/resolve_task.py` and its policy inputs
|
||||
are absent from the audited revision, so no task-packet hash or formal evidence profile can be
|
||||
produced before control-plane recovery.
|
||||
|
||||
## 1. Problem Statement
|
||||
|
||||
The repository describes a high-assurance CI and architecture-governance control plane, but the
|
||||
audited Git tree does not contain the hidden root paths that implement it. The current revision
|
||||
contains no `.harness`, `.agents`, `.claude`, `.codex`, or `.github` tree. It also lacks the root
|
||||
`.tool-versions`, `.trivyignore.yaml`, and `.gitattributes` contracts referenced by the tracked
|
||||
guidance and Gradle build.
|
||||
|
||||
This is not a Gradle wrapper failure. The tracked wrapper downloads and starts Gradle 9.0.0 under
|
||||
Java 21, but every project task stops while evaluating `src/settings.gradle` because
|
||||
`.harness/project/modules.yaml` is missing. The same missing registry also prevents the architecture
|
||||
dependency gate and task-packet resolution from running.
|
||||
|
||||
The CI host is Gitea 1.27.0, not GitHub. The public repository API reports `has_actions: false`, so
|
||||
the repository Actions unit is disabled. An unauthenticated request to the runner API returns
|
||||
`401`, which proves that runner state must be checked with repository or administrator
|
||||
authorization; it does not prove that a usable runner exists. Gitea Actions requires both the
|
||||
repository Actions unit and an online runner.
|
||||
|
||||
The container build has an additional independent defect. Compose and both Dockerfile examples use
|
||||
`src/` as the build context, while Gradle resolves the registry from the repository root. Even after
|
||||
the hidden assets are restored, a build with the current context cannot copy the registry into the
|
||||
builder.
|
||||
|
||||
## 2. Audit Baseline
|
||||
|
||||
| Surface | Command or source | Observed result |
|
||||
| --- | --- | --- |
|
||||
| Git revision | `git rev-parse HEAD` | `821fe00c323b5335980f271c7ee47b92ac2168f2` |
|
||||
| Hidden assets | `git cat-file -e HEAD:.harness` and equivalent checks | `.harness`, `.agents`, `.claude`, `.codex`, `.github` absent |
|
||||
| Ignore rules | `git check-ignore -v --no-index ...` | exit `1`; missing paths are not ignored |
|
||||
| Recoverable local objects | `git fsck --full --no-reflogs --unreachable` | exit `0`; no unreachable objects reported |
|
||||
| Gradle launcher | `cd src && ./gradlew --version` | exit `0`; Gradle 9.0.0 and Java 21 |
|
||||
| Gradle project task | `cd src && ./gradlew tasks --console=plain` | exit `1`; missing module registry at `src/settings.gradle:12` |
|
||||
| Gradle release gate | `cd src && ./gradlew check --console=plain` | exit `1`; same settings failure |
|
||||
| Task resolver | `python3 .harness/validators/resolve_task.py ...` | impossible; resolver file absent |
|
||||
| Compose syntax | `docker compose -f docker-compose.yml -f docker-compose.local.yml config --quiet` | exit `0` |
|
||||
| Gitea version | `GET /api/v1/version` | `1.27.0` |
|
||||
| Repository Actions | public repository API | `has_actions: false` |
|
||||
| Runner API | unauthenticated runner request | `401`; authorized runner inventory still required |
|
||||
|
||||
The most recent commit added 971 files over a parent that contained only a two-line README. The
|
||||
root tree contains no dot-prefixed entry, while nested files such as `src/.env`,
|
||||
`src/.dockerignore`, and `src/.gitignore` were included. This is consistent with a top-level shell
|
||||
glob used during copying or staging. That is a falsifiable root-cause hypothesis, not proof of the
|
||||
exact command that was used.
|
||||
|
||||
## 3. Goals
|
||||
|
||||
1. Prefer byte-for-byte recovery of the authoritative hidden control-plane assets; when that source
|
||||
is unavailable or incomplete, require an explicit human reconstruction decision and record new
|
||||
provenance without presenting reconstruction as restoration.
|
||||
2. Restore the 19-leaf module registry and the harness behavior approved in the 2026-07-20 design.
|
||||
3. Make a fresh checkout fail early with a precise control-plane error before Gradle configuration.
|
||||
4. Enable repository Actions on Gitea 1.27.0 and provide an isolated, repository-scoped runner.
|
||||
5. Keep `.github/workflows` as the canonical workflow directory while preventing an accidental
|
||||
`.gitea/workflows` shadow.
|
||||
6. Restore a single release-blocking fan-in status and the complementary vulnerability status.
|
||||
7. Make public-path, Trivy suppression, dependency-lock, and architecture gates fail closed.
|
||||
8. Make Docker builds consume the same root registry without duplicating the registry under `src/`.
|
||||
9. Pause Renovate automerge until required CI statuses and lockfile refresh behavior are proven.
|
||||
10. Bring README, gate-matrix, workflow, and physical-path claims back into parity.
|
||||
|
||||
## 4. Non-Goals
|
||||
|
||||
- This recovery does not change production Java behavior or module boundaries.
|
||||
- It does not redesign the 19-leaf registry approved in the 2026-07-20 harness policy design.
|
||||
- It does not silently synthesize hidden policy files or present reconstructed content as recovered
|
||||
authority.
|
||||
- It does not store a Gitea API token, runner registration token, or repository secret in Git.
|
||||
- It does not enable deployment to a production environment. Release artifact construction and
|
||||
scanning are restored, but a separate deployment decision remains human-owned.
|
||||
- It does not re-enable Renovate automerge merely because workflow files exist; branch protection
|
||||
and a successful dependency-update exercise are also required.
|
||||
|
||||
## 5. Governing Invariants
|
||||
|
||||
### 5.1 Recovery mode is an explicit human decision
|
||||
|
||||
The original working tree, archive, or source repository that produced the 2026-07-20 harness
|
||||
design is the preferred recovery authority. Before copying anything into this repository, the
|
||||
recovery source must be inventoried and hashed outside the worktree.
|
||||
|
||||
The minimum authoritative set is:
|
||||
|
||||
- `.harness/`
|
||||
- `.agents/`
|
||||
- `.claude/`
|
||||
- `.codex/`
|
||||
- `.github/`
|
||||
- `.tool-versions`
|
||||
- `.trivyignore.yaml`
|
||||
- `.gitattributes`
|
||||
|
||||
The human owner chooses one of two modes and records it before repository writes:
|
||||
|
||||
**Mode A — authoritative restore.** All minimum paths exist in the recovery source. The executor
|
||||
hashes them, copies them byte-for-byte, proves source/destination equality, and preserves their
|
||||
native provenance.
|
||||
|
||||
**Mode B — controlled reconstruction.** The original source is unavailable or incomplete. The
|
||||
human records that fact and explicitly authorizes reconstruction from the approved
|
||||
[`2026-07-20-harness-policy-engine-design.md`](2026-07-20-harness-policy-engine-design.md),
|
||||
[`2026-07-20-harness-policy-engine.md`](../plans/2026-07-20-harness-policy-engine.md), the tracked
|
||||
Gradle/module sources, and this design. Reconstructed artifacts receive new hashes and a
|
||||
`controlled-reconstruction` provenance record. Schema, renderer parity, mutation coverage, Gradle
|
||||
project discovery, and architecture dependency checks must pass before the new artifacts can act as
|
||||
authority.
|
||||
|
||||
The first Mode B inventory covers the reconstructed harness and generated platform assets only.
|
||||
After Tasks 3-6 and every later change to a covered path are final, the executor regenerates one
|
||||
complete, sorted SHA-256 inventory for `.harness`, `.agents`, `.claude`, `.codex`, `.github`,
|
||||
`.tool-versions`, `.trivyignore.yaml`, `.gitattributes`, `.dockerignore`, and
|
||||
`docs/security/public-paths-snapshot.txt`. The provenance record names that final evidence path.
|
||||
Only this post-change inventory is used for the human handoff.
|
||||
|
||||
Implementation stops only until the human chooses Mode A or Mode B. An incomplete Mode A export
|
||||
must never be filled silently. It may instead cause the human to switch the recorded decision to
|
||||
Mode B. The tracked `AGENTS.md`, `CLAUDE.md`, and design documents are evidence of intended
|
||||
behavior, but reconstructed schemas, agents, registries, and workflows become authoritative only
|
||||
after the required new evidence passes.
|
||||
|
||||
### 5.2 Human-only commit policy
|
||||
|
||||
Recovery and implementation may leave reviewed changes in the working tree, but agents do not
|
||||
stage, commit, amend, or push. A human decides commit boundaries after reviewing recovery hashes,
|
||||
generated-file parity, test evidence, and Gitea status checks.
|
||||
|
||||
### 5.3 One registry and one workflow source
|
||||
|
||||
`.harness/project/modules.yaml` remains the only module-edge and focused-command registry.
|
||||
Container builds copy that file from the repository root; they do not create a second copy under
|
||||
`src/`.
|
||||
|
||||
`.github/workflows` remains the canonical workflow directory because the repository documentation
|
||||
and portability contract already point there. Gitea's default `WORKFLOW_DIRS` value is
|
||||
`.gitea/workflows,.github/workflows`, and Gitea uses the first directory that exists. Therefore
|
||||
`.gitea/workflows` must remain absent unless the project later adopts a generated-mirror design
|
||||
with an explicit parity check and a separate approved specification.
|
||||
|
||||
### 5.4 Fail closed before expensive work
|
||||
|
||||
CI runs a repository control-plane preflight before invoking build or container work. Missing
|
||||
policies, workflow shadowing, a missing committed public-path baseline, or an incomplete structured
|
||||
Trivy contract fail immediately. Once Java/Gradle is available, that same blocking preflight runs
|
||||
`verifyTrivyignore`; the vulnerability scanner must explicitly consume `.trivyignore.yaml`.
|
||||
|
||||
## 6. Target Architecture
|
||||
|
||||
```text
|
||||
authoritative hidden-asset export
|
||||
|
|
||||
v
|
||||
recovery inventory + SHA-256 comparison
|
||||
|
|
||||
v
|
||||
.harness/.agents/.claude/.codex/.github restored
|
||||
|
|
||||
v
|
||||
control-plane preflight
|
||||
|-- required paths
|
||||
|-- generated-agent parity
|
||||
|-- canonical workflow directory
|
||||
|-- committed security baselines
|
||||
`-- task-packet resolver availability
|
||||
|
|
||||
v
|
||||
resolved high-risk CI/deployment task packet
|
||||
|
|
||||
+-----------------------------+
|
||||
| |
|
||||
v v
|
||||
Gradle quality gates Docker root-context builds
|
||||
|-- dependency locks |-- production bootJar
|
||||
|-- architecture edges `-- sample bootJar
|
||||
|-- focused/ArchUnit tests
|
||||
|-- test/check
|
||||
`-- public/env/security contracts
|
||||
| |
|
||||
+--------------+--------------+
|
||||
v
|
||||
CI quality release-gate
|
||||
+
|
||||
dependency-vulnerability required status
|
||||
|
|
||||
v
|
||||
Gitea protected-branch decision
|
||||
```
|
||||
|
||||
## 7. Design Decisions
|
||||
|
||||
### 7.1 Recovery gate and physical control-plane manifest
|
||||
|
||||
After Mode A restore parity or Mode B reconstruction evidence passes, the harness gains a small
|
||||
physical manifest at `.harness/project/control-plane.yaml`. It lists required files, required
|
||||
directories, the canonical workflow directory, and the forbidden shadow directory. The file uses
|
||||
JSON syntax, matching the 2026-07-20 design's stdlib-only JSON-as-YAML convention.
|
||||
|
||||
`.harness/validators/validate_control_plane.py` reads the manifest and reports every missing path in
|
||||
one deterministic result. It also rejects `.gitea/workflows`. Its tests live at
|
||||
`.harness/tests/test_control_plane.py`.
|
||||
|
||||
This validator checks physical availability only. It does not duplicate module edges, risk rules,
|
||||
or workflow gate semantics. Module semantics stay in `modules.yaml`; the gate matrix stays in
|
||||
`.github/ci-gate-matrix.yml`.
|
||||
|
||||
### 7.2 Gitea Actions and runner control
|
||||
|
||||
The repository owner enables `Enable Repository Actions` in the repository settings. The public API
|
||||
must then report `has_actions: true`.
|
||||
|
||||
The runner is registered at repository scope, uses an isolated Docker execution mode, and exposes
|
||||
the exact `ubuntu-22.04` label used by the workflows. Registration credentials remain in the runner
|
||||
host's protected state or secret manager. They never enter workflow YAML, shell history captured by
|
||||
CI, Docker image layers, or repository files.
|
||||
|
||||
An authenticated repository runner inventory must show at least one enabled, online runner before
|
||||
the first required workflow is treated as operational. The previous unauthenticated `401` remains
|
||||
an expected access-control result.
|
||||
|
||||
### 7.3 Canonical workflow directory and Gitea shadowing
|
||||
|
||||
The recovery restores canonical workflows under:
|
||||
|
||||
- `.github/workflows/ci-quality-gates.yml`
|
||||
- `.github/workflows/build-release-supply-chain.yml`
|
||||
- `.github/workflows/dependency-vulnerability.yml`
|
||||
|
||||
No workflow is copied to `.gitea/workflows`. With Gitea's default directory ordering, the mere
|
||||
existence of `.gitea/workflows` would cause `.github/workflows` to be ignored. The preflight
|
||||
validator makes that shadow a blocking failure.
|
||||
|
||||
Instance administration must confirm that `[actions].WORKFLOW_DIRS` still contains
|
||||
`.github/workflows`. If the instance has a non-default value that excludes it, the administrator
|
||||
changes the instance setting or the project stops before enabling required checks.
|
||||
|
||||
### 7.4 Preflight and release-gate topology
|
||||
|
||||
`ci-quality-gates.yml` starts with `control-plane-preflight`. No Gradle, test, or Docker job runs
|
||||
unless preflight succeeds.
|
||||
|
||||
The release-blocking fan-out includes:
|
||||
|
||||
- restored harness unit and mutation suite
|
||||
- `validate_modules.py`, renderer parity, policy parity, and `verify-gate-matrix.sh`
|
||||
- structured Trivy validation through `verifyTrivyignore`
|
||||
- Gradle wrapper launch and Java 21 assertion
|
||||
- `verifyDependencyLocks`
|
||||
- `verifyCleanArchitectureDependencies`
|
||||
- focused ArchUnit coverage
|
||||
- `verifyPublicPathSnapshot`
|
||||
- `test`
|
||||
- `check`
|
||||
- production and sample Docker builds
|
||||
- reproducible artifact verification when owned by the restored workflow contract
|
||||
|
||||
The workflow ends with a single `release-gate` job that uses `if: always()` and fails unless every
|
||||
release-blocking dependency succeeded. Quarantine remains non-blocking and is intentionally absent
|
||||
from the fan-in.
|
||||
|
||||
Gitea cannot express `needs` across separate workflow files. The dependency vulnerability workflow
|
||||
therefore publishes its own blocking status. Protected branches require both the quality
|
||||
`release-gate` status and the vulnerability status.
|
||||
|
||||
### 7.5 Missing contracts
|
||||
|
||||
The recovery must restore `.tool-versions`, `.gitattributes`, `.trivyignore.yaml`, workflow scripts,
|
||||
gate matrix, CODEOWNERS, and vulnerability policy from the authoritative source.
|
||||
|
||||
The structured empty Trivy contract is retained even when there are no suppressions:
|
||||
|
||||
```yaml
|
||||
vulnerabilities: []
|
||||
licenses: []
|
||||
misconfigurations: []
|
||||
secrets: []
|
||||
```
|
||||
|
||||
The quality preflight and release-tag preflight both run `verifyTrivyignore`. The independent
|
||||
vulnerability workflow also runs that verifier and passes
|
||||
`trivyignores: .trivyignore.yaml` to the pinned Trivy action, so a present-but-unconsumed or
|
||||
malformed suppression file cannot satisfy a required status.
|
||||
|
||||
`docs/security/public-paths-snapshot.txt` becomes a committed baseline. The current approved value
|
||||
derived from `src/.env` is `/api/healthcheck`. A missing baseline is a failure, not an instruction to
|
||||
create one during verification.
|
||||
|
||||
The read-only `verifyPublicPathSnapshot` task compares the committed baseline to `src/.env`.
|
||||
Generation moves to a separate, explicitly approved update task. `check` and the quality workflow
|
||||
both depend on the read-only verification task.
|
||||
|
||||
### 7.6 Dependency locks
|
||||
|
||||
All 19 leaf modules retain Gradle strict locking. CI runs `verifyDependencyLocks` before compile or
|
||||
test jobs so an incomplete Renovate update fails with a direct lock error.
|
||||
|
||||
The only supported lock refresh command remains:
|
||||
|
||||
```bash
|
||||
cd src
|
||||
./gradlew resolveAndLockAll --write-locks --console=plain
|
||||
```
|
||||
|
||||
A dependency update is acceptable only when the declaration, all affected `gradle.lockfile` files,
|
||||
and the quality gate agree. CI never runs `--write-locks`.
|
||||
|
||||
### 7.7 Docker root context
|
||||
|
||||
Compose changes the app build context from `src/` to the repository root and addresses the
|
||||
Dockerfile as `src/Dockerfile`. Both Dockerfiles keep the Gradle project at `/build/src` and copy:
|
||||
|
||||
1. `.harness/project/modules.yaml` to `/build/.harness/project/modules.yaml`;
|
||||
2. wrapper, build descriptors, and lockfiles to `/build/src`;
|
||||
3. the complete `src/` tree only after dependency verification.
|
||||
|
||||
A root `.dockerignore` replaces the context role previously owned by `src/.dockerignore`. It
|
||||
excludes Git metadata, build output, IDE state, environment files, and secrets, while explicitly
|
||||
allowing the module registry, wrapper, build descriptors, lockfiles, and Java/resources trees.
|
||||
|
||||
This preserves a single registry and makes local Compose, production image, sample image, and CI use
|
||||
the same context contract.
|
||||
|
||||
### 7.8 Renovate safety state
|
||||
|
||||
`renovate.json` sets `automerge: false` for every update type during recovery. The current comment
|
||||
already states that automerge requires trustworthy CI, while the repository currently has no
|
||||
operational Actions unit.
|
||||
|
||||
Limited patch/pin/digest automerge can be reconsidered only after all of the following are observed:
|
||||
|
||||
1. repository API reports `has_actions: true`;
|
||||
2. an authenticated runner inventory reports an online runner;
|
||||
3. protected branches require both blocking statuses;
|
||||
4. a real Renovate dependency pull request updates strict lock state and passes;
|
||||
5. a deliberately stale lockfile fails `verifyDependencyLocks`.
|
||||
|
||||
The configuration description must also stop claiming that the project has
|
||||
`gradle/libs.versions.toml` unless the project separately adopts a version catalog.
|
||||
|
||||
### 7.9 Documentation parity
|
||||
|
||||
Root and `src/` README files must point to paths that exist in Git and commands that pass from a
|
||||
fresh checkout. The control-plane validator covers required physical paths, and the restored
|
||||
README-command and gate-matrix checks cover executable behavior.
|
||||
|
||||
The documentation must distinguish:
|
||||
|
||||
- Gitea repository Actions enablement from workflow files;
|
||||
- unauthenticated runner API access from authorized runner health;
|
||||
- `.github/workflows` as canonical from `.gitea/workflows` as a shadow risk;
|
||||
- lock verification from lock regeneration;
|
||||
- read-only public-path verification from approved baseline update.
|
||||
|
||||
## 8. Phased Recovery
|
||||
|
||||
### Phase 0 — Preserve evidence
|
||||
|
||||
Capture the current revision, clean status, missing-path evidence, Gitea version, repository
|
||||
Actions state, and runner authorization behavior. Hash the authoritative recovery source before
|
||||
copying it.
|
||||
|
||||
### Phase 1 — Establish authority
|
||||
|
||||
The human chooses Mode A or Mode B. Mode A restores the hidden asset set byte-for-byte and verifies
|
||||
source equality. Mode B reconstructs from the two approved 2026-07-20 documents, records new
|
||||
provenance/hashes, and runs schema, renderer-parity, mutation, Gradle discovery, and architecture
|
||||
checks. The executor then writes a controller-approved overlay to the recorded recovery evidence
|
||||
path, invokes the recovered resolver with that file, persists the resolved packet plus packet/rule
|
||||
checksums, and proves deterministic re-resolution. Task 3 cannot start until those exact artifacts
|
||||
verify.
|
||||
|
||||
### Phase 2 — Establish fail-fast local gates
|
||||
|
||||
Add the physical control-plane manifest and validator. Restore missing security contracts and make
|
||||
the public-path baseline fail closed. Run harness checks before Gradle.
|
||||
|
||||
### Phase 3 — Repair build paths
|
||||
|
||||
Switch Docker to the repository-root context, add the root ignore contract, and verify production
|
||||
and sample images.
|
||||
|
||||
### Phase 4 — Activate Gitea
|
||||
|
||||
Enable repository Actions, register the isolated runner, confirm workflow directory configuration,
|
||||
and run the preflight workflow. Do not configure required statuses until job names are stable and a
|
||||
successful run exists.
|
||||
|
||||
### Phase 5 — Enforce merge controls
|
||||
|
||||
Enable the quality `release-gate` and vulnerability status as protected-branch requirements. Seed
|
||||
negative exercises for a missing required path, public-path drift, forbidden module edge, stale
|
||||
lockfile, and failed Docker build.
|
||||
|
||||
### Phase 6 — Reassess automation
|
||||
|
||||
Run a real Renovate dependency update with automerge disabled. Re-enable limited automerge only by a
|
||||
separate human decision backed by the acceptance evidence.
|
||||
|
||||
## 9. Verification Strategy
|
||||
|
||||
1. Mode A hashes match the authoritative export, or Mode B records the human decision, new hashes,
|
||||
and `controlled-reconstruction` provenance.
|
||||
2. Harness unit, mutation, schema, renderer, and parity tests pass under the selected mode.
|
||||
3. Control-plane validator passes on the complete tree and fails on each seeded missing/shadow
|
||||
mutation.
|
||||
4. Task-packet resolver emits a stable high-risk CI/deployment packet from the recorded overlay;
|
||||
the packet and governing-rule checksums verify again at the Task 3 boundary.
|
||||
5. Gradle wrapper, project discovery, architecture dependency verification, focused ArchUnit,
|
||||
dependency locks, `test`, and `check` pass.
|
||||
6. Public-path verification passes with the committed baseline and fails when it is absent or
|
||||
changed.
|
||||
7. Production and sample Docker images build from repository-root context.
|
||||
8. Gitea reports repository Actions enabled and at least one authorized runner online.
|
||||
9. The preflight and quality fan-in statuses appear on a real pull request.
|
||||
10. Protected branches reject seeded failures.
|
||||
11. Renovate config validation and a real dependency update pass without automerge.
|
||||
|
||||
## 10. Risks and Countermeasures
|
||||
|
||||
| Risk | Countermeasure |
|
||||
| --- | --- |
|
||||
| Reconstructed policy differs from the lost authority | Require the human Mode B decision, label provenance as reconstruction, assign new hashes, and require schema/parity/mutation/Gradle evidence |
|
||||
| `.gitea/workflows` silently shadows canonical workflows | Block the directory in the physical preflight and confirm instance `WORKFLOW_DIRS` |
|
||||
| Runner can expose host Docker authority | Use a repository-scoped isolated runner, restrict fork execution, and keep registration credentials outside jobs |
|
||||
| Workflow exists but repository Actions remains disabled | Require API `has_actions: true` and a real run before branch-protection setup |
|
||||
| Required status name changes and bypasses protection | Keep stable job names in the gate matrix and verify protection after workflow changes |
|
||||
| Public-path baseline is regenerated in CI | Separate update and verify tasks; verification fails when the committed file is missing |
|
||||
| Docker root context sends secrets | Root `.dockerignore` excludes environment/secret paths and CI checks the context contract |
|
||||
| Renovate updates declarations without strict locks | Run `verifyDependencyLocks` before tests and keep automerge disabled through a real update exercise |
|
||||
| Restored workflows assume GitHub-only behavior | Exercise every event, context, action, and fan-in on Gitea 1.27.0 before making the status required |
|
||||
|
||||
## 11. Acceptance Criteria
|
||||
|
||||
- Mode A has a byte-identical authoritative inventory, or Mode B has a human-recorded reconstruction
|
||||
decision, new provenance, preliminary hashes, and a post-change complete hash inventory.
|
||||
- The 2026-07-20 harness registry, validators, generated agents, mutation suite, and parity checks
|
||||
pass under the selected mode.
|
||||
- A stable task packet is resolved after recovery; no implementation-complete claim relies on the
|
||||
pre-recovery state. Its overlay, output packet, packet checksum, and rule checksums are retained in
|
||||
the recovery evidence directory.
|
||||
- Fresh checkout control-plane preflight reports no missing required path.
|
||||
- `.gitea/workflows` is absent and `.github/workflows` is recognized by the Gitea instance.
|
||||
- Repository API reports `has_actions: true`.
|
||||
- An authorized runner inventory reports an enabled online runner with the workflow label.
|
||||
- Gradle `projects`, dependency locks, architecture gates, focused tests, `test`, and `check` pass.
|
||||
- Missing or changed public-path baseline fails read-only verification.
|
||||
- Production and sample images build from repository-root context without a duplicated registry.
|
||||
- The quality `release-gate` and vulnerability status are required on the protected branch.
|
||||
- Release-blocking preflights run the full harness unit/mutation, module validation, renderer/parity,
|
||||
gate-matrix, and structured Trivy checks; Trivy explicitly consumes `.trivyignore.yaml`.
|
||||
- Renovate automerge remains disabled until the explicit five-part re-enable condition is met.
|
||||
- README, gate matrix, workflow jobs, and physical repository paths agree.
|
||||
- No agent stages, commits, amends, or pushes the recovery.
|
||||
|
||||
## 12. External Authorities
|
||||
|
||||
- [Gitea Actions quick start](https://docs.gitea.com/usage/actions/quickstart): repository Actions
|
||||
enablement, runner requirement, and the `.gitea/workflows` quick-start location.
|
||||
- [Gitea configuration cheat sheet](https://docs.gitea.com/administration/config-cheat-sheet):
|
||||
`[actions].ENABLED` and the default
|
||||
`WORKFLOW_DIRS=.gitea/workflows,.github/workflows` first-existing-directory behavior.
|
||||
- [Gitea runner documentation](https://docs.gitea.com/usage/actions/act-runner): repository-scoped
|
||||
registration, runner modes, credential handling, and Docker isolation trade-offs.
|
||||
@@ -0,0 +1,59 @@
|
||||
# Harness-Free Mode B Amendment
|
||||
|
||||
- **Date:** 2026-07-25
|
||||
- **Status:** Approved scope amendment
|
||||
- **Mode:** B — controlled reconstruction from repository evidence
|
||||
- **Supersedes:** `2026-07-20-harness-policy-engine-design.md` and
|
||||
`2026-07-20-harness-policy-engine.md` in full as executable guidance; both superseded documents
|
||||
remain only as historical provenance
|
||||
|
||||
## Decision
|
||||
|
||||
The repository will recover Gradle configuration and Clean Architecture dependency enforcement
|
||||
without reconstructing the absent development harness. A Gradle-owned JSON registry at
|
||||
`src/config/architecture/modules.json` becomes the single source of truth for the current 19 leaf
|
||||
modules, their repository-relative source paths, Gradle paths, and allowed production project
|
||||
dependencies.
|
||||
|
||||
Both `src/settings.gradle` and `verifyCleanArchitectureDependencies` consume that file. Settings
|
||||
validation fails closed for malformed, empty, duplicate, unsafe, or missing module entries. The
|
||||
dependency gate continues to require complete leaf coverage and reject unapproved production
|
||||
project edges; production leaves may never depend on the `sample-portfolio` fixture consumer.
|
||||
|
||||
## Evidence and provenance
|
||||
|
||||
Registry entries are reconstructed from the checked-in Gradle topology and each leaf
|
||||
`build.gradle`'s `api`, `implementation`, `compileOnly`, and `runtimeOnly` project dependencies.
|
||||
Test-only and fixture-only configurations are not architecture production edges. This is Mode B
|
||||
provenance: it restores the repository's observable build contract, not unavailable historical
|
||||
artifacts.
|
||||
|
||||
The pre-change RED command is:
|
||||
|
||||
```bash
|
||||
cd src
|
||||
./gradlew help --console=plain
|
||||
```
|
||||
|
||||
It fails because `src/settings.gradle` requires the absent
|
||||
`.harness/project/modules.yaml`.
|
||||
|
||||
## Explicit non-goals
|
||||
|
||||
- No `.harness/` tree, task resolver, task packet, or policy-hash runtime.
|
||||
- No `.agents/`, `.claude/`, `.codex/`, agent plugin, hook, renderer, or platform parity
|
||||
reconstruction.
|
||||
- No production Java or runtime behavior change.
|
||||
- No byte-identical restoration claim.
|
||||
- No claim that the earlier Harness Policy Engine plan or the broader refactor is complete.
|
||||
|
||||
## Enforcement and workflow
|
||||
|
||||
Gradle and CI gates replace harness runtime dependencies for module discovery and dependency
|
||||
policy. Root and module guidance point to the Gradle-owned registry and retain the eight local
|
||||
HARD-STOP meanings, architecture responsibilities, focused-test discipline, human-only git
|
||||
policy, and LLM Wiki capture workflow.
|
||||
|
||||
Acceptance requires successful Gradle `help`, `projects`, and
|
||||
`verifyCleanArchitectureDependencies`, an independent deterministic 19-leaf registry check,
|
||||
`git diff --check`, and a reviewed working-tree status.
|
||||
@@ -0,0 +1,97 @@
|
||||
# Harness-Free Quality and Security CI Design
|
||||
|
||||
- **Date:** 2026-07-25
|
||||
- **Status:** Approved Mode B reconstruction
|
||||
- **Scope:** Repository-internal quality, dependency-vulnerability, and link-check controls
|
||||
|
||||
## Decision and provenance
|
||||
|
||||
Mode B reconstructs observable CI contracts from the current Gradle build, active documentation,
|
||||
and the incomplete `/home/donghyeon/dev/ca-tmpl` checkout. The candidate checkout is evidence, not
|
||||
an authoritative or byte-identical restoration source. Its useful policy is adapted to the current
|
||||
`main` branch and current tasks; stale `master`, feature-branch ownership, and absent workflow
|
||||
claims are removed.
|
||||
|
||||
`.github/workflows/` is the canonical workflow path. No `.gitea/workflows` shadow is created. The
|
||||
origin is Gitea, but server-side Actions is externally disabled, so these files define repository
|
||||
controls without claiming that remote jobs currently execute.
|
||||
|
||||
Every external `uses:` reference is pinned to a verified 40-character commit SHA. Its immutable
|
||||
release tag remains beside the SHA as an inline review label; moving major-version tags are not an
|
||||
execution authority.
|
||||
|
||||
## Scope boundary
|
||||
|
||||
This slice owns:
|
||||
|
||||
- pinned Java tool evidence and text/binary normalization;
|
||||
- structured Trivy suppression governance and CODEOWNERS review surfaces;
|
||||
- the quality-gate matrix and its drift verifier;
|
||||
- quality, filesystem vulnerability, and documentation-link workflows;
|
||||
- human-readable dependency severity, suppression, network, and forge-compatibility policy.
|
||||
|
||||
The development harness remains excluded: no `.harness`, `.agents`, `.claude`, or `.codex`
|
||||
runtime is reconstructed. Build/release supply-chain, tag release, image scanning, signing,
|
||||
provenance, SBOM, retention, and Docker root-context work belongs to the later Phase A2 slice and
|
||||
is not represented as a present workflow job.
|
||||
|
||||
## Considered approaches
|
||||
|
||||
1. Copy the candidate files unchanged. Rejected because they target `master`, refer to missing
|
||||
supply-chain scripts/jobs, and describe obsolete branch ownership.
|
||||
2. Reconstruct a minimal current control plane from repository evidence. Selected because every
|
||||
gate can be checked against a present Gradle task, test, script, or workflow job.
|
||||
3. Merge all checks into one workflow. Rejected because GitHub-only dependency APIs need forge
|
||||
guards, scheduled vulnerability scans have different triggers, and link checks are path-scoped.
|
||||
|
||||
## Components and gate flow
|
||||
|
||||
`ci-quality-gates.yml` runs three required jobs: the aggregate Gradle quality suite, the sample-off
|
||||
axis, and gate-matrix lint. Before Java setup or Gradle, the quality job requires
|
||||
`docs/security/public-paths-snapshot.txt` to be committed and non-empty. The worktree now contains
|
||||
the canonical baseline for `/api/healthcheck`; because agents do not stage or commit, a human must
|
||||
track and commit it before CI's `git ls-files` precondition can pass. This prevents
|
||||
`verifyPublicPathSnapshot` from creating a first-run baseline inside CI and passing without
|
||||
comparison.
|
||||
|
||||
`release-gate` uses `if: always()` and accepts only `success` from those three jobs; the advisory
|
||||
quarantine job is deliberately outside its `needs`.
|
||||
|
||||
The quality aggregate runs `check`, `verifyPublicPathSnapshot`, and `verifyDependencyLocks`
|
||||
explicitly. `check` already pulls in Clean Architecture dependency enforcement, environment/readme
|
||||
drift checks, Trivy-ignore governance, format/static analysis, normal tests, and quarantine sunset.
|
||||
|
||||
`dependency-vulnerability.yml` keeps GitHub Dependency Graph operations behind
|
||||
`github.server_url == 'https://github.com'`. Platform-neutral `trivy-fs` runs for PR, `main` push,
|
||||
daily schedule, and manual dispatch. Trivy and jq install into `${RUNNER_TEMP}` and expose their
|
||||
directories through `${GITHUB_PATH}`. Every Trivy scan names `.trivyignore.yaml`; High/Critical and
|
||||
KEV matches block, while Medium/Low only report. The KEV gate first rejects blank metadata,
|
||||
non-positive/non-integral or mismatched counts, empty arrays, invalid CVE identifiers, and duplicate
|
||||
identifiers. It separately rejects malformed/empty Trivy JSON before extracting candidate IDs.
|
||||
Dependency review reports through its check only and does not request permission to write a PR
|
||||
summary comment. Vulnerability DB, tool release, malformed/empty KEV or Trivy data, and KEV feed
|
||||
network failures remain blocking unless internal mirrors are configured.
|
||||
|
||||
`link-check.yml` is path-scoped for PR and `main` push, and remains manually runnable.
|
||||
|
||||
## Drift verification and failure behavior
|
||||
|
||||
`.github/ci-gate-matrix.yml` lists only current mechanisms/jobs. The verifier resolves the
|
||||
repository root from its own physical location, rejects incomplete/duplicate records, and checks
|
||||
referenced Gradle custom tasks, plugins, contract-test files, workflow files, and job IDs.
|
||||
Delegated-pending is supported only when a row is explicitly marked; no absent supply-chain job is
|
||||
invented in this slice.
|
||||
|
||||
The CI release fan-in fails for failed, cancelled, or unexpectedly skipped required jobs. Trivy's
|
||||
KEV feed cross-check is fail-closed. GitHub-only jobs may skip by their explicit forge/event
|
||||
conditions and are not dependencies of the quality release fan-in.
|
||||
|
||||
## Verification
|
||||
|
||||
Acceptance requires the prescribed RED for the absent `.trivyignore.yaml`, GREEN
|
||||
`verifyTrivyignore`, proof that the snapshot precondition rejects missing, empty, or untracked
|
||||
baselines, and a human-tracked canonical snapshot for CI. It also requires strict synthetic KEV
|
||||
catalog negative/positive cases, shell syntax and matrix verification, workflow YAML/static checks,
|
||||
evidence that `main` is the only active branch trigger, Trivy ignorefile coverage, exact release
|
||||
fan-in, absence of harness/Gitea shadow workflows, `git diff --check`, and reviewed working-tree
|
||||
status. Network Trivy scans are intentionally not run locally.
|
||||
@@ -0,0 +1,389 @@
|
||||
# Module and Gradle Hygiene Refactoring Design
|
||||
|
||||
- **Date:** 2026-07-25
|
||||
- **Status:** Approved
|
||||
- **Scope:** all 19 Gradle leaf modules, their project/external dependencies, test conventions,
|
||||
architecture-analysis classpath, runtime composition, and dependency locks
|
||||
- **Source:** repository audit performed on 2026-07-25 against commit `821fe00`
|
||||
|
||||
## 1. Prerequisite
|
||||
|
||||
CI recovery is a hard prerequisite, not part of this refactoring. The implementation may start only
|
||||
after the repository again contains the harness registry and CI contract assets and these commands
|
||||
reach task execution:
|
||||
|
||||
```bash
|
||||
cd src
|
||||
./gradlew projects --console=plain
|
||||
./gradlew :app-bootstrap:test --tests \
|
||||
'dev.caskeleton.bootstrap.contract.DeveloperExperienceContractTest' --console=plain
|
||||
./gradlew :app-bootstrap:test --tests \
|
||||
'dev.caskeleton.bootstrap.contract.SampleRemovalSmokeContractTest' --console=plain
|
||||
./gradlew verifyTrivyignore --console=plain
|
||||
```
|
||||
|
||||
At audit time `settings.gradle` fails before project configuration because
|
||||
`.harness/project/modules.yaml` is absent. `.tool-versions`, `.trivyignore.yaml`, and the workflow
|
||||
files read by the two contract tests are absent as well. Dependency removal must not be mixed with
|
||||
that recovery because a red baseline cannot distinguish a pre-existing CI failure from a refactoring
|
||||
regression.
|
||||
|
||||
## 2. Problem Statement
|
||||
|
||||
The module direction is broadly clean, but the declared Gradle graph is wider than the source graph:
|
||||
many leaves declare every allowed core dependency even when they use only one contract. Pure-core
|
||||
tests inherit Spring MVC from a global convention. `application-core` imports SLF4J for one outbox
|
||||
use case and therefore carries the complete Spring Boot starter at compile and runtime. Several
|
||||
leaves retain unused Groovy, Spock, generated-stub, UUID, or configuration-processor dependencies.
|
||||
|
||||
The existing central ArchUnit suite analyzes whatever happens to be on the
|
||||
`app-bootstrap` test runtime classpath. Optional leaves are therefore not guaranteed to be analyzed.
|
||||
The sample-isolation contract also carries a hard-coded subset of modules instead of reading the
|
||||
19-leaf registry. Locking is strict, but the lock verifier is not a release-gate dependency and
|
||||
non-BOM version ownership is scattered.
|
||||
|
||||
This design reduces the graph only after characterization, makes topology and architecture coverage
|
||||
registry-driven, restores pure-core test isolation, and separates application logging intent from
|
||||
the logging framework.
|
||||
|
||||
## 3. Evidence Classification
|
||||
|
||||
### 3.1 Observed facts
|
||||
|
||||
The following findings are deterministic observations and do not need dependency-removal debate:
|
||||
|
||||
1. `src/settings.gradle` cannot configure without `.harness/project/modules.yaml`.
|
||||
2. There are exactly 19 leaf `build.gradle` files and 19 leaf `gradle.lockfile` files.
|
||||
3. No production configuration depends on `:sample-portfolio`;
|
||||
`app-bootstrap` has one test-only `sampleFixture` edge.
|
||||
4. The only adapter-to-adapter project edges are:
|
||||
`messaging`, `cache-redis`, `notification`, and `httpclient` to
|
||||
`adapter:outbound:support`.
|
||||
5. `domain-core` and `shared-contract` main source contain no Spring, JPA, Jackson, or SLF4J imports.
|
||||
6. `application-core` main source contains no Spring import. Its only framework imports are SLF4J in
|
||||
`PublishPendingOutboxEventsUseCase`.
|
||||
7. The root build adds Spring Boot test and Spring MVC test starters to every leaf.
|
||||
8. `cache-redis`, `messaging`, and `notification` have no Groovy tests although their builds apply
|
||||
Groovy and add Spock.
|
||||
9. `adapter:outbound:identifier` does not use `uuid-creator`.
|
||||
10. `src/sample-portfolio/.jqwik-database` is a tracked Java-serialization runtime artifact.
|
||||
11. `persistence-mongo` owns adapter-local `Example*` domain/document/repository/mapper types and its
|
||||
repository adapter implements no application/domain port.
|
||||
|
||||
### 3.2 Static candidates
|
||||
|
||||
The following are source-reference candidates, not approved removals. Each must first pass a
|
||||
leaf-specific compile/test characterization:
|
||||
|
||||
| Leaf | Candidate project edges |
|
||||
| --- | --- |
|
||||
| `adapter:inbound:graphql` | `application-core`, `domain-core` |
|
||||
| `adapter:inbound:grpc` | `application-core`, `domain-core` |
|
||||
| `adapter:inbound:web` | `domain-core` |
|
||||
| `adapter:inbound:websocket` | `application-core`, `shared-contract` |
|
||||
| `adapter:outbound:cache-redis` | `domain-core`, `application-core` |
|
||||
| `adapter:outbound:httpclient` | `domain-core`, `application-core` |
|
||||
| `adapter:outbound:identifier` | `domain-core` |
|
||||
| `adapter:outbound:messaging` | `domain-core` |
|
||||
| `adapter:outbound:notification` | `domain-core` |
|
||||
| `adapter:outbound:persistence-jpa` | `domain-core` |
|
||||
| `adapter:outbound:persistence-mongo` | `application-core`, `shared-contract` |
|
||||
| `adapter:outbound:support` | `domain-core`, `application-core`, `shared-contract` |
|
||||
|
||||
The same characterization rule applies to these external candidates:
|
||||
|
||||
- GraphQL configuration processor and JSR-310 module.
|
||||
- gRPC protobuf/stub/annotations dependencies in the no-generated-stub skeleton.
|
||||
- broad `spring-boot-starter` usage in gRPC, fileserver, and objectstorage.
|
||||
- explicit Flyway core where the starter already supplies the required API.
|
||||
- duplicate starter/test declarations in app-bootstrap and sample-portfolio.
|
||||
|
||||
An allowed registry edge is permission, not a requirement to declare that edge.
|
||||
|
||||
## 4. Goals
|
||||
|
||||
1. Keep all module paths and allowed edges in `.harness/project/modules.yaml` only.
|
||||
2. Make the actual project DAG the smallest graph required by source, tests, and runtime
|
||||
composition.
|
||||
3. Preserve the approved outbox failure-reporting refactor's removal of Spring and logging
|
||||
frameworks from `application-core` compile/runtime classpaths.
|
||||
4. Give `domain-core`, `application-core`, and `shared-contract` framework-free test conventions.
|
||||
5. Enforce external dependency purity for core modules from resolved compile/runtime graphs.
|
||||
6. Analyze every registered production leaf with the architecture suite regardless of runtime
|
||||
composition.
|
||||
7. Apply the Spring configuration processor exactly where main source declares
|
||||
`@ConfigurationProperties`.
|
||||
8. Verify strict locks in the release gate and assign one owner to every non-BOM version.
|
||||
9. State which optional adapters are in the default app runtime and which are opt-in.
|
||||
10. Remove generated jqwik state from source control.
|
||||
11. Remove the Mongo adapter-local example domain from production without creating a duplicate
|
||||
sample implementation.
|
||||
|
||||
## 5. Non-Goals
|
||||
|
||||
- No feature behavior, endpoint, persistence schema, or public contract change.
|
||||
- No conversion to convention plugins, `buildSrc`, an included build, or a version catalog in this
|
||||
change. Build-logic migration starts only from a green post-refactoring baseline.
|
||||
- No automatic inclusion of every optional adapter in the production runtime.
|
||||
- No new Mongo business port or second Mongo sample in `sample-portfolio`.
|
||||
- No relocation of the shared ThreadLocal implementation in this change.
|
||||
- No LLM Wiki write as part of this documentation-only design task.
|
||||
|
||||
## 6. Target Topology and Registry Policy
|
||||
|
||||
The registry remains the only topology authority. Every leaf entry must continue to own:
|
||||
|
||||
- stable id
|
||||
- source path
|
||||
- Gradle path
|
||||
- role/family
|
||||
- allowed project dependencies
|
||||
- focused command
|
||||
- nearest module guidance
|
||||
|
||||
Each runtime-capable leaf also receives one explicit runtime membership:
|
||||
|
||||
- `core`: contract/core leaf consumed by registered adapters or bootstrap.
|
||||
- `app-default`: present on the default `app-bootstrap` runtime classpath.
|
||||
- `opt-in`: built and architecture-analyzed but absent from the default application runtime.
|
||||
- `sample-only`: used only by the sample fixture/runtime.
|
||||
- `composition-root`: `app-bootstrap` or `sample-portfolio` itself.
|
||||
|
||||
The registry validator rejects missing membership, unknown dependency ids, duplicate Gradle paths,
|
||||
production edges to `sample-portfolio`, adapter peer edges not explicitly allowed by the source
|
||||
module's `allowed_dependencies`, and cycles. Gradle settings, the project-dependency verifier,
|
||||
sample-isolation checks, and architecture-analysis classpath all consume this data. No Java test
|
||||
keeps a copied module list.
|
||||
|
||||
The current default runtime membership is preserved during graph cleanup. Optional adapters do not
|
||||
become runtime dependencies merely because architecture analysis needs their classes.
|
||||
|
||||
## 7. Approved Application Logging Boundary
|
||||
|
||||
The logging-boundary implementation is owned by
|
||||
`docs/superpowers/specs/2026-07-25-application-outbox-failure-reporting-design.md` and its matching
|
||||
implementation plan. That design is a prerequisite for dependency pruning in this plan and is not
|
||||
redefined here.
|
||||
|
||||
The selected contract is:
|
||||
|
||||
- `application-core` owns `OutboxRelayFailureReportPort`;
|
||||
- the port has one `report(OutboxRelayFailureReport)` method;
|
||||
- the safe immutable report carries the approved FAILED/DEAD operational fields and never carries
|
||||
payload or idempotency data;
|
||||
- `adapter:outbound:messaging` owns the structured SLF4J reporter implementation;
|
||||
- `app-bootstrap` injects the port into the manually assembled relay use case;
|
||||
- application source and dependency guardrails prevent Spring and logging frameworks from returning
|
||||
to `application-core`.
|
||||
|
||||
Module hygiene begins only after that focused plan is green. This design then verifies the resulting
|
||||
application dependency purity and removes unrelated static-candidate edges; it does not introduce a
|
||||
second reporting port or relocate reporter ownership.
|
||||
|
||||
## 8. Test Dependency Conventions
|
||||
|
||||
Test dependencies are role-specific:
|
||||
|
||||
| Role | Baseline |
|
||||
| --- | --- |
|
||||
| `domain-core` | JUnit Jupiter API/engine and AssertJ only when tests exist |
|
||||
| `application-core` | JUnit Jupiter, AssertJ; hand-written fakes; no Spring context |
|
||||
| `shared-contract` | JUnit Jupiter and AssertJ; no Spring context |
|
||||
| inbound web/GraphQL/WebSocket/gRPC | transport test modules required by that protocol only |
|
||||
| persistence adapters | mapping/unit baseline plus datastore Testcontainers only where vendor behavior is tested |
|
||||
| other outbound adapters | JUnit/Spock selected by actual test language; fake external systems |
|
||||
| `app-bootstrap` | Spring Boot context/slice support, ArchUnit, and integration-test dependencies |
|
||||
| `sample-portfolio` | feature, property, slice, and integration-test dependencies owned by the sample |
|
||||
|
||||
The root build may supply JUnit platform launch/runtime configuration, but it must not supply Spring
|
||||
MVC or Spring context libraries to every leaf. A test dependency belongs in the leaf that uses it.
|
||||
|
||||
## 9. External Dependency Purity Gate
|
||||
|
||||
The logging-boundary plan first introduces `verifyApplicationCoreDependencyPurity`. This refactoring
|
||||
then replaces that task with the registry-wide `verifyExternalDependencyPurity`; the two tasks do
|
||||
not remain as overlapping gates. The replacement preserves the application main/test classpath
|
||||
rules and `:application-core:check` wiring, then resolves each registered leaf's production
|
||||
`compileClasspath` and `runtimeClasspath` and applies the broader role rules:
|
||||
|
||||
- `domain-core` and `shared-contract`: no external production module at all.
|
||||
- `application-core`: no Spring, SLF4J/logging backend, JPA/Hibernate, servlet, transport, database,
|
||||
cloud, or adapter implementation dependency.
|
||||
- inbound/outbound adapters: no logging implementation dependency; SLF4J API is allowed.
|
||||
- all production leaves: no test framework on production configurations.
|
||||
|
||||
The task reports `module → configuration → forbidden coordinate → rule`. It checks resolved
|
||||
coordinates so transitive framework leakage is visible. Existing project-edge verification remains
|
||||
separate and registry-driven.
|
||||
|
||||
Because the registry and harness API were absent at design time, the exact Python/Groovy/Java
|
||||
implementation of registry membership, purity/processor gates, and architecture classpath wiring is
|
||||
written in a post-recovery implementation addendum after the stable task packet is resolved. The
|
||||
addendum must contain complete code against the recovered API and pass review before any of those
|
||||
control-plane files are changed. Entry is fail-closed on a concrete overlay, an actual resolver
|
||||
invocation, matching overlay/packet content hashes, and the resolved rule hash; `--help` output or a
|
||||
prose-only confirmation is not packet evidence.
|
||||
|
||||
## 10. Registry-Driven Architecture Analysis
|
||||
|
||||
`app-bootstrap` gets an `architectureAnalysis` dependency bucket populated from every registered
|
||||
production leaf, independent of `app-default` runtime membership. The architecture test runtime
|
||||
extends this bucket; the application production runtime does not.
|
||||
|
||||
`CleanArchitectureTest` therefore sees GraphQL, gRPC, WebSocket, fileserver, objectstorage, Mongo,
|
||||
and every other registered leaf. `SampleRemovalSmokeContractTest` reads production module paths from
|
||||
the same registry instead of its current hard-coded list.
|
||||
|
||||
The architecture configuration is non-consumable and non-resolvable itself; only the dedicated test
|
||||
runtime is resolvable. This prevents it from being published or accidentally used by `bootJar`.
|
||||
|
||||
## 11. Configuration Processor Consistency
|
||||
|
||||
The rule is mechanical:
|
||||
|
||||
- a leaf with main-source `@ConfigurationProperties` declares
|
||||
`annotationProcessor 'org.springframework.boot:spring-boot-configuration-processor'`;
|
||||
- a leaf without it does not.
|
||||
|
||||
A verification task scans registered main source roots and compares the result with the declared
|
||||
annotation-processor dependency. This removes the unused GraphQL processor and adds missing
|
||||
processors to settings-owning leaves without relying on a copied module list.
|
||||
|
||||
Binding/validation tests remain required for every settings class; generated metadata is not a
|
||||
substitute for behavior tests. The post-recovery control-plane addendum inventories that mapping and
|
||||
contains complete focused test code for every uncovered class before processor declarations change.
|
||||
|
||||
## 12. Runtime Composition and Component Scanning
|
||||
|
||||
`app-bootstrap` keeps only registry members marked `app-default` on its production runtime.
|
||||
`opt-in` modules remain independently buildable and architecture-analyzed. Adoption of an opt-in
|
||||
module is an explicit registry and composition-root change with its own focused tests.
|
||||
|
||||
`CaSkeletonApplication` narrows component and configuration-properties scanning to:
|
||||
|
||||
- `dev.caskeleton.bootstrap`
|
||||
- `dev.caskeleton.adapter`
|
||||
|
||||
It removes `dev.caskeleton.application`, `dev.caskeleton.domain`, and `dev.caskeleton.shared` from
|
||||
both scans. Those core packages own no Spring component or configuration-properties class, and the
|
||||
outbox use case remains manually composed. A context test pins this boundary.
|
||||
|
||||
This is the smallest safe scan change in this refactoring. Converting all adapter configuration to
|
||||
explicit `@Import` or auto-configuration is a separate design change.
|
||||
|
||||
## 13. Leaf-Specific Cleanup
|
||||
|
||||
All 19 leaves receive a characterization record and focused command:
|
||||
|
||||
| Leaf | Target decision |
|
||||
| --- | --- |
|
||||
| `domain-core` | preserve zero-framework main; isolate pure tests |
|
||||
| `application-core` | consume the approved outbox-reporting result; verify Boot/SLF4J remain absent |
|
||||
| `shared-contract` | preserve stdlib-only production graph |
|
||||
| `adapter:inbound:web` | remove only compile-proven unused core edge; retain transport dependencies |
|
||||
| `adapter:inbound:graphql` | remove compile-proven core/tooling candidates |
|
||||
| `adapter:inbound:grpc` | retain server/health/reflection runtime; remove no-stub candidates only after compile |
|
||||
| `adapter:inbound:websocket` | retain domain-event and WebSocket dependencies; prune unused core edges |
|
||||
| `adapter:outbound:support` | retain only compile-proven core edges and SLF4J API/autoconfigure |
|
||||
| `adapter:outbound:cache-redis` | remove unused core edges and unused Groovy/Spock |
|
||||
| `adapter:outbound:fileserver` | keep application/shared ports; narrow starter only after characterization |
|
||||
| `adapter:outbound:httpclient` | keep shared/support and actual Groovy/Spock tests; prune unused core edges |
|
||||
| `adapter:outbound:identifier` | keep application pseudonymizer port; remove unused domain/uuid-creator |
|
||||
| `adapter:outbound:messaging` | retain the approved outbox reporter plus application/shared/support; remove unused domain and Groovy/Spock |
|
||||
| `adapter:outbound:notification` | keep application/shared/support; remove unused domain and Groovy/Spock |
|
||||
| `adapter:outbound:objectstorage` | keep application/shared/AWS SDK; narrow starter only after characterization |
|
||||
| `adapter:outbound:persistence-jpa` | keep application/shared/JPA/vendor runtime; test domain/Flyway candidates |
|
||||
| `adapter:outbound:persistence-mongo` | remove all adapter-local `Example*` types/tests; keep generic opt-in config/properties and binding/disabled-mode tests; remove application/shared edges |
|
||||
| `app-bootstrap` | preserve composition role; separate architecture classpath; narrow scans and duplicate tests |
|
||||
| `sample-portfolio` | preserve fixture-only isolation; remove generated jqwik state and own sample-only dependencies |
|
||||
|
||||
## 14. Generated jqwik State
|
||||
|
||||
`src/sample-portfolio/.jqwik-database` is deleted from version control.
|
||||
`src/.gitignore` ignores `.jqwik-database` at any module working directory. Property tests remain
|
||||
deterministic from committed seeds/configuration rather than a developer-machine serialization
|
||||
cache.
|
||||
|
||||
## 15. Shared ThreadLocal Decision
|
||||
|
||||
`ThreadLocalDomainContextPropagator` and `DomainContextPropagatorFactory` stay in
|
||||
`shared-contract` for this refactoring. They are Java-stdlib-only operational infrastructure, and
|
||||
moving them changes concurrency composition rather than dependency hygiene.
|
||||
|
||||
This is a deliberate secondary decision, not an accidental omission. The purity gate pins their
|
||||
zero-external-dependency status. Relocation to bootstrap or an adapter requires a separate design
|
||||
with virtual-thread/context-propagation characterization and is not bundled into graph cleanup.
|
||||
|
||||
## 16. Persistence Mongo Decision
|
||||
|
||||
The production Mongo leaf removes:
|
||||
|
||||
- `ExampleRecord`
|
||||
- `ExampleMongoDocument`
|
||||
- `ExampleMongoMapper`
|
||||
- `ExampleMongoRepository`
|
||||
- `ExampleMongoRepositoryAdapter`
|
||||
- their example mapping/repository tests
|
||||
|
||||
`MongoPersistenceConfig` and `MongoPersistenceProperties` remain as generic opt-in Spring Mongo
|
||||
machinery. Tests cover properties binding, disabled-by-default behavior, and mock-backed
|
||||
enabled-mode creation of one Boot 4 `MongoClient` and `MongoTemplate` without a network connection
|
||||
or sample repository.
|
||||
|
||||
No duplicate Mongo domain is added to `sample-portfolio`; the WorkLog JPA sample remains the sole
|
||||
reference business domain. The Mongo leaf then removes its unused application/shared project edges.
|
||||
|
||||
## 17. Locking and Version Ownership
|
||||
|
||||
After each leaf cleanup:
|
||||
|
||||
1. run its compile and focused test;
|
||||
2. regenerate its lock state through the repository `resolveAndLockAll --write-locks` entrypoint;
|
||||
3. run `verifyDependencyLocks`;
|
||||
4. inspect that removed coordinates disappeared from production configurations.
|
||||
|
||||
`check` or the CI release gate invokes `verifyDependencyLocks`. Spring Boot BOM owns managed Spring,
|
||||
Jackson, Micrometer, Testcontainers, and related versions. Existing root extension values own gRPC,
|
||||
protobuf, and AWS BOM versions. Every remaining non-BOM direct version has one root-level owner.
|
||||
|
||||
The build remains Groovy DSL with the current root configuration during this work. A version catalog
|
||||
or convention-plugin migration is considered only after the complete refactoring and full `check`
|
||||
are green, so build-system migration cannot mask dependency-removal regressions.
|
||||
|
||||
## 18. Verification Strategy
|
||||
|
||||
Verification proceeds from narrow to broad:
|
||||
|
||||
1. CI/harness prerequisite commands.
|
||||
2. Registry schema, cycle, membership, and 19-leaf parity tests.
|
||||
3. Before/after dependency reports for each candidate leaf.
|
||||
4. Leaf `compileJava`, focused test, and configuration-processor check.
|
||||
5. Pure-core external dependency gate.
|
||||
6. Registry-driven project dependency and architecture tests.
|
||||
7. Sample-off and optional-runtime composition tests.
|
||||
8. Dependency lock verification.
|
||||
9. Full `test` and `check`.
|
||||
|
||||
No removal is accepted when a focused command is skipped without a named environmental reason and
|
||||
recorded residual risk.
|
||||
|
||||
## 19. Acceptance Criteria
|
||||
|
||||
- All four prerequisite commands pass before hygiene edits begin.
|
||||
- Registry validation reports exactly 19 unique, acyclic leaves and owns runtime membership.
|
||||
- No copied production-module list remains in architecture or sample-isolation tests.
|
||||
- The approved outbox failure-reporting plan is green before hygiene pruning starts.
|
||||
- `application-core` production dependencies continue to contain no Spring or logging coordinate.
|
||||
- Domain, application, and shared tests run without Spring MVC/context dependencies.
|
||||
- Every production leaf is present on the architecture-analysis test runtime.
|
||||
- No production configuration depends on `sample-portfolio`.
|
||||
- Every adapter peer edge is explicitly allowed by the source module's registry entry; the current
|
||||
graph's peer edges all target `adapter:outbound:support`.
|
||||
- Configuration processor declarations exactly match main-source properties classes.
|
||||
- Every removed project/external dependency has before/after compile and focused-test evidence.
|
||||
- Mongo production source contains no `Example*` domain/document/repository/mapper type.
|
||||
- `.jqwik-database` is untracked and ignored.
|
||||
- `verifyExternalDependencyPurity`, `verifyCleanArchitectureDependencies`, architecture tests,
|
||||
`verifyDependencyLocks`, `test`, and `check` pass.
|
||||
- No convention-plugin or version-catalog migration is included.
|
||||
- Agents do not stage, commit, amend, or push; commit policy remains human-only.
|
||||
@@ -0,0 +1,173 @@
|
||||
# Harness-Free Module and Gradle Hygiene Design
|
||||
|
||||
- **Date:** 2026-07-25
|
||||
- **Status:** Approved
|
||||
- **Mode:** B reconstruction without `.harness`
|
||||
- **Scope:** all 19 Gradle leaves, dependency declarations, test baselines, Mongo scaffolding,
|
||||
runtime-composition documentation, and dependency locks
|
||||
- **Topology SSOT:** `src/config/architecture/modules.json`
|
||||
|
||||
## 1. Context
|
||||
|
||||
The 19-leaf project dependency graph obeys the registered allowed edges, and the three core
|
||||
production source sets are free of Spring, persistence, transport, logging, and metrics imports.
|
||||
The audit nevertheless found a wider declared graph than the source graph, Spring WebMVC test
|
||||
libraries on pure-core test classpaths, Boot 3-era OpenAPI tooling on Spring Boot 4, example-domain
|
||||
code in the production Mongo adapter, and direct MDC access in sample application services.
|
||||
|
||||
This design follows the user-approved Mode B reconstruction. It does not recreate or depend on
|
||||
`.harness`; settings and verification continue to consume the JSON registry.
|
||||
|
||||
## 2. Goals
|
||||
|
||||
1. Keep the exact 19 leaves and all allowed project edges in the JSON registry.
|
||||
2. Remove only dependencies proven unnecessary by source/test inspection plus focused
|
||||
compile/test verification.
|
||||
3. Give `domain-core`, `application-core`, and `shared-contract` JUnit/AssertJ-only test
|
||||
classpaths.
|
||||
4. Keep Spring Boot 4.0.0 and replace `springdoc-openapi` 2.x with the Boot 4-compatible 3.0.0
|
||||
line.
|
||||
5. Remove unused direct Jackson 2 declarations from GraphQL and WebSocket.
|
||||
6. Require the Spring configuration processor exactly in leaves whose main source declares
|
||||
`@ConfigurationProperties`.
|
||||
7. Remove adapter-local `Example*` business concepts from `persistence-mongo`; retain only
|
||||
opt-in Mongo infrastructure and typed enablement settings.
|
||||
8. Replace sample application-layer MDC reads with an application-owned correlation-context port
|
||||
implemented by the inbound web adapter.
|
||||
9. Remove tracked jqwik runtime state and ignore future `.jqwik-database` files.
|
||||
10. Describe the default bootstrap as the default runtime composition, not as wiring every
|
||||
optional leaf.
|
||||
11. Regenerate only affected strict dependency locks and finish with the full release gates.
|
||||
|
||||
## 3. Non-goals
|
||||
|
||||
- No endpoint, persistence schema, public response, outbox transition, or sample-domain behavior
|
||||
change.
|
||||
- No version catalog, convention-plugin, `buildSrc`, module rename, or registry schema expansion.
|
||||
- No automatic addition of GraphQL, gRPC, WebSocket, Mongo, file server, or object storage to the
|
||||
default `app-bootstrap` runtime.
|
||||
- No stage, commit, amend, or push.
|
||||
|
||||
## 4. Approved dependency decisions
|
||||
|
||||
An allowed registry edge is permission, not an obligation to declare it.
|
||||
|
||||
| Leaf | Remove after focused proof | Preserve |
|
||||
| --- | --- | --- |
|
||||
| `application-core` | unused `domain-core` edge | `shared-contract` |
|
||||
| `inbound:web` | unused `domain-core` edge | application/shared and transport dependencies |
|
||||
| `inbound:graphql` | application/domain edges, direct Jackson 2, unused processor | shared and GraphQL/web test transport |
|
||||
| `inbound:grpc` | application/domain edges, unused annotations/direct protobuf declarations | shared, netty, services, configuration processor |
|
||||
| `inbound:websocket` | application/shared edges, direct Jackson 2 | domain, WebSocket, configuration processor |
|
||||
| `outbound:support` | domain/application/shared edges | autoconfigure and SLF4J API |
|
||||
| `outbound:cache-redis` | domain/application, unused Groovy/Spock | shared/support |
|
||||
| `outbound:httpclient` | domain/application | shared/support, actual Groovy/Spock tests |
|
||||
| `outbound:identifier` | domain, `uuid-creator` | application, actual Groovy/Spock tests |
|
||||
| `outbound:messaging` | domain, unused Groovy/Spock | application/shared/support/SLF4J |
|
||||
| `outbound:notification` | domain, unused Groovy/Spock | application/shared/support/web/SLF4J |
|
||||
| `outbound:persistence-jpa` | domain; explicit Flyway core only if focused compile proves the starter sufficient | application/shared/JPA/PostgreSQL |
|
||||
| `outbound:persistence-mongo` | application/shared, `Example*`, example Testcontainers tests | Mongo opt-in infrastructure/settings |
|
||||
| `outbound:fileserver` | broad Boot starter | application/shared, autoconfigure, SLF4J |
|
||||
| `outbound:objectstorage` | broad Boot starter | application/shared/AWS, autoconfigure, SLF4J, vendor IT |
|
||||
|
||||
Production composition-root dependencies remain even when bootstrap source does not statically
|
||||
import their types: their purpose is runtime assembly. Duplicate test declarations may be removed
|
||||
only when the focused test classpath continues to compile and execute.
|
||||
|
||||
## 5. Pure-core test and verification policy
|
||||
|
||||
`domain-core`, `application-core`, and `shared-contract` receive only JUnit Jupiter, AssertJ, and
|
||||
the JUnit launcher from the root convention. All other leaves keep the existing Spring test
|
||||
baseline in this change; family-wide convention plugins are out of scope.
|
||||
|
||||
The existing application dependency-purity gate remains. A new registry-driven configuration
|
||||
processor parity gate applies this Boolean invariant to every leaf and is wired into `check`:
|
||||
main source contains one or more exact `@ConfigurationProperties(` occurrences if and only if the
|
||||
leaf `build.gradle` contains exactly one Spring configuration-processor declaration. It must ignore
|
||||
`@ConfigurationPropertiesScan`; the number of settings classes is not compared with the number of
|
||||
processor declarations.
|
||||
|
||||
## 6. Spring Boot 4 compatibility
|
||||
|
||||
The web adapter changes
|
||||
`org.springdoc:springdoc-openapi-starter-webmvc-api:2.8.6` to `3.0.0`, the first stable
|
||||
springdoc line released for Spring Boot 4.0.0. The existing sample tests that boot a real server
|
||||
and call `/v3/api-docs` are the behavior gate. Snapshot changes are accepted only if they are a
|
||||
deterministic library-version result and retain the public API contract.
|
||||
|
||||
Springdoc 3 otherwise widens `ApiError.details` from the committed `type: object` to an
|
||||
unconstrained OAS 3.1 schema. A web-adapter-owned `OpenApiCustomizer` must restore the object schema
|
||||
in the final generated document. Both real-server test applications import that production
|
||||
configuration. `shared-contract` remains free of Swagger annotations and dependencies.
|
||||
|
||||
GraphQL and WebSocket remove direct `com.fasterxml.jackson` declarations because neither source
|
||||
set imports them and Spring Boot 4 owns its JSON stack through the relevant starters.
|
||||
The web adapter retains the `JsonNullable` value type, but its `0.2.6` artifact also declares
|
||||
Jackson 2 transitively while this repository supplies explicit Jackson 3 serializers. Before and
|
||||
after dependency insight plus focused present/null/undefined serialization tests determine whether
|
||||
that transitive edge can be excluded. Exclusion is applied only if those tests and the real-server
|
||||
OpenAPI tests pass; springdoc/Swagger's independently required JSON graph is not removed by
|
||||
assumption.
|
||||
|
||||
## 7. Mongo production boundary
|
||||
|
||||
Delete the adapter-local `ExampleRecord`, document, mapper, repository, repository adapter, and
|
||||
their tests. `MongoPersistenceConfig` remains conditional on
|
||||
`ca-skeleton.persistence-mongo.enabled=true` and explicitly imports the Mongo client/data
|
||||
auto-configurations without owning a fake business repository.
|
||||
|
||||
The starter also registers Mongo auto-configuration directly through Boot metadata, independently
|
||||
of `MongoPersistenceConfig`. A module-level `AutoConfigurationImportFilter`, registered through
|
||||
Boot 4's `META-INF/spring.factories` discovery path, must exclude the Boot 4 sync/reactive client,
|
||||
data, repository, health, and metrics Mongo auto-configurations while the enable property is absent
|
||||
or false. It must allow them unchanged when the property is true; consumers must not need to set
|
||||
`spring.autoconfigure.exclude`.
|
||||
|
||||
Replacement tests must prove:
|
||||
|
||||
- an actual `@EnableAutoConfiguration` context in default/false mode creates no Mongo
|
||||
infrastructure;
|
||||
- properties bind the enable flag;
|
||||
- enabled mode can create the infrastructure with a supplied mock `MongoClient`, without a real
|
||||
network connection;
|
||||
- production source contains no `Example*` type.
|
||||
|
||||
The Testcontainers dependencies leave this module when the example repository IT is removed.
|
||||
|
||||
## 8. Correlation context boundary
|
||||
|
||||
`application-core` owns a framework-free `CorrelationIdPort` whose read result is optional.
|
||||
`adapter:inbound:web` implements it from the sanitized request MDC correlation key.
|
||||
`CreateWorkLogUseCase` and `PosterEventPublisher` depend only on the port and preserve the current
|
||||
fallback to the generated event id when no correlation id exists.
|
||||
|
||||
Tests first pin present/blank/absent behavior and prove the sample application packages no longer
|
||||
import SLF4J/MDC. Diagnostic storage remains an adapter concern.
|
||||
|
||||
## 9. Runtime composition and generated state
|
||||
|
||||
`app-bootstrap` keeps its current default runtime modules. Its build description and README must
|
||||
state that optional leaves require an explicit registry and composition-root dependency change.
|
||||
Optional adapters remain independently buildable and testable.
|
||||
|
||||
The tracked four-byte `src/sample-portfolio/.jqwik-database` is generated runtime state. Delete it
|
||||
and add `.jqwik-database` to `src/.gitignore`; retain jqwik itself because property tests use it.
|
||||
|
||||
## 10. Verification
|
||||
|
||||
Run focused compile/tests before and after each dependency group. Regenerate locks only through
|
||||
each affected leaf's `:leaf-path:resolveAndLockAll --write-locks` task, then run:
|
||||
|
||||
```bash
|
||||
cd src
|
||||
./gradlew check --console=plain
|
||||
./gradlew test --console=plain
|
||||
./gradlew verifyCleanArchitectureDependencies --console=plain
|
||||
./gradlew verifyApplicationCoreDependencyPurity --console=plain
|
||||
./gradlew verifyConfigurationPropertiesProcessor --console=plain
|
||||
./gradlew verifyDependencyLocks --console=plain
|
||||
./gradlew verifyPublicPathSnapshot verifyEnvKeys --console=plain
|
||||
```
|
||||
|
||||
Completion requires fresh review, `git diff --check`, and an LLM Wiki branch note or an explicit
|
||||
capture blocker for the mandated exact vault path.
|
||||
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
File diff suppressed because it is too large
Load Diff
+546
@@ -0,0 +1,546 @@
|
||||
# Fileserver R2 Control Plane and Provider Selection Design
|
||||
|
||||
- Date: 2026-07-28
|
||||
- Status: 구현·전체 repository gate·독립 spec/quality review 완료
|
||||
- Scope: provider-neutral R2 control plane, explicit destination/provider selection, first
|
||||
`local-persistent` qualification provider
|
||||
- Parent:
|
||||
[Fileserver Production Capability Deep Design](2026-07-26-fileserver-production-capability-design.md)
|
||||
|
||||
## 1. 목표
|
||||
|
||||
현재 `LocalFilePublicationAdapter`의 single-node process-restart R1을 운영 topology의 기본값으로
|
||||
승격하지 않는다. 이번 increment는 다음을 구현한다.
|
||||
|
||||
1. application에는 기존 provider-neutral `FilePublicationPort`만 유지한다.
|
||||
2. adapter 내부에 destination binding, provider descriptor, durable operation/manifest/reference
|
||||
control plane을 둔다.
|
||||
3. 활성화된 Fileserver는 정확한 destination과 provider를 명시해야 하며 implicit local fallback을
|
||||
금지한다.
|
||||
4. 첫 qualification provider로 pre-provisioned persistent filesystem을 사용하는
|
||||
`local-persistent`를 구현한다.
|
||||
5. `shared-mounted`와 `sftp`가 같은 control-plane state machine을 재사용할 수 있게 하되 이번
|
||||
increment에서 가짜 provider나 동작하지 않는 bean을 만들지 않는다.
|
||||
|
||||
`local-persistent`는 container writable layer나 임시 디렉터리를 의미하지 않는다. 단일 노드 또는
|
||||
node-attached persistent volume과 private owner boundary가 증명된 환경만 대상으로 한다.
|
||||
|
||||
## 2. 비범위
|
||||
|
||||
이번 increment에 포함하지 않는다.
|
||||
|
||||
- NFS 또는 다른 shared mount의 multi-client correctness;
|
||||
- SFTP SDK, connection pool, credential, OpenSSH qualification;
|
||||
- cross-node producer fencing;
|
||||
- background reaper, retention delete, quota reservation;
|
||||
- metrics/tracing/health implementation;
|
||||
- optional content read/delete/list API;
|
||||
- object storage. Object storage는 별도 outbound leaf의 책임이다.
|
||||
|
||||
이 항목은 seam만 만들지 않는다. 실제 semantic provider를 구현하는 후속 increment에서만
|
||||
dependency, bean, setting을 추가한다.
|
||||
|
||||
## 3. 검토한 접근
|
||||
|
||||
### A. 현재 local adapter를 바로 R2로 표시
|
||||
|
||||
설정과 change surface는 작지만 provider selector, terminal manifest, opaque-reference direct
|
||||
lookup과 strict startup evidence가 없다. R2를 과장하므로 선택하지 않는다.
|
||||
|
||||
### B. Local, NFS, SFTP를 동시에 구현
|
||||
|
||||
최종 기능은 많지만 서로 다른 보장과 real-service CI가 한 change surface에 결합된다. NFS와
|
||||
OpenSSH 인프라가 없으면 검증되지 않은 provider가 남으므로 선택하지 않는다.
|
||||
|
||||
### C. Provider-neutral control plane + local-persistent 첫 qualification
|
||||
|
||||
공통 state machine과 binding을 먼저 고정하고 한 provider를 실제 crash/security 테스트로
|
||||
qualification한다. 이후 provider가 control-plane 계약을 재사용하면서도 각자의 보장을 별도로
|
||||
증명할 수 있다. 이 접근을 선택한다.
|
||||
|
||||
## 4. 계층과 모듈 경계
|
||||
|
||||
```text
|
||||
application-core
|
||||
FilePublicationPort
|
||||
FilePublishRequest
|
||||
FilePublishReceipt
|
||||
|
|
||||
v
|
||||
adapter:outbound:fileserver
|
||||
RoutingFilePublicationAdapter
|
||||
|
|
||||
+-- DestinationBindingRegistry
|
||||
+-- FilePublicationProviderRegistry
|
||||
+-- DurablePublicationCoordinator
|
||||
+-- ProviderControlPlane
|
||||
|
|
||||
+-- LocalPersistentPublicationProvider
|
||||
```
|
||||
|
||||
- application/domain에는 provider ID, filesystem path, manifest locator, Spring 또는 NIO 타입을
|
||||
추가하지 않는다.
|
||||
- `RoutingFilePublicationAdapter`만 production `FilePublicationPort` bean이다.
|
||||
- provider와 control-plane SPI는 fileserver package 내부 타입이다. 범용 filesystem/SDK API를
|
||||
public bean으로 노출하지 않는다.
|
||||
- `shared-mounted`와 `sftp` 타입 값은 구현 전까지 accepted setting으로 등록하지 않는다.
|
||||
|
||||
## 5. Application 계약 변경
|
||||
|
||||
기존 request와 opaque reference를 유지한다. R2 provider가 달성한 보장을 정확히 보고할 수 있도록
|
||||
`FilePublishReceipt.DurabilityGuarantee`에 다음 값만 추가한다.
|
||||
|
||||
```text
|
||||
FILE_AND_DIRECTORY_SYNC
|
||||
```
|
||||
|
||||
이 값은 startup probe와 process-crash qualification을 모두 통과한 provider만 반환한다.
|
||||
호출한 sync가 물리 device, volume replica 또는 storage-controller power-loss protection까지
|
||||
완료됐다는 뜻은 아니다. 그 축은 deployment/storage evidence로 별도 판정한다.
|
||||
`PROCESS_LOCAL_SYNC` 또는 `PROVIDER_ACK_ONLY`를 요구 보장보다 약한 상태에서 자동으로 R2 값으로
|
||||
올리지 않는다.
|
||||
|
||||
새 opaque reference 형식은 다음 의미를 가지되 application은 내부 segment를 해석하지 않는다.
|
||||
|
||||
```text
|
||||
fsr1.<route-token>.<file-id>.<check-digits>
|
||||
```
|
||||
|
||||
- `route-token`: destination binding의 canonical policy digest에서 재시작 안정적으로 파생한
|
||||
bounded route allowlist 값. 형식은 `r` + digest의 첫 31 lowercase hex이며 startup에서 token
|
||||
collision을 거부한다;
|
||||
- `file-id`: CSPRNG 128-bit 이상;
|
||||
- `check-digits`: accidental truncation/corruption 검출;
|
||||
- provider locator, operation ID, tenant/user ID, host/path는 포함하지 않는다.
|
||||
|
||||
Reference는 authorization token이 아니다. authorization은 application use case의 책임이다.
|
||||
|
||||
## 6. 명시적 설정과 선택
|
||||
|
||||
새 canonical prefix는 `app.fileserver`다.
|
||||
|
||||
```yaml
|
||||
app:
|
||||
fileserver:
|
||||
enabled: false
|
||||
destinations:
|
||||
local-export:
|
||||
provider-ref: local-primary
|
||||
required-publication: unique-atomic-create
|
||||
required-durability: file-and-directory-sync
|
||||
maximum-rows: 1000000
|
||||
maximum-encoded-bytes: 1073741824
|
||||
providers:
|
||||
local-primary:
|
||||
type: local-persistent
|
||||
root-directory: ${APP_FILESERVER_LOCAL_ROOT:}
|
||||
auto-create: false
|
||||
strict-path-security: true
|
||||
expected-file-store-name: ${APP_FILESERVER_LOCAL_EXPECTED_FILE_STORE_NAME:}
|
||||
expected-file-store-type: ${APP_FILESERVER_LOCAL_EXPECTED_FILE_STORE_TYPE:}
|
||||
mount-sentinel-name: .ca-fileserver-volume
|
||||
mount-sentinel-sha256: ${APP_FILESERVER_LOCAL_MOUNT_SENTINEL_SHA256:}
|
||||
expected-owner: ${APP_FILESERVER_LOCAL_EXPECTED_OWNER:}
|
||||
maximum-root-mode: "0750"
|
||||
```
|
||||
|
||||
규칙:
|
||||
|
||||
- `enabled=true`이면 destination과 provider가 각각 하나 이상 필요하다.
|
||||
- 모든 destination은 존재하는 provider 하나를 참조한다.
|
||||
- provider ID별로 provider/control/payload runtime을 정확히 하나만 만들며 같은 provider를
|
||||
참조하는 destination은 그 인스턴스를 공유한다. 서로 다른 provider ID가 같은 normalized
|
||||
root를 가리키면 동일 control namespace의 이중 소유가 되므로 startup에서 거부한다.
|
||||
- request destination에 binding이 없으면 producer 호출 전에 실패한다.
|
||||
- provider type의 기본값은 없다.
|
||||
- `local-persistent` root는 absolute, existing, pre-provisioned directory여야 한다.
|
||||
- `auto-create=true`는 `local-persistent`에서 거부한다.
|
||||
- root와 mount sentinel은 operator가 미리 만든다. Root attestation이 끝난 뒤 adapter가 private
|
||||
top-level control/data directory와 bounded hash shard를 restrictive POSIX creation mode로
|
||||
생성할 수 있으며, 생성할 때마다 parent identity와 directory sync를 확인한다.
|
||||
- container ephemeral 경로를 위한 `local-dev`는 별도 후속 profile이다. production 설정과
|
||||
같은 guarantee를 공유하지 않는다.
|
||||
- 기존 `ca-skeleton.fileserver.*`는 R1/legacy compatibility selector로만 남는다. 새 R2 설정과
|
||||
동시에 활성화되면 어느 쪽 filesystem 초기화보다 먼저 startup을 실패시킨다. 양쪽 bean
|
||||
factory가 같은 ambiguity validator를 호출해 Spring bean 생성 순서에 의존하지 않으며, 암묵
|
||||
migration이나 conditional precedence를 두지 않는다.
|
||||
- R2 settings는 unknown field를 거부해 provider/destination 키 오타를 silent fallback으로
|
||||
취급하지 않는다.
|
||||
|
||||
## 7. Startup capability compilation
|
||||
|
||||
application traffic을 받기 전에 destination별 effective descriptor를 한 번 compile한다.
|
||||
|
||||
Descriptor compilation과 first reservation은 새 설정 키 없이 같은 canonical digest helper를
|
||||
사용한다.
|
||||
|
||||
- startup descriptor는 destination ID, provider ID, limits, required guarantees,
|
||||
format/encoder revision을 length-prefixed canonical encoding으로 직렬화한
|
||||
`effectivePolicyDigest`를 freeze한다;
|
||||
- ordered schema ID/version/column contract를 같은 canonical encoding 규칙으로 계산하는
|
||||
request별 `schemaDigest`는 first reservation에서 계산한다;
|
||||
- startup descriptor는 format/encoder revision과 canonical options의 `formatPolicyDigest`를
|
||||
freeze한다;
|
||||
- `r` + `effectivePolicyDigest`의 첫 31 lowercase hex로 만든 32-character deterministic route
|
||||
token.
|
||||
|
||||
문자열 단순 연결이나 JVM/JSON map iteration order에 digest를 의존시키지 않는다. 같은 startup
|
||||
allowlist 안에서 route token이 충돌하면 더 긴 prefix로 임의 복구하지 않고 startup을 실패시킨다.
|
||||
기존 operation은 journal에 freeze된 revision/digest/token으로만 복구하며 현재 설정으로 조용히
|
||||
재해석하지 않는다.
|
||||
|
||||
`local-persistent`는 다음을 모두 검증한다.
|
||||
|
||||
1. root와 모든 ancestor가 symbolic link가 아니다.
|
||||
2. root real path가 설정 absolute path와 일치한다.
|
||||
3. configured owner와 실제 owner가 일치한다.
|
||||
4. POSIX permission이 configured maximum보다 넓지 않고 group/world writable이 아니다.
|
||||
5. `FileStore.name()`과 `type()`이 설정 값과 일치한다.
|
||||
6. mount sentinel이 regular no-follow file이고 configured SHA-256와 일치한다.
|
||||
7. data, staging, operations, manifests, references, quarantine directory가 같은
|
||||
`FileStore`에 있다.
|
||||
8. control directory는 private owner boundary이며 symlink가 아니다.
|
||||
9. `SecureDirectoryStream`을 열 수 있다.
|
||||
10. exclusive create, file force, hard-link create, directory force가 private probe directory에서
|
||||
성공한다.
|
||||
|
||||
Probe artifact는 unique name만 사용하며 successful cleanup과 parent directory force까지
|
||||
완료해야 한다. Probe 실패는 capability downgrade가 아니라 startup failure다.
|
||||
|
||||
JDK가 directory-relative hard-link primitive를 제공하지 않으므로 hard-link publish는 다음
|
||||
boundary에서만 허용한다.
|
||||
|
||||
- root/control/data directories가 adapter owner 전용이고 untrusted writer가 없음;
|
||||
- publish 직전과 직후 root identity, directory file key, mount sentinel을 다시 확인;
|
||||
- target은 CSPRNG unique name;
|
||||
- pre/post identity가 바뀌면 성공을 반환하지 않고 `PUBLISH_INDETERMINATE`;
|
||||
- privileged host administrator 또는 same-owner malicious process와의 경쟁은 guarantee 범위가
|
||||
아니며 deployment isolation requirement로 기록한다.
|
||||
|
||||
untrusted writer가 같은 root에 entry를 만들 수 있는 환경은 strict local R2가 아니다.
|
||||
|
||||
## 8. Durable control plane
|
||||
|
||||
```text
|
||||
.ca-fileserver/
|
||||
operations/<prefix>/<operation-id>.json
|
||||
manifests/<prefix>/<file-id>.json
|
||||
references/<prefix>/<file-id>.json
|
||||
staging/<prefix>/<operation-id>.part
|
||||
quarantine/
|
||||
probe/
|
||||
data/<prefix>/<generated-file-name>
|
||||
```
|
||||
|
||||
모든 locator는 validated single segment 또는 adapter가 생성한 bounded relative segment다.
|
||||
Caller path를 받지 않는다. Manifest/reference의 `internalLocator`는 generated filename 한
|
||||
segment만 저장하고, data shard는 `fileId`의 첫 두 hex에서 파생한다. 따라서 실제 lookup은
|
||||
`data/<file-id-prefix>/<internalLocator>`이며 control record에 slash를 저장하지 않는다.
|
||||
|
||||
### 8.1 Operation journal v2
|
||||
|
||||
필수 필드:
|
||||
|
||||
```text
|
||||
schemaVersion
|
||||
stateRevision
|
||||
state
|
||||
operationId
|
||||
requestFingerprint
|
||||
effectivePolicyRevision
|
||||
effectivePolicyDigest
|
||||
destinationId
|
||||
providerId
|
||||
fileId
|
||||
routeToken
|
||||
publishedFileName
|
||||
stageFileName
|
||||
byteSize
|
||||
rowCount
|
||||
columnCount
|
||||
sha256
|
||||
formulaMitigatedCount
|
||||
manifestDigest
|
||||
referenceDigest
|
||||
createdAt
|
||||
sealedAt
|
||||
publishedAt
|
||||
lastFailureCode
|
||||
receiptSnapshot
|
||||
```
|
||||
|
||||
State는 `WRITING`, `SEALED`, `DATA_PUBLISHED`, `MANIFEST_PUBLISHED`,
|
||||
`REFERENCE_PUBLISHED`, `PUBLISHED`, `QUARANTINED`다.
|
||||
|
||||
### 8.2 Private manifest v1
|
||||
|
||||
Manifest는 operation/file/provider/reference/fingerprint, schema·format·policy digest, byte/count,
|
||||
SHA-256, achieved guarantees, internal relative locator를 기록한다. Absolute path, raw row/cell,
|
||||
credential, raw tenant/user ID는 저장하지 않는다.
|
||||
|
||||
### 8.3 Reference index v1
|
||||
|
||||
Reference index는 opaque `file-id`에서 operation ID, file version, manifest digest와 internal
|
||||
relative locator로 direct lookup한다. Directory scan은 receipt restoration의 authority가 아니다.
|
||||
|
||||
### 8.4 Record update
|
||||
|
||||
각 control record는:
|
||||
|
||||
1. sibling private temp file을 `CREATE_NEW`;
|
||||
2. bounded canonical JSON encoding;
|
||||
3. file `force(true)`;
|
||||
4. same-directory atomic replace;
|
||||
5. parent directory force;
|
||||
6. read-back schema/revision/digest verification;
|
||||
|
||||
순서로 갱신한다. 낮은 revision, fingerprint mismatch, newer schema는 자동 덮어쓰지 않는다.
|
||||
|
||||
Operation schema v2는 별도 `formatPolicyDigest` snapshot을 저장하지 않으므로 recovery는 저장된
|
||||
`effectivePolicyRevision`과 `effectivePolicyDigest`가 현재 compiled destination과 정확히 같을
|
||||
때만 현재 format-policy digest를 사용한다. Encoder/policy 변경으로 digest가 달라지면 과거
|
||||
format을 추정하지 않고 indeterminate로 중단한다. 여러 format revision에 대한 forward
|
||||
recovery는 non-secret policy snapshot을 포함하는 후속 operation schema에서만 지원한다.
|
||||
|
||||
Operation direct lookup은 같은 secure relative read에서 schema를 typed dispatch한다. Schema v2는
|
||||
현재 R2 record로만 decode/write하고, schema v1은 strict UTF-8 decode 후 canonical v1 re-encode
|
||||
byte equality를 만족하는 terminal compatibility record만 read-only로 반환한다. Unknown/newer
|
||||
schema, malformed UTF-8, non-canonical v1은 absent로 취급하지 않는다.
|
||||
|
||||
Crash qualification을 위해 control-plane fault context는 package-private로 record kind,
|
||||
record identity, 해당하는 경우 operation state/revision, force boundary를 함께 전달한다.
|
||||
Production 기본 callback은 no-op이며 runtime 설정이나 public bean으로 노출하지 않는다.
|
||||
|
||||
## 9. Publication ordering
|
||||
|
||||
```text
|
||||
J-WRITING
|
||||
-> stage stream/force
|
||||
J-SEALED
|
||||
-> exclusive hard-link data publish
|
||||
-> data directory force
|
||||
J-DATA_PUBLISHED
|
||||
-> private manifest publish/force
|
||||
J-MANIFEST_PUBLISHED
|
||||
-> reference index publish/force
|
||||
J-REFERENCE_PUBLISHED
|
||||
-> terminal journal + receipt snapshot publish/force
|
||||
J-PUBLISHED
|
||||
-> receipt return
|
||||
```
|
||||
|
||||
- Producer는 accepted attempt에서 최대 한 번 호출한다.
|
||||
- `SEALED` 이후 retry/recovery는 staged bytes만 사용한다.
|
||||
- terminal journal force 전에는 receipt를 반환하지 않는다.
|
||||
- target collision, digest mismatch 또는 root identity change는 자동 overwrite하지 않는다.
|
||||
- final data가 있어도 manifest/reference가 없으면 아직 terminal success가 아니다.
|
||||
- staging/data shard 생성, stage force, stable no-follow read/digest, exact delete는
|
||||
package-private `PayloadOperations`를 통해 `SecureDirectoryStream` 상대 연산으로 수행한다.
|
||||
Portable relative primitive가 없는 hard-link와 directory force만 private-owner boundary 안에서
|
||||
root/directory/file identity pre/post 검증으로 감싼다.
|
||||
- hard-link 뒤 journal 갱신 전에 중단된 `SEALED + matching data` 복구는 기존 data shard를 다시
|
||||
identity 검증하고 directory force한 뒤에만 `DATA_PUBLISHED`로 전이한다. 이미 존재하는 data를
|
||||
overwrite-capable publication 경로에 다시 넣지 않는다.
|
||||
- `WRITING` 저장 뒤 producer 또는 stage/write가 실패하면 partial stage를 exact cleanup하고
|
||||
unsealed `QUARANTINED` evidence를 남긴다. 원래 producer exception은 보존하고 cleanup/control
|
||||
failure는 suppressed로 연결한다. Retry 진입 시 기존 `WRITING` 또는 unsealed
|
||||
`QUARANTINED`가 보이면 producer를 다시 호출하지 않고 indeterminate/quarantine으로
|
||||
fail-closed한다.
|
||||
|
||||
## 10. Deterministic recovery
|
||||
|
||||
Recovery는 operation ID direct lookup으로 실행하며 startup full scan에 의존하지 않는다.
|
||||
|
||||
| 확인된 상태 | 조치 |
|
||||
| --- | --- |
|
||||
| terminal journal + matching manifest/reference/data | 저장된 receipt 복원 |
|
||||
| SEALED + valid stage, data 없음 | data publication부터 재개 |
|
||||
| SEALED + matching data | manifest publication부터 재개 |
|
||||
| DATA_PUBLISHED + matching data | manifest publication 재개 |
|
||||
| MANIFEST_PUBLISHED + matching manifest/data | reference publication 재개 |
|
||||
| REFERENCE_PUBLISHED + all matching | terminal journal 완성 |
|
||||
| non-terminal data/manifest/reference digest mismatch | `QUARANTINED`, integrity failure |
|
||||
| `PUBLISHED` artifact/metadata/receipt mismatch | terminal journal과 artifacts를 불변 보존하고 typed integrity/indeterminate |
|
||||
| required manifest/reference/data 누락 | 성공 복원 금지, fail-closed indeterminate/quarantine |
|
||||
| marker/manifest/reference schema newer | 보존 후 fail-fast/quarantine |
|
||||
| fingerprint conflict | typed conflict, 기존 artifact 보존 |
|
||||
| root/mount identity change | indeterminate, write/recovery 중단 |
|
||||
|
||||
Truth priority:
|
||||
|
||||
```text
|
||||
matching data + private manifest + reference
|
||||
> terminal operation record
|
||||
> non-terminal operation record
|
||||
> in-memory state
|
||||
```
|
||||
|
||||
모순이 있으면 임의 성공이나 삭제를 하지 않는다. Non-terminal operation은 기존 operation
|
||||
journal을 `QUARANTINED`로 전이할 수 있다. 이미 `PUBLISHED`인 operation은 terminal
|
||||
receipt snapshot을 지우거나 journal을 덮지 않고 관련 data/manifest/reference도 보존한 채 typed
|
||||
integrity/indeterminate로 실패한다. 별도 immutable quarantine incident record는 후속 설계 전까지
|
||||
가정하지 않는다.
|
||||
|
||||
Recovery verifier는 operation, incoming request, data, manifest, reference, receipt snapshot의
|
||||
identity/digest/locator/count/time/guarantee를 모두 교차검증한다. Terminal receipt는 verified
|
||||
manifest/reference에서 재구성한 expected receipt와 전체 equality가 확인될 때만 반환한다.
|
||||
Operation record의 일부 필드만 맞거나 durability/publication guarantee, file version,
|
||||
format/media/charset가 다르면 terminal success가 아니다. Crash 뒤 먼저 발견한 immutable
|
||||
manifest/reference의 verified `publishedAt`은 새 clock 값으로 덮지 않고 recovery context로
|
||||
재사용한다. 새 attempt에만 현재 configured maximum을 적용하고, sealed recovery artifact는
|
||||
operation에 freeze된 exact byte size로 bounded inspection한다. Stage와 data가 함께 있으면
|
||||
digest equality만이 아니라 stable file key가 같은 hard-link인지 확인한 뒤에만 stage를
|
||||
exact-delete한다.
|
||||
|
||||
## 11. Compatibility
|
||||
|
||||
- R1 compatibility는 별도 미설정 root나 동시에 활성화된 legacy bean이 아니다. Operator가 기존
|
||||
R1 root를 owner/mode/FileStore/sentinel 등 R2 attestation 조건에 맞춰 명시적으로
|
||||
pre-provision한 뒤, 그 root를 R2 destination으로 전환하는 in-place read-only migration이다.
|
||||
- R1과 R2 operation journal은 같은 hashed path를 사용하므로 secure relative typed schema
|
||||
dispatch로 schema v1을 읽고 schema v2만 쓴다.
|
||||
- R1 journal schema v1은 strict UTF-8와 canonical re-encode byte equality를 만족하는 terminal
|
||||
record만 읽을 수 있어야 한다.
|
||||
- R1 terminal receipt는 기존 `PROCESS_LOCAL_SYNC` 보장 그대로 복원한다.
|
||||
- R1 root-level artifact도 attested root의 `SecureDirectoryStream` 상대 no-follow bounded
|
||||
streaming inspection으로 journal의 byte size와 SHA-256을 확인한 뒤에만 receipt를 복원한다.
|
||||
- R1 artifact를 자동으로 R2 manifest/reference로 승격하지 않는다.
|
||||
- R2 writer는 journal v2만 생성한다.
|
||||
- 기존 overwrite-capable legacy port는 별도 root와 opt-in을 유지하며 R2 control plane에 접근하지
|
||||
않는다.
|
||||
- R1과 R2 selector가 동시에 활성화되면 ambiguous composition으로 startup을 실패시킨다.
|
||||
|
||||
## 12. Failure semantics
|
||||
|
||||
- 설정/보장 mismatch: startup failure;
|
||||
- destination 없음: producer 전 deterministic request failure;
|
||||
- stage 이전 capacity/validation failure: not applied;
|
||||
- stage/write failure: failed, partial stage는 recovery evidence가 아니면 exact cleanup하고
|
||||
unsealed `QUARANTINED`로 producer replay를 차단;
|
||||
- sealed 이후 filesystem timeout/IO/root identity change: indeterminate;
|
||||
- non-terminal published data와 metadata 불일치: integrity/quarantine;
|
||||
- terminal `PUBLISHED` data/metadata/receipt 불일치: terminal evidence 불변 보존 후 typed
|
||||
integrity/indeterminate;
|
||||
- journal/control record corruption: provider exception을 노출하지 않고 typed indeterminate;
|
||||
- guarantee를 낮춰 성공시키는 fallback은 없다.
|
||||
|
||||
## 13. 테스트와 증거
|
||||
|
||||
### 13.1 Unit/contract
|
||||
|
||||
- exact destination/provider selection과 no-default;
|
||||
- R1/R2 simultaneous activation rejection;
|
||||
- reference grammar/check digits/forged route rejection;
|
||||
- journal v2, manifest, reference canonical round-trip;
|
||||
- deterministic route token collision rejection과 canonical policy/schema/format digest;
|
||||
- same operation path의 strict canonical R1 read-only/v2 write-only typed dispatch;
|
||||
- state revision과 fingerprint conflict;
|
||||
- achieved durability value invariants.
|
||||
|
||||
### 13.2 Local integration
|
||||
|
||||
- pre-provisioned root requirement;
|
||||
- owner/mode/FileStore/sentinel mismatch startup failure;
|
||||
- symlink ancestor/control/data rejection;
|
||||
- staging/final/control same `FileStore`;
|
||||
- successful capability probe와 cleanup;
|
||||
- partial final visibility 0건;
|
||||
- same operation concurrency와 producer once;
|
||||
- unsealed `WRITING` failure quarantine와 retry producer 0회;
|
||||
- target collision no overwrite;
|
||||
- non-terminal data/manifest/reference digest mismatch quarantine;
|
||||
- terminal mismatch의 PUBLISHED journal/artifact 불변 보존과 typed integrity/indeterminate.
|
||||
|
||||
### 13.3 Crash qualification
|
||||
|
||||
Forked JVM helper를 사용해 다음 force boundary 직후 process를 강제 종료하고 새 JVM에서 같은
|
||||
operation을 재시도한다.
|
||||
|
||||
```text
|
||||
J-WRITING
|
||||
stage force
|
||||
J-SEALED
|
||||
data link
|
||||
data directory force
|
||||
manifest force
|
||||
manifest directory force
|
||||
reference force
|
||||
reference directory force
|
||||
terminal journal force
|
||||
terminal journal directory force
|
||||
```
|
||||
|
||||
각 boundary에서 결과는 다음 중 하나여야 한다.
|
||||
|
||||
- producer 재실행 없이 동일 receipt 복원;
|
||||
- verified sealed bytes로 publication 완성;
|
||||
- typed indeterminate/quarantine.
|
||||
|
||||
partial final, overwrite, 다른 receipt, silent guarantee downgrade는 허용하지 않는다.
|
||||
|
||||
같은 attested root와 operation ID에 대해 process A가 OS operation lock을 보유하는 동안 forked
|
||||
process B의 bounded non-blocking/timed acquire가 critical section에 진입하지 못하고, A의
|
||||
release 또는 강제 종료 뒤 B가 획득하는지도 별도로 증명한다. 이 증거는 동일 JVM stripe 테스트로
|
||||
대체하지 않는다.
|
||||
|
||||
### 13.4 플랫폼
|
||||
|
||||
- Linux/POSIX + `SecureDirectoryStream` + directory force qualification lane에서만
|
||||
`FILE_AND_DIRECTORY_SYNC`을 검증한다.
|
||||
- capability가 없는 일반 unit-test filesystem에서는 R1 보장만 테스트하며 R2 service test를
|
||||
skip 성공으로 처리하지 않는다.
|
||||
|
||||
## 14. 완료 기준
|
||||
|
||||
이번 increment의 완료는 “Fileserver 전체가 모든 운영환경에서 R2”라는 뜻이 아니다.
|
||||
|
||||
완료를 주장하려면:
|
||||
|
||||
1. provider 기본값 없이 exact binding이 동작한다.
|
||||
2. `local-persistent` startup probe가 모든 required capability를 증명한다.
|
||||
3. terminal manifest/reference direct lookup이 구현된다.
|
||||
4. 모든 publication force boundary의 crash test가 deterministic result를 낸다.
|
||||
5. strict path/mount identity/security tests가 통과한다.
|
||||
6. public path와 clean architecture gate가 통과한다.
|
||||
7. R1 compatibility artifact를 R2로 자동 승격하지 않는다.
|
||||
8. 문서와 receipt는 `local-persistent` qualification만 R2라고 표시한다.
|
||||
|
||||
후속 순서는 Phase 3 maintenance/resource limits, Phase 4 SFTP, Phase 5 shared-mounted/NFS evidence다.
|
||||
|
||||
## 15. 구현 및 readiness 판정
|
||||
|
||||
2026-07-28 구현은 다음 경계를 만족한다.
|
||||
|
||||
- application에는 provider/path/framework 타입이 없는 `FilePublicationPort`만 유지한다.
|
||||
- adapter 내부의 canonical operation/manifest/reference model, opaque reference, provider SPI,
|
||||
exact destination router는 provider-neutral control/selection boundary로 구현되었다.
|
||||
- `app.fileserver.enabled`는 disabled-default이며, enable 시 destination/provider를 exact
|
||||
compile한다. Unknown destination은 producer 호출 전에 실패하고 implicit local fallback은
|
||||
없다.
|
||||
- 같은 provider ID를 참조하는 destination은 하나의 provider/control/payload runtime을
|
||||
공유한다. 서로 다른 provider ID가 같은 normalized root를 소유하면 startup에서 실패한다.
|
||||
- R2 provider는 `local-persistent` 하나만 구현·qualification한다. Absolute/existing
|
||||
pre-provisioned root와 owner/mode/FileStore/sentinel/path/capability attestation이 모두
|
||||
성공해야 bean이 구성된다.
|
||||
- operation v2, private manifest, direct reference index, ordered force publication과
|
||||
deterministic recovery를 구현했다. Forked-process qualification은 각 force boundary와 OS
|
||||
operation lock을 대상으로 하며, focused/module/full gate 결과와 함께 완료 증거를 판정한다.
|
||||
- 기존 schema-v1 terminal record와 root-level R1 artifact는 strict UTF-8/canonical/direct
|
||||
read-only compatibility다. 원래 `PROCESS_LOCAL_SYNC` receipt만 복원하며 schema-v2 rewrite,
|
||||
manifest/reference 생성, `FILE_AND_DIRECTORY_SYNC` 자동 승격을 하지 않는다.
|
||||
|
||||
`FILE_AND_DIRECTORY_SYNC`는 attested local filesystem protocol에서 file과 관련 directory
|
||||
force가 성공했다는 의미다. Physical device, volatile storage-controller cache, volume replica,
|
||||
backup 또는 site 단위 power-loss protection을 주장하지 않는다. 그 보장은 Fileserver 코드가
|
||||
아니라 선택한 storage/deployment의 별도 evidence가 필요하다.
|
||||
|
||||
다음 capability는 구현되지 않았고 setting/env/bean으로 노출하지 않는다.
|
||||
|
||||
- `shared-mounted`/NFS multi-client correctness와 cross-node producer fencing;
|
||||
- SFTP SDK, connection/session pool, host-key/credential, remote reconciliation;
|
||||
- background reconcile/reaper, managed retention/delete;
|
||||
- quota reservation, backpressure, capacity admission;
|
||||
- Fileserver 전용 readiness/health, metrics, tracing, audit.
|
||||
|
||||
따라서 이 increment의 운영 claim은 “모든 Fileserver topology가 R2”가 아니라
|
||||
“strictly attested `local-persistent` profile만 R2”다.
|
||||
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
File diff suppressed because it is too large
Load Diff
+1154
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,137 @@
|
||||
# Redis Cache Resilience Increment Design
|
||||
|
||||
**Status:** approved for implementation
|
||||
|
||||
**Parent:** `2026-07-26-redis-production-capability-design.md` §§15, 16, 18
|
||||
|
||||
## Goal
|
||||
|
||||
Complete one coherent production-facing cache increment on top of the current standalone R1 Redis
|
||||
runtime:
|
||||
|
||||
1. a framework-free cache-aside policy in `application-core`;
|
||||
2. bounded local single-flight and source bulkhead protection;
|
||||
3. deterministic TTL jitter plus soft/hard expiry and stale lookup semantics in the Redis adapter.
|
||||
|
||||
This increment does not promote Redis beyond standalone cache R1. Distributed refresh leases,
|
||||
generation invalidation, rate limiting, owner-safe locks, idempotency, sessions, Sentinel/Cluster,
|
||||
TLS/ACL and fault qualification remain later increments.
|
||||
|
||||
## Architecture boundary
|
||||
|
||||
- `application-core` owns lookup interpretation, source-result classification, cache-aside
|
||||
sequencing, stale-if-error, local coalescing and source admission policy.
|
||||
- `adapter:outbound:cache-redis` owns physical TTL, envelope timestamps, deterministic jitter,
|
||||
serialization and Redis command outcomes.
|
||||
- The application contract contains no Redis/Lettuce/Lua/Spring type.
|
||||
- Cache fallback never becomes unlimited source fallback. A miss, provider outage and waiter burst
|
||||
all pass through the same bounded source path.
|
||||
|
||||
## Application contract
|
||||
|
||||
`CacheSourceLoader<K,V>` returns a typed `SourceLoadOutcome<V>`:
|
||||
|
||||
- `Loaded(value, sourceRevision)`;
|
||||
- `AuthoritativeAbsent(reason, sourceRevision)`;
|
||||
- `TransientFailure(SourceFailure)`;
|
||||
- `PermanentFailure(SourceFailure)`;
|
||||
- `Cancelled`.
|
||||
|
||||
`SourceFailure` carries a bounded code and the original cause. It never serializes the cause message
|
||||
into Redis or metric tags. An unclassified thrown exception is rethrown unchanged and is never
|
||||
negative-cached or converted to stale success.
|
||||
|
||||
`CacheResult<V>` distinguishes:
|
||||
|
||||
- fresh cache hit;
|
||||
- source-loaded value and its cache-record outcome;
|
||||
- authoritative absence and its cache-record outcome;
|
||||
- stale fallback after a classified transient source failure;
|
||||
- source failure;
|
||||
- bounded overload/timeout rejection;
|
||||
- cancellation.
|
||||
|
||||
`CacheAsidePolicy` is immutable and constructed once per semantic region. It contains maximum
|
||||
in-flight source keys, waiter limit per key, source concurrency, admission wait, load deadline and
|
||||
whether transient source failure may serve stale.
|
||||
|
||||
## Cache-aside state machine
|
||||
|
||||
1. `Hit(FRESH)` returns immediately.
|
||||
2. `NegativeHit` returns immediately.
|
||||
3. `Hit(STALE)` retains the value and attempts a bounded refresh.
|
||||
4. `Miss`, an `IncompatibleSchema(QUARANTINE_AND_RELOAD)` carrying a usable opaque observation
|
||||
token, and `Unavailable` enter the same bounded source path. `FAIL_FAST` schema results and
|
||||
unobservable incompatible values are not overwritten.
|
||||
5. A local single-flight elects one leader per semantic key. Waiters share the typed source outcome.
|
||||
6. The leader must acquire the source bulkhead before calling the loader.
|
||||
7. A miss records with `ONLY_IF_ABSENT`. A stale or quarantined observation records with
|
||||
`ONLY_IF_OBSERVED`, which atomically compares the digest captured by lookup before replacing the
|
||||
value. No lookup-then-delete sequence is used, so a concurrent writer is never deleted.
|
||||
8. Only `AuthoritativeAbsent` records a negative entry, using the same absent/observed condition as
|
||||
a positive source result.
|
||||
9. `TransientFailure` may return the retained stale value when policy allows it.
|
||||
10. `PermanentFailure`, unclassified exceptions and cancellation are never hidden by negative cache.
|
||||
11. Entries are removed from the flight map after success or failure. In-flight keys and waiters are
|
||||
bounded; waiting uses a finite deadline and preserves thread interruption.
|
||||
|
||||
The loader is synchronous and cancellation is cooperative. Its token exposes deadline/interruption;
|
||||
the executor bounds admission and waiter time but cannot safely terminate arbitrary source code.
|
||||
|
||||
## Redis envelope and TTL policy
|
||||
|
||||
The positive envelope moves to version 2 and stores:
|
||||
|
||||
- source revision;
|
||||
- `softExpiresAt` epoch milliseconds;
|
||||
- `hardExpiresAt` epoch milliseconds;
|
||||
- payload and SHA-256 integrity digest.
|
||||
|
||||
Negative envelopes store only the hard expiry. Lookup behavior is:
|
||||
|
||||
- `now < softExpiresAt`: `Hit(FRESH)`;
|
||||
- `softExpiresAt <= now < hardExpiresAt`: `Hit(STALE)`;
|
||||
- `now >= hardExpiresAt`: `Miss(EXPIRED)`;
|
||||
- negative `now < hardExpiresAt`: `NegativeHit`;
|
||||
- expired negative: `Miss(EXPIRED)`.
|
||||
|
||||
Version 1 becomes an explicit retired schema result. Future versions and corrupt envelopes fail
|
||||
fast. Digest-valid retired/unknown envelopes carry an opaque observation token so an approved
|
||||
quarantine reload can compare-and-replace the exact observation. Structurally invalid current
|
||||
envelopes remain corrupt/fail-fast even when their digest is valid. Unknown envelopes remain typed
|
||||
incompatibility results and are not silently treated as misses. Envelope integrity is checked
|
||||
before the version byte is trusted.
|
||||
|
||||
The policy contains positive soft TTL, positive hard TTL, negative TTL, jitter ratio, minimum hard
|
||||
TTL and maximum value bytes. Construction rejects:
|
||||
|
||||
- non-positive or over-30-day TTLs;
|
||||
- soft TTL greater than hard TTL;
|
||||
- jitter outside `0.0..0.5`;
|
||||
- minimum hard TTL greater than either configured hard TTL.
|
||||
- configured hard TTL plus maximum positive jitter greater than 30 days.
|
||||
|
||||
Jitter is deterministic from the HMAC-derived physical key and the compiled policy revision. It
|
||||
uses a symmetric bounded factor. The actual positive soft/hard TTLs use the same factor so ordering
|
||||
is preserved. Physical Redis TTL equals the encoded hard expiry duration in the same `SET`.
|
||||
Negative TTL is jittered independently and also respects the hard minimum.
|
||||
|
||||
## Evidence
|
||||
|
||||
Tests must prove:
|
||||
|
||||
- fresh/negative hits do not call the source;
|
||||
- concurrent same-key misses call the loader once;
|
||||
- in-flight-key, waiter, bulkhead and deadline bounds;
|
||||
- completion/failure cleanup and exception/interruption behavior;
|
||||
- only authoritative absence is negative-cached;
|
||||
- stale is served only after a classified transient failure;
|
||||
- fresh/stale/expired boundaries with an injected `Clock`;
|
||||
- deterministic bounded jitter and hard minimum;
|
||||
- version 1/future/corrupt envelope behavior;
|
||||
- Redis physical TTL matches the encoded hard expiry.
|
||||
- observed replace reads only the trailing digest and never overwrites a concurrent writer;
|
||||
- the exact 16MiB opt-in payload is accepted while 16MiB+1 is rejected before dispatch;
|
||||
- mutation interruption restores the thread flag and maps to indeterminate certainty.
|
||||
|
||||
Focused checks run before the repository-wide architecture, dependency, env and public-path gates.
|
||||
@@ -0,0 +1,144 @@
|
||||
# Redis Distributed Rate-Limit Increment Design
|
||||
|
||||
**Status:** implemented as standalone R1
|
||||
|
||||
**Parent:** `2026-07-26-redis-production-capability-design.md` §§19–21
|
||||
|
||||
## Goal and readiness
|
||||
|
||||
Provide three selectable, bounded distributed rate-limit algorithms:
|
||||
|
||||
- fixed window;
|
||||
- sliding-window counter;
|
||||
- token bucket.
|
||||
|
||||
This increment is a standalone Redis R1 provider. It does not claim R2 topology/security/failover
|
||||
qualification and does not implement sliding log, GCRA, leaky bucket, evaluation dedup, hierarchical
|
||||
all-or-nothing policies or local emergency fallback.
|
||||
|
||||
## Ownership
|
||||
|
||||
- `shared-contract` owns the edge-enforcement semantic port and provider-neutral request, policy,
|
||||
decision and failure outcomes. Business quotas remain application use-case policy and do not use
|
||||
this port.
|
||||
- `adapter:outbound:cache-redis` owns Redis keys, atomic Lua programs, structured reply parsing,
|
||||
failure certainty and the provider implementation.
|
||||
- `app-bootstrap` owns the explicit provider/policy selection.
|
||||
- The existing inbound-web local limiter remains a compatibility path until a separate inbound
|
||||
migration. Its types do not cross into the Redis provider.
|
||||
|
||||
The rate-limit runtime does not reuse `app.cache.redis`, the cache connection or cache fail-open
|
||||
decorators. Coordination has different failure and deployment semantics.
|
||||
|
||||
## Shared semantic contract
|
||||
|
||||
`EdgeRateLimitPort.evaluate(RateLimitRequest)` accepts:
|
||||
|
||||
- bounded `policyId`;
|
||||
- already pseudonymized/bounded `subjectDigest`;
|
||||
- positive request cost;
|
||||
- optional evaluation ID (rejected in this non-deduplicating revision);
|
||||
- finite caller deadline.
|
||||
|
||||
`RateLimitPolicy` freezes policy ID/revision, one algorithm-specific parameter subtype, maximum
|
||||
cost, cleanup grace, maximum clock regression and `FAIL_CLOSED`. Construction rejects mismatched
|
||||
algorithm/parameters, arithmetic outside Lua's exact integer range and unsupported failure/dedup
|
||||
claims.
|
||||
|
||||
The outcome is one of:
|
||||
|
||||
- `Evaluated(decision)`;
|
||||
- `Unavailable(policyId, retryAfter, category)` for known pre-send/no-mutation failures and unsafe
|
||||
server clock;
|
||||
- `Indeterminate(policyId, retryAfter)` for post-dispatch uncertain mutation;
|
||||
- `Incompatible(policyId, category)` for state/program/reply mismatch.
|
||||
|
||||
`RateLimitDecision` includes allow/deny, limit, remaining, retry-after, reset-at, policy ID/revision,
|
||||
`GLOBAL_REDIS` source and certainty. Fixed window and token bucket are `CERTAIN`;
|
||||
sliding-window counter is `APPROXIMATE_ALGORITHM`.
|
||||
|
||||
## Atomic programs
|
||||
|
||||
Each v1 program uses one versioned hash key and calls Redis `TIME` exactly once.
|
||||
|
||||
```text
|
||||
rate-fixed-window-v1.lua
|
||||
rate-sliding-counter-v1.lua
|
||||
rate-token-bucket-v1.lua
|
||||
```
|
||||
|
||||
Every program returns exactly seven bounded scalar fields:
|
||||
|
||||
```text
|
||||
status, serverNowMillis, effectiveNowMillis,
|
||||
limit, remaining, retryAfterMillis, resetAtMillis
|
||||
```
|
||||
|
||||
Statuses are `ALLOWED`, `DENIED`, `CLOCK_UNSAFE`, `STATE_INCOMPATIBLE`, `INVALID`.
|
||||
Unknown arity/status/numeric syntax/range is a compatibility failure, never allow/fail-open.
|
||||
|
||||
Common rules:
|
||||
|
||||
- Redis server time drives enforcement;
|
||||
- small backward movement clamps to stored `lastObservedMillis`;
|
||||
- regression beyond policy threshold returns `CLOCK_UNSAFE` without consuming state;
|
||||
- policy/schema/algorithm mismatch returns `STATE_INCOMPATIBLE`;
|
||||
- denied requests do not consume quota;
|
||||
- state receives a finite TTL;
|
||||
- all arithmetic stays within `2^53-1`;
|
||||
- raw principal/IP/API-key/route never appears in the physical key.
|
||||
|
||||
The existing scalar Lua executor stays intact. A structured program path adds bounded MULTI reply
|
||||
support and uses `EVALSHA`, falling back to the exact compiled source only on `NOSCRIPT`.
|
||||
|
||||
## Algorithm rules
|
||||
|
||||
Fixed window stores window ID and consumed count. Allow increments only when
|
||||
`consumed + cost <= limit`; retry/reset points to the current window end.
|
||||
|
||||
Sliding counter stores previous/current window IDs and counts, using scale `1_000_000` and
|
||||
conservative ceiling weight. It reports approximate certainty and a bounded conservative retry.
|
||||
|
||||
Token bucket stores scaled tokens, last refill time and the sub-token division remainder. Refill is
|
||||
therefore independent of evaluation frequency, uses quotient/remainder arithmetic without an
|
||||
unsafe `numerator + denominator - 1` intermediate, and saturates at capacity. Denial does not
|
||||
subtract tokens; retry and full-reset use integer ceiling.
|
||||
|
||||
## Physical key
|
||||
|
||||
The existing canonical builder is reused with:
|
||||
|
||||
```text
|
||||
capability=rate
|
||||
region=<policyId>
|
||||
kind=state
|
||||
digest(policyId, policyRevision, algorithm, subjectDigest)
|
||||
```
|
||||
|
||||
Policy revision appears in both digest input and stored state. A policy revision therefore rolls to
|
||||
a new key while old state expires naturally.
|
||||
|
||||
## Runtime and composition
|
||||
|
||||
`app.rate-limit` is disabled by default. Enabling requires:
|
||||
|
||||
- `provider=redis`;
|
||||
- one default policy and an exact policy definition;
|
||||
- a dedicated Redis coordination endpoint and HMAC secret;
|
||||
- finite command/admission bounds.
|
||||
|
||||
Only `role=coordination` and `failure-policy=fail-closed` are accepted in v1. Disabled mode creates
|
||||
no connection, thread or semantic port. Cache Redis settings/beans are never an implicit fallback.
|
||||
|
||||
## Evidence
|
||||
|
||||
Unit tests cover contract bounds, policy arithmetic, key privacy/revision, structured reply
|
||||
validation, `NOSCRIPT`, boundary vectors, denial-no-consume, clock regression, pre/post-dispatch
|
||||
failure certainty and disabled composition. The explicit Redis 7.4 service lane executes all three
|
||||
programs, exact-boundary admission after a denied non-consuming request, excessive clock-regression
|
||||
state immutability, `TYPE` response normalization, token refill-remainder carry, malformed hash-state
|
||||
classification, cache `NX`, and observation-token compare-and-replace. Redis 7.4 is the minimum
|
||||
version declared by the program manifests until a lower-version service lane exists. The caller
|
||||
deadline is an admission precheck against the fixed command timeout; R1 does not claim per-command
|
||||
dynamic timeout or hard cancellation after dispatch. Missing TLS/ACL, Sentinel/Cluster, failover and
|
||||
persistence/eviction evidence keeps the provider at R1.
|
||||
@@ -0,0 +1,160 @@
|
||||
# Release Hygiene Refactoring Design
|
||||
|
||||
**Date:** 2026-08-01
|
||||
**Status:** approved by the user's instruction to apply the preceding review
|
||||
**Scope:** release-blocking architecture test, Gradle wrapper supply-chain integrity, Docker build configuration inputs, SpotBugs analysis completeness, and the observed Gradle 10 deprecation
|
||||
|
||||
## Context
|
||||
|
||||
The repository-wide review found that the 19-leaf Clean Architecture dependency model is healthy,
|
||||
but the release surface is not green:
|
||||
|
||||
- `:app-bootstrap:sampleOffTest` fails because a whole-composition Object Storage ArchUnit rule is
|
||||
evaluated on the intentionally sample-free classpath with `allowEmptyShould(false)`.
|
||||
- the two Dockerfiles run Gradle before copying configuration-time registry inputs, while the root
|
||||
build also requires a Git checkout during configuration even though the Docker context excludes
|
||||
`.git`;
|
||||
- `gradle-wrapper.properties` selects Gradle 9.0.0 while the checked-in wrapper JAR is from another
|
||||
official Gradle release, and the distribution checksum is absent;
|
||||
- clean SpotBugs analysis reports missing Spring Session, Micrometer Context Propagation, and
|
||||
protobuf classes;
|
||||
- a root task calls `Task.project` during execution, which is deprecated and scheduled to fail in
|
||||
Gradle 10.
|
||||
|
||||
This design deliberately closes those release-hygiene defects before changing idempotency, outbox,
|
||||
security, or sample data behavior. Each later subsystem gets a separate design and plan so that a
|
||||
reviewer can accept or revert it independently.
|
||||
|
||||
## Considered Approaches
|
||||
|
||||
### Approach A: weaken the existing global gates
|
||||
|
||||
Set ArchUnit rules to allow empty matches, ignore SpotBugs missing-class messages, and make Docker
|
||||
configuration registries optional. This is the smallest diff, but it makes the architecture and
|
||||
static-analysis gates less trustworthy. Rejected.
|
||||
|
||||
### Approach B: patch each symptom in place
|
||||
|
||||
Condition the ArchUnit rule on a sample flag, copy only the two currently missing registry files,
|
||||
and add the three currently missing SpotBugs JARs manually. This would pass today's cases but would
|
||||
recur whenever another leaf, registry, source set, or dependency is added. Rejected because it
|
||||
duplicates ownership knowledge.
|
||||
|
||||
### Approach C: align ownership and derive inputs from the owning model
|
||||
|
||||
Move the leaf-specific architecture rule to the Object Storage leaf, keep root tests responsible
|
||||
for cross-leaf registration, treat `config/**` as a declared Docker configuration input, move Git
|
||||
evidence checks to the evidence task execution phase, align the wrapper artifacts to one version,
|
||||
and derive SpotBugs auxiliary inputs from each analyzed source set's runtime classpath. Selected.
|
||||
|
||||
## Architecture Test Ownership
|
||||
|
||||
`adapter-outbound-objectstorage` owns rules about the public types of its production adapter methods.
|
||||
The rule moves out of `app-bootstrap` and runs in the Object Storage module's normal test suite.
|
||||
It remains strict: the Object Storage module must contain matching production classes and the rule
|
||||
must not globally allow an empty `should` clause.
|
||||
|
||||
`app-bootstrap` continues to own cross-module rules. Its sample-off suite verifies that production
|
||||
composition works without `sample-portfolio`; it does not require sample-only leaves to be present.
|
||||
The existing module registry and dependency verification remain the SSOT for leaf coverage.
|
||||
|
||||
## Gradle Wrapper Integrity
|
||||
|
||||
Gradle 9.0.0 remains the selected version for this refactoring. The wrapper scripts, properties, and
|
||||
JAR are regenerated from Gradle 9.0.0 in a trusted environment. The official 9.0.0 binary
|
||||
distribution SHA-256 is recorded as:
|
||||
|
||||
```text
|
||||
8fad3d78296ca518113f3d29016617c7f9367dc005f932bd9d93bf45ba46072b
|
||||
```
|
||||
|
||||
The wrapper properties are one exact ordered eight-line byte contract, preventing Java Properties
|
||||
duplicate-key, separator, escape, and continuation semantics from overriding the reviewed values.
|
||||
The complete six-file workflow path set and every workflow's SHA-256 are embedded as a reviewed
|
||||
byte lock in the verifier. This is the primary completeness boundary: YAML has aliases, encoded
|
||||
keys, duplicate-key overrides, custom shells, and other equivalent representations that a partial
|
||||
Bash parser cannot safely model. Any workflow addition, removal, rename, symlink replacement, or
|
||||
byte change fails until the complete workflow diff is intentionally reviewed and the sorted lock
|
||||
is refreshed in the same change.
|
||||
|
||||
The restricted block-style workflow grammar remains defense in depth and supplies actionable
|
||||
diagnostics for ordinary drift. Every Gradle-running job uses an unconditional validation step
|
||||
with a stable ID and the action pinned by commit SHA. Checkout and validation precede every Gradle
|
||||
invocation, not only the first; a cleanup/sanitizer step that intentionally uses `always()` also
|
||||
requires the validation step's successful outcome. This is consistent with the repository's
|
||||
existing pinned `actions/setup-java` policy and prevents wrapper failure from being bypassed by
|
||||
step conditions.
|
||||
|
||||
## Docker Configuration Contract
|
||||
|
||||
Both Docker build dependency-cache stages preserve the repository layout with `WORKDIR /build/src`
|
||||
and copy the complete `config/**` tree before invoking Gradle. The parent `/build` is therefore the
|
||||
repository root expected by registry `source_path: src/**` entries. This is intentional: Gradle
|
||||
configuration registries and their repository-relative path base are build inputs, while the
|
||||
registry's exact internal file list may evolve.
|
||||
|
||||
Git revision validation no longer runs unconditionally while the build script is being configured.
|
||||
A root-owned resolver is invoked once from each root evidence action or leaf evidence test's
|
||||
root-suite completion action; eager scalar evidence properties are removed. Only evidence-producing
|
||||
tasks resolve the checkout revision during their execution. Docker builds provide
|
||||
`-PgitRevision=<40 lowercase hex>` and do not copy `.git` into the image context.
|
||||
|
||||
The boot JAR path is obtained from Gradle's archive output contract rather than selecting the first
|
||||
filesystem match. The final images retain the existing digest-pinned base image, non-root user,
|
||||
read-only root filesystem, and JRE-only runtime.
|
||||
|
||||
## SpotBugs and Gradle 10 Compatibility
|
||||
|
||||
Every SpotBugs task analyzes a named source set and receives that source set's runtime classpath as
|
||||
its auxiliary analysis classpath, excluding its own compiled output. Custom test source sets are
|
||||
covered by the same rule. No production dependency scope is widened merely to silence SpotBugs.
|
||||
|
||||
Missing-analysis-class output is treated as a gate failure. The clean gate must produce zero
|
||||
`classes needed for analysis were missing` messages.
|
||||
|
||||
The observed Gradle 10 deprecation is removed by capturing the application-core project during
|
||||
configuration instead of calling `Task.project` from the task action. The dependency-purity gate
|
||||
still traverses that project's configurations during execution, so it explicitly opts out of the
|
||||
configuration cache rather than claiming serializable declared inputs it does not have.
|
||||
|
||||
## Error Handling and Failure Semantics
|
||||
|
||||
- sample-off fails only for a real production composition or architecture violation;
|
||||
- an empty Object Storage rule in its owning module is a test failure;
|
||||
- a wrapper JAR or distribution checksum mismatch fails before Gradle build logic executes in CI;
|
||||
- missing Docker configuration input fails with a named build-contract test rather than an opaque
|
||||
settings error;
|
||||
- invalid or absent `gitRevision` fails only an evidence task that requires it;
|
||||
- SpotBugs missing classes fail static analysis instead of producing a successful partial report.
|
||||
|
||||
## Verification Design
|
||||
|
||||
The implementation follows red-green-refactor. Each behavior has a regression test or executable
|
||||
contract that fails before the production/configuration change:
|
||||
|
||||
1. reproduce `sampleOffTest` failure, then add an owner-module architecture test and remove the
|
||||
misplaced global rule;
|
||||
2. add wrapper property and workflow contract assertions before regenerating the wrapper;
|
||||
3. extend Docker contract tests so a cache-stage Gradle configuration fixture requires `config/**`
|
||||
and accepts an attested `gitRevision` without `.git`;
|
||||
4. add Gradle build-contract coverage for source-set-derived SpotBugs auxiliary classpaths, the
|
||||
removed execution-time `Task.project` access, and the explicit configuration-cache opt-out;
|
||||
5. run focused gates, then the clean repository-wide gate and gate-matrix script.
|
||||
|
||||
## Non-Goals
|
||||
|
||||
- no dependency version upgrade beyond aligning the wrapper to the already selected Gradle 9.0.0;
|
||||
- no business/domain behavior changes;
|
||||
- no idempotency, outbox, Poster publication, security, DTO, or database migration changes;
|
||||
- no broad extraction of the 3,768-line root build script in this phase;
|
||||
- no agent-created branch, stage, commit, amend, or push.
|
||||
|
||||
## Decision Summary
|
||||
|
||||
- Object Storage-specific ArchUnit rules live with Object Storage.
|
||||
- Root architecture rules remain strict and cross-module only.
|
||||
- Gradle stays at 9.0.0 and gains exact wrapper/distribution validation.
|
||||
- Docker copies `config/**`; Git evidence is execution-scoped and supplied by `gitRevision`.
|
||||
- SpotBugs uses source-set runtime classpaths and fails on missing analysis classes.
|
||||
- The dependency-purity task avoids execution-time `Task.project` access and truthfully declares
|
||||
its configuration-cache incompatibility while it still inspects project configurations.
|
||||
@@ -0,0 +1,74 @@
|
||||
# Client-Safe Error Boundary Design
|
||||
|
||||
**Date:** 2026-08-02
|
||||
**Status:** approved by the user's instruction to apply the detailed P1/P2 review sequentially
|
||||
**Scope:** HTTP error envelopes in `adapter:inbound:web` and the `sample-portfolio` domain advice
|
||||
|
||||
## Context
|
||||
|
||||
Several handlers pass `Exception#getMessage()`, rejected request values, or a raw request URL into
|
||||
the public error envelope. Those values are not a stable API contract and can contain identifiers,
|
||||
tokens, uploaded values, configuration details, or internal diagnostics. Persistence and outbound
|
||||
dependency failures already use fixed client-safe messages; the rest of the HTTP boundary must
|
||||
follow the same rule.
|
||||
|
||||
## Decision
|
||||
|
||||
The inbound adapter owns a message allowlist keyed by stable error code. Handlers may expose only:
|
||||
|
||||
- stable `code`, `category`, HTTP status, and `retryable` from `ApiErrorCode`;
|
||||
- fixed, code-specific client messages;
|
||||
- bounded structural details such as field name, validation reason code, expected Java type,
|
||||
supported HTTP methods, or supported media types.
|
||||
|
||||
They must not expose exception messages, rejected values, raw request URLs, adapter/configuration
|
||||
diagnostics, opaque cursors, authentication diagnostics, resource identifiers, or duplicate domain
|
||||
values. Bean Validation interpolated/default messages are also discarded because custom templates
|
||||
can include the validated value. Validation details contain only normalized server-owned property
|
||||
names plus allowlisted reason codes and fixed messages; collection/map keys and indices are removed.
|
||||
|
||||
`ClientSafeErrorMessages` is extended for skeleton-wide operational codes. The sample keeps its
|
||||
domain wording in a separate package-private `PortfolioClientSafeErrorMessages`, preserving the
|
||||
rule that production modules do not know sample business concepts.
|
||||
|
||||
## Public Messages
|
||||
|
||||
Representative mappings are fixed as follows:
|
||||
|
||||
- `MAPPING_FAILED` → `Request data could not be mapped`;
|
||||
- `BAD_PARAMETER` → `Request parameter is invalid`;
|
||||
- `INVALID_TOKEN` → `Authentication token is invalid`;
|
||||
- `UNAUTHENTICATED` → `Authentication is required`;
|
||||
- authorization denials → `Access is denied`;
|
||||
- `PRECONDITION_FAILED` → `Resource state changed; refresh and retry`;
|
||||
- page/cursor failures → generic corrective text, with safe field/reason details retained;
|
||||
- `ADAPTER_DISABLED` and internal classifications → `Internal server error`;
|
||||
- domain not-found/conflict/invariant codes → fixed noun-level text with no ID/title value.
|
||||
|
||||
Transport overrides use fixed wording and retain only safe protocol metadata. For example, 405
|
||||
still emits `Allow`, while both controller-route (`NoHandlerFoundException`) and static-resource
|
||||
(`NoResourceFoundException`) 404s use the same envelope without echoing the request URL.
|
||||
|
||||
## Testing
|
||||
|
||||
Tests inject conspicuous secret sentinels into exception messages, rejected values, URLs, tokens,
|
||||
IDs, and duplicate titles. Every resulting response must preserve its status/code/category while
|
||||
excluding the sentinel from both `error.message` and `error.details`.
|
||||
|
||||
Validation tests additionally place sentinels in interpolated/default messages and iterable
|
||||
keys/indices. A real MockMvc resource-resolution request verifies the Spring 7
|
||||
`NoResourceFoundException` path rather than calling the advice method directly.
|
||||
|
||||
The focused module suites remain the primary verification:
|
||||
|
||||
- `:adapter:inbound:web:test` for operational and transport handlers;
|
||||
- `:sample-portfolio:test` for domain advice and sample wire behavior;
|
||||
- `verifyCleanArchitectureDependencies` for dependency direction.
|
||||
|
||||
## Non-Goals
|
||||
|
||||
- no change to error codes, categories, statuses, or retryability;
|
||||
- no suppression of server-side logs or tracing in this batch;
|
||||
- no application/domain dependency on HTTP response types;
|
||||
- no generic exception-message sanitizer based on regexes or truncation;
|
||||
- no staging, commit, amend, or push by an agent.
|
||||
@@ -0,0 +1,118 @@
|
||||
# Conditional Inbound Transport Boundary Design
|
||||
|
||||
**Date:** 2026-08-02
|
||||
**Status:** approved by the user's instruction to apply the detailed P1/P2 review sequentially
|
||||
**Scope:** the opt-in GraphQL, gRPC, and WebSocket leaf modules and their release evidence
|
||||
|
||||
## Context
|
||||
|
||||
The three leaves are registered and tested independently, but neither `app-bootstrap` nor
|
||||
`sample-portfolio` has a production dependency on them. That omission is intentional: adding a
|
||||
classpath edge today would activate GraphQL, start a plaintext/reflection-enabled gRPC server by
|
||||
default, and unconditionally expose a wildcard-origin STOMP broker that serializes arbitrary domain
|
||||
events. The leaf documentation nevertheless describes sample contributions that do not exist, and
|
||||
the ordinary root `check` can become `NO-SOURCE` without a transport-specific positive-count and
|
||||
zero-skip qualification gate.
|
||||
|
||||
P1 therefore makes opt-in status executable and makes accidental activation fail closed. It does
|
||||
not add these leaves to the default runtime or claim the P2 production baselines.
|
||||
|
||||
## Runtime Membership SSOT
|
||||
|
||||
Every entry in `config/architecture/modules.json` gains an exact `runtime_memberships` array whose
|
||||
values are limited to the two composition roots: `app-bootstrap` and `sample-portfolio`.
|
||||
|
||||
- A composition root includes itself in its membership.
|
||||
- Direct production `api`/`implementation`/`compileOnly`/`runtimeOnly` project dependencies must
|
||||
equal the registry members for that root, excluding the root itself.
|
||||
- An empty array means the leaf is built and architecture-checked but absent from both shipped
|
||||
runtime graphs. GraphQL, gRPC, WebSocket, and Mongo remain in this state.
|
||||
- Test fixtures and custom qualification configurations do not change production membership.
|
||||
|
||||
Settings validation is fail-closed for missing, duplicate, or unknown membership names. A Gradle
|
||||
verification task compares the registry to both composition roots and is part of `check`.
|
||||
|
||||
## Explicit Qualification Composition
|
||||
|
||||
`app-bootstrap` owns a `conditionalTransportTest` source set whose classpath explicitly includes
|
||||
the three opt-in leaves. It proves that the opt-in artifacts resolve together while the registry
|
||||
still declares them absent from both default runtime graphs. It is evidence composition, not a new
|
||||
production dependency edge.
|
||||
|
||||
The root registers exact qualification `Test` tasks for GraphQL, gRPC, and WebSocket. Each task:
|
||||
|
||||
- names required test classes rather than broad discovery;
|
||||
- fails on no match or no discovery;
|
||||
- always reruns in UTC;
|
||||
- fails if the root suite reports any skipped test.
|
||||
|
||||
An aggregate `conditionalTransportQualification` task depends on the composition contract and all
|
||||
three exact lanes. CI invokes it explicitly from the existing release-blocking quality job, and the
|
||||
gate matrix records the task.
|
||||
|
||||
## gRPC P1 Boundary
|
||||
|
||||
gRPC activation becomes explicit and local-only until a later TLS/mTLS design exists:
|
||||
|
||||
- `enabled=false` and `reflectionEnabled=false` are defaults; missing properties create no runner,
|
||||
health manager, reflection service, or listener.
|
||||
- The current insecure credential mode requires an explicit local-development override and a
|
||||
loopback bind address. Non-loopback insecure bind fails startup.
|
||||
- Feature services require a caller-supplied authentication policy/interceptor. Missing or invalid
|
||||
metadata returns stable `UNAUTHENTICATED`; valid metadata reaches the service.
|
||||
- Health remains a local lifecycle probe; reflection is a separate explicit flag.
|
||||
- The error interceptor wraps `ServerCall.close`, so handler throws, listener throws, ordinary
|
||||
`responseObserver.onError`, and raw `StatusRuntimeException` all pass the same sanitizer.
|
||||
Recognized `ApiErrorCarrier` causes produce stable code/category trailers; unrecognized status
|
||||
descriptions become fixed `INTERNAL_ERROR` with no raw diagnostic.
|
||||
|
||||
A real ephemeral Netty unary service verifies authentication, reflection-off, all error paths, and
|
||||
sentinel redaction. TLS/mTLS, external bind, deadlines, streaming, and protobuf compatibility are
|
||||
P2 and remain unclaimed.
|
||||
|
||||
## GraphQL P1 Boundary
|
||||
|
||||
GraphQL remains classpath-selected: its absence from the default runtime is the disable mechanism,
|
||||
and the qualification classpath is the explicit opt-in mechanism. The wire lane starts a real
|
||||
random-port MVC server and crosses HTTP JSON, Spring Security, and CORS.
|
||||
|
||||
It verifies unauthenticated rejection, authenticated health success, allowed/disallowed origins,
|
||||
GraphiQL disabled, production-style introspection disabled, stable carrier errors, unknown errors,
|
||||
and absence of distinct secret sentinels from the complete response body. The existing resolver is
|
||||
changed only if a failing wire contract proves unsafe behavior.
|
||||
|
||||
Feature schema/resolvers, field authorization, depth/cost, persisted queries, DataLoader, schema
|
||||
compatibility, and subscriptions remain P2.
|
||||
|
||||
## WebSocket P1 Boundary
|
||||
|
||||
WebSocket gains `ca-skeleton.websocket.enabled=false`; both configuration and broadcaster are
|
||||
conditional. Enabled settings reject wildcard/blank origins and invalid endpoint/destination
|
||||
shapes.
|
||||
|
||||
The inbound channel requires an authenticated handshake principal, permits subscription only to
|
||||
the configured server topic, permits authenticated application sends under `/app/**`, and rejects
|
||||
client sends to `/topic/**`. A custom STOMP error handler emits only a fixed client-safe code.
|
||||
|
||||
The broadcaster no longer serializes arbitrary `@DomainEvent` objects. It consults an explicit
|
||||
projection allowlist; an event without exactly one projection is not sent. Projection output is a
|
||||
bounded primitive map, not the domain object graph.
|
||||
|
||||
A real random-port WebSocket/STOMP lane verifies disabled absence, origin/auth/connect/subscribe,
|
||||
server push, broker-send rejection, error redaction, and no projection/no broadcast. The simple
|
||||
broker remains local/R1 only; broker relay, cross-node durability, replay, backpressure, and a
|
||||
domain-specific versioned projection catalog remain P2.
|
||||
|
||||
## Documentation Truthfulness
|
||||
|
||||
Leaf READMEs and CLAUDE files describe only code that exists. Sample GraphQL schemas, gRPC services,
|
||||
and WebSocket publishers are future adoption examples, not current runtime features. Each document
|
||||
states the activation switch, exact P1 evidence, and unimplemented P2 limits.
|
||||
|
||||
## Non-Goals
|
||||
|
||||
- adding any of the three leaves to a shipped default runtime;
|
||||
- adding a production project dependency edge outside the registry;
|
||||
- claiming production readiness from local loopback/simple-broker tests;
|
||||
- implementing sample feature APIs or domain payloads;
|
||||
- staging, committing, amending, or pushing changes.
|
||||
@@ -0,0 +1,79 @@
|
||||
# P2 Verification Governance Refactoring Design
|
||||
|
||||
## Goal
|
||||
|
||||
Remove the remaining fail-open verification paths without changing production behavior or adding
|
||||
unadopted runtime capabilities. P2 strengthens qualification tasks, tracked contract resources,
|
||||
CI parser evidence, JSON Schema conformance, registry ownership, and bounded documentation debt.
|
||||
|
||||
## Scope and sequence
|
||||
|
||||
1. Move strict qualification `Test` registration to each owner leaf through one shared convention.
|
||||
2. Resolve tracked repository contract resources from an explicit repository root and fail when
|
||||
tracked files or directories are absent.
|
||||
3. Exercise the real gate-matrix shell validator through isolated mutation fixtures.
|
||||
4. Validate every Redis program manifest with the committed Draft 2020-12 schema.
|
||||
5. Make the tracked registry set explicit, resolve every `required_test` identifier, and govern
|
||||
temporary runbook stubs with owners and expiry dates.
|
||||
6. Apply bounded P2 cleanup: module-doc link coverage, migration-neutral gate labels, and
|
||||
deterministic outbound HTTP timeout tests.
|
||||
|
||||
Each item is independently reviewable. A later item may reuse infrastructure from an earlier item,
|
||||
but no batch may weaken an existing check while waiting for a subsequent batch.
|
||||
|
||||
## Qualification convention
|
||||
|
||||
The owner project applies `gradle/strict-qualification-test.gradle` and registers its own exact
|
||||
qualification tasks. The root project only aggregates absolute task paths and validates resulting
|
||||
JUnit XML.
|
||||
|
||||
Every strict qualification task must:
|
||||
|
||||
- name at least one required FQCN;
|
||||
- depend on compilation and fail before test execution when any required class file is absent;
|
||||
- use exact JUnit filters with no-match and no-discovery failures enabled;
|
||||
- force fresh execution in UTC and emit JUnit XML;
|
||||
- reject skipped tests and require a positive, failure-free XML count.
|
||||
|
||||
This applies to conditional transports, Messaging evidence lanes, object-storage release lanes,
|
||||
the Poster migration lane, and the app-bootstrap conditional-composition proof. Ordinary optional
|
||||
or quarantine tests are deliberately excluded.
|
||||
|
||||
## Repository contract resources
|
||||
|
||||
`app-bootstrap` injects `ca.repository.root` into contract tests. A package-private resolver
|
||||
normalizes the root, rejects traversal, and exposes `requireTrackedFile` and
|
||||
`requireTrackedDirectory`. Missing tracked resources are assertion failures, never assumptions.
|
||||
Assumptions remain valid only for truly optional external infrastructure.
|
||||
|
||||
## CI parser evidence
|
||||
|
||||
The gate-matrix validator accepts an optional repository-root argument. Contract tests construct a
|
||||
minimal temporary repository fixture and invoke the actual shell script. Mutations for deceptive
|
||||
step names, execution-suppressing flags, missing or duplicated gates, and unregistered tasks must
|
||||
produce non-zero exits with stable diagnostics. Java must not contain a second parser.
|
||||
|
||||
## Schema and registry governance
|
||||
|
||||
- Redis manifests are validated by a Draft 2020-12 implementation in addition to existing catalog
|
||||
cross-checks.
|
||||
- A registry catalog has an exact one-to-one relationship with tracked `docs/registries/*.yaml`.
|
||||
- Stable `required_test` IDs resolve through a tracked catalog to a single owner Gradle path and
|
||||
source test/method. Unknown, duplicate, and dangling mappings fail.
|
||||
- Temporary runbook stubs are listed in tracked debt data with owner, issue, start, and sunset.
|
||||
Missing or expired debt entries fail.
|
||||
|
||||
## Non-goals
|
||||
|
||||
- No GraphQL feature schema, cost/depth policy, gRPC TLS/streaming, WebSocket relay, or other
|
||||
production capability is introduced.
|
||||
- No lockfile consolidation, version-catalog migration, JVM test-suite migration, or broad module
|
||||
boundary change is included.
|
||||
- Root Gradle capability extraction and a typed settings/build registry model remain separate
|
||||
refactors unless their benefit can be proven without expanding this verification change.
|
||||
|
||||
## Verification
|
||||
|
||||
Each batch starts with a focused failing contract and finishes with its owner `check`. Final
|
||||
verification runs root `test`, `check`, architecture/dependency/runtime membership gates, CI shell
|
||||
validators, dependency locks, public-path/env gates, and `git diff --check`.
|
||||
@@ -0,0 +1,79 @@
|
||||
# Redis Session HTTP Boundary Design
|
||||
|
||||
**Date:** 2026-08-02
|
||||
**Status:** approved by the user's instruction to apply the reviewed P1/P2 work sequentially
|
||||
**Scope:** composition of inbound browser-session security with the outbound versioned Redis session repository
|
||||
|
||||
## Context
|
||||
|
||||
Inbound-web unit contracts prove CSRF, fixation, hardened cookie settings, and primitive security
|
||||
snapshot behavior with `MockHttpSession`/in-memory repositories. Cache-redis contracts prove the
|
||||
versioned session repository and Lua semantics against Redis. No test currently crosses the actual
|
||||
Spring Session filter, production SecurityFilterChain, real Redis, and a second application context.
|
||||
|
||||
Putting this test in inbound-web would require a forbidden dependency on the outbound Redis leaf.
|
||||
The composition root already depends on both leaves and owns the `redisCompositionTest` source set,
|
||||
so app-bootstrap is the correct boundary owner.
|
||||
|
||||
## Decision
|
||||
|
||||
Add a tagged `redis-session-http` integration contract under app-bootstrap's existing
|
||||
`redisCompositionTest` source set. Ordinary `redisCompositionTest` excludes the tag. A new explicit
|
||||
`redisSessionHttpIntegrationTest` task includes only that tag, fails on no discovery or any skip,
|
||||
always reruns, pins UTC, and passes the checked-in Redis image registry path.
|
||||
|
||||
The task is deliberately not attached to ordinary local `check`, because it requires Docker. It is
|
||||
added to the existing release-blocking `redis-standalone` CI job, which is the Docker-capable Redis
|
||||
lane. Docker availability and container startup are attempted directly; no condition, assumption,
|
||||
or environment flag may convert absence into a skip.
|
||||
|
||||
The test loads `redis.approved.image` from `src/gradle/redis-test-images.properties` and rejects an
|
||||
unpinned reference. It creates an ephemeral CA/server certificate and a named, least-privilege ACL
|
||||
user, then connects with TLS, full hostname verification, and explicit CA trust. A
|
||||
runtime-generated Redis password and 32-byte HMAC are supplied through caller-owned versioned
|
||||
material; no secret value is checked in, passed on the Redis command line, or logged. Missing
|
||||
Docker or OpenSSL is a hard failure, not a skip.
|
||||
|
||||
The custom source set needs the Spring Session API at compile time. App-bootstrap therefore adds
|
||||
`spring-session-core` only to `redisCompositionTestImplementation`; the existing version is reused
|
||||
and the lockfile records the new custom compile configuration without changing a dependency
|
||||
version.
|
||||
|
||||
## HTTP/Session Contract
|
||||
|
||||
1. A state-changing request without CSRF is 403.
|
||||
2. Accessing the CSRF endpoint emits the configured Secure, non-HttpOnly CSRF cookie.
|
||||
3. Login with matching cookie/header creates only the bounded primitive authentication snapshot.
|
||||
4. The session cookie is host-only, Secure, HttpOnly, SameSite=Lax, path `/`, and session-scoped.
|
||||
5. After the first web context closes, a second independent context restores `/whoami` from the
|
||||
same cookie through real Redis.
|
||||
6. Logout force-revokes/tombstones the session; the old cookie is unauthenticated and a previously
|
||||
loaded stale session object cannot save over the tombstone.
|
||||
7. If Redis becomes unavailable during session lookup, the request fails closed before the
|
||||
protected controller and the surfaced exception graph contains only the repository's fixed
|
||||
availability message, not endpoint/password/session material.
|
||||
|
||||
The RED run exposed two production composition gaps which are part of this boundary:
|
||||
|
||||
- the primitive security-context repository must wrap the response and persist before response
|
||||
commit, otherwise a successful response can commit before the first session is created;
|
||||
- the API security chain disables Spring Security's request cache, otherwise an unauthenticated
|
||||
request stores a `DefaultSavedRequest` framework graph that the primitive session codec correctly
|
||||
rejects.
|
||||
|
||||
## Architecture
|
||||
|
||||
- Inbound-web remains provider-neutral and has no outbound dependency.
|
||||
- Cache-redis keeps Redis keys, Lua, codec, HMAC, and tombstone policy private.
|
||||
- App-bootstrap assembles both adapters only for a cross-module composition contract.
|
||||
- No production dependency edge or dependency version changes; only a custom-test compile
|
||||
configuration is added to the existing lock entry.
|
||||
|
||||
## Non-Goals
|
||||
|
||||
- Redis Sentinel/Cluster sessions (production activation explicitly rejects them today);
|
||||
- browser-engine proof of SameSite behavior;
|
||||
- credential/certificate rotation qualification (the fixture still uses mandatory TLS, full
|
||||
hostname verification, explicit trust, and a named ACL user);
|
||||
- attaching Docker work to ordinary `check`;
|
||||
- staging, commit, amend, or push by an agent.
|
||||
@@ -0,0 +1,84 @@
|
||||
# Verification Purity Refactoring Design
|
||||
|
||||
**Date:** 2026-08-02
|
||||
**Status:** approved by the user's instruction to apply the P1/P2 review sequentially
|
||||
**Scope:** stale traceable JAR verification/cleanup and public-path snapshot verification/update
|
||||
|
||||
## Context
|
||||
|
||||
Two root Gradle verification paths currently mutate files while they are expected to be safe gates:
|
||||
|
||||
- every `Jar` task deletes stale traceable archives in `doFirst`, and
|
||||
`verifyNoStaleTraceableJars` depends on `cleanStaleTraceableJars`;
|
||||
- `verifyPublicPathSnapshot` creates a missing snapshot and updates drift when
|
||||
`-PapprovePublicPathChange` is supplied.
|
||||
|
||||
That makes `check` capable of hiding the state it is meant to detect. This batch restores the
|
||||
standard contract: verification observes and fails, while explicitly named maintenance tasks own
|
||||
writes.
|
||||
|
||||
## Considered Approaches
|
||||
|
||||
### Keep the root build logic in place and inspect source text in tests
|
||||
|
||||
This is the smallest diff, but a source assertion cannot prove task side effects. Rejected.
|
||||
|
||||
### Invoke the entire repository build from a copied checkout
|
||||
|
||||
This tests the actual root build but requires copying all 19 leaves and resolving every root plugin
|
||||
for two small contracts. It is slow and couples the tests to unrelated configuration. Rejected.
|
||||
|
||||
### Extract only the two task concerns into applied Gradle scripts and exercise them with TestKit
|
||||
|
||||
Selected. The production root applies the same scripts that an isolated functional fixture uses.
|
||||
The fixture observes exit status and filesystem state, so it proves behavior rather than source
|
||||
shape. This is a bounded extraction required for testability, not the broad P2 root-build rewrite.
|
||||
|
||||
## Archive Hygiene Contract
|
||||
|
||||
`gradle/archive-hygiene.gradle` owns stale traceable archive discovery and the two root tasks:
|
||||
|
||||
- `verifyNoStaleTraceableJars` reports every stale archive and fails without deleting anything;
|
||||
- `cleanStaleTraceableJars` deletes only names matching the traceable archive pattern for a known
|
||||
`Jar` task and never deletes the current archive;
|
||||
- normal `jar`/`bootJar` execution never performs cleanup.
|
||||
|
||||
The existing traceable version naming and manifest metadata remain unchanged.
|
||||
|
||||
## Public-Path Snapshot Contract
|
||||
|
||||
`gradle/public-path-snapshot.gradle` owns canonicalization and two root tasks:
|
||||
|
||||
- `verifyPublicPathSnapshot` fails when the env file or committed snapshot is missing, when content
|
||||
drifts, or when the update-only approval property is passed to the verifier. It never creates
|
||||
directories or writes files;
|
||||
- `updatePublicPathSnapshot` requires `-PapprovePublicPathChange` and writes the canonical snapshot.
|
||||
|
||||
A clean-worktree requirement is intentionally not used: the normal update workflow necessarily has
|
||||
an intentional `.env` change. Explicit task naming, the approval property, and the resulting diff
|
||||
are the review boundary.
|
||||
|
||||
The canonical header names `updatePublicPathSnapshot`, so documentation and the committed snapshot
|
||||
do not instruct users to mutate through a verification task.
|
||||
|
||||
## Testing
|
||||
|
||||
`BuildVerificationPurityContractTest` runs from an isolated `functionalTest` source set using Gradle
|
||||
TestKit against temporary projects that apply the production scripts directly. Keeping TestKit off
|
||||
the ordinary `testRuntimeClasspath` prevents Gradle's SLF4J provider from replacing Logback during
|
||||
Spring tests. It proves:
|
||||
|
||||
1. a normal `jar` leaves a matching stale archive untouched;
|
||||
2. verification fails and preserves the stale archive;
|
||||
3. explicit cleanup deletes the stale archive but preserves the current archive;
|
||||
4. missing/drifted public-path snapshots cause read-only failure;
|
||||
5. the verifier rejects the update approval property;
|
||||
6. only the explicit updater with approval creates or changes the snapshot.
|
||||
|
||||
## Non-Goals
|
||||
|
||||
- no change to archive naming, versions, manifests, production dependency versions, or project edges;
|
||||
- only the new isolated functional-test configurations are added to `app-bootstrap/gradle.lockfile`;
|
||||
- no public-path allow-list value change;
|
||||
- no broad root Gradle convention-plugin migration;
|
||||
- no staging, commit, amend, or push by an agent.
|
||||
@@ -0,0 +1,448 @@
|
||||
# Warning-Zero Build Refactoring Design
|
||||
|
||||
**Date:** 2026-08-02
|
||||
**Status:** Approved design, pending written-spec review
|
||||
**Scope:** Java compilation, Error Prone, Checkstyle, SpotBugs, test JVM diagnostics, expected-negative
|
||||
shell-contract output, and intentional legacy/architecture-test compatibility seams.
|
||||
|
||||
## Goal
|
||||
|
||||
Make the standard repository build both functionally green and warning-clean. A successful build
|
||||
must no longer conceal compiler warnings, test-source SpotBugs findings, ignored Checkstyle
|
||||
findings, deprecated third-party API calls, or expected-negative subprocess diagnostics that look
|
||||
like real failures.
|
||||
|
||||
The final local proof is a fresh `./gradlew clean build --warning-mode=all --no-daemon
|
||||
--console=plain` with:
|
||||
|
||||
- exit code zero;
|
||||
- zero compiler/Error Prone warnings;
|
||||
- zero Checkstyle and SpotBugs findings in every executed source set;
|
||||
- zero `SpotBugs ended with exit code 1` messages;
|
||||
- zero OpenJDK CDS warnings from test JVMs;
|
||||
- no successful Redis lab contract printing its expected-negative child diagnostics;
|
||||
- only the five currently intentional optional-adapter/TestKit skips, with no qualification lane
|
||||
silently skipped.
|
||||
|
||||
## Baseline Evidence
|
||||
|
||||
The fresh pre-change command completed successfully in 20 minutes 26 seconds with 283 of 283 tasks
|
||||
executed. Success did not mean warning-clean:
|
||||
|
||||
- 123 compiler warning diagnostics across 19 warning rules (122 distinct file-line/rule
|
||||
coordinates because one line emits two separate removal diagnostics);
|
||||
- one test-source SpotBugs `DMI_RANDOM_USED_ONLY_ONCE` finding;
|
||||
- ten OpenJDK CDS warning lines from Mockito-using test JVMs;
|
||||
- 82 `redis-lab:` expected-negative stderr lines;
|
||||
- five intentional skipped tests;
|
||||
- no test failure, compiler error, Checkstyle finding, SpotBugs analysis error, or missing analysis
|
||||
class.
|
||||
|
||||
The Gradle Problems report is an informational index over compiler diagnostics, not a separate
|
||||
defect. It must become empty as a consequence of removing the underlying warnings; it must not be
|
||||
hidden.
|
||||
|
||||
### Warning inventory traceability
|
||||
|
||||
| Rule | Diagnostic instances | Required resolution |
|
||||
| --- | ---: | --- |
|
||||
| `removal` | 46 | Exact legacy lifecycle/suppression policy in section 4 |
|
||||
| `MissingOverride` | 16 | Add annotations to the implementing test fakes in section 2 |
|
||||
| `StringCaseLocaleUsage` | 10 | `Locale.ROOT` behavior fixes and test cleanup in sections 1–2 |
|
||||
| `SameNameButDifferent` | 9 | Qualify the two Redis nested enum types in section 2 |
|
||||
| `DefaultCharset` | 9 | Explicit UTF-8 test data in sections 1–2 |
|
||||
| `ArrayRecordComponent` | 7 | Exact record policies and copy regressions in section 2 |
|
||||
| `CanonicalDuration` | 5 | `Duration.ofDays(3)` in section 2 |
|
||||
| `StringSplitter` | 4 | ETag scanner plus three grammar-specific test fixes in sections 1–2 |
|
||||
| `EmptyCatch` | 4 | Cleanup failure propagation in section 1 |
|
||||
| `StringConcatToTextBlock` | 2 | Byte-identical text blocks in section 2 |
|
||||
| `InvalidBlockTag` | 2 | Inline-code annotation names in section 2 |
|
||||
| `BigDecimalLiteralDouble` | 2 | Method-only intentional-fixture suppressions in section 5 |
|
||||
| `TypeParameterUnusedInFormals` | 1 | Spring Session method-only suppression in section 2 |
|
||||
| `ThreadLocalUsage` | 1 | Instance-isolation regression and field-only suppression in section 2 |
|
||||
| `ReferenceEquality` | 1 | Redis catalog identity regression and constructor-only suppression in section 2 |
|
||||
| `MissingSummary` | 1 | Public Javadoc summary in section 2 |
|
||||
| `JavaTimeDefaultTimeZone` | 1 | Fixed date/explicit zone in section 1 |
|
||||
| `FutureReturnValueIgnored` | 1 | Observe the future in section 1 |
|
||||
| `BooleanLiteral` | 1 | Literal assertion cleanup in section 2 |
|
||||
|
||||
This table accounts for all 123 Error Prone/compiler-warning diagnostics. The separate
|
||||
`-Xlint:deprecation,unchecked` inventory is covered by the third-party migrations and exact legacy
|
||||
seam policy below; it is not allowed to disappear through a source-set suppression.
|
||||
|
||||
## Non-Goals
|
||||
|
||||
- Do not remove the legacy poster-image endpoint, `StoredObjectResponse`, raw-key compatibility
|
||||
data, or legacy object-storage adapters during warning cleanup.
|
||||
- Do not switch the sample runtime from legacy to publication mode without the separately required
|
||||
API, data-adoption, dual-read, and external-consumer approvals.
|
||||
- Do not apply module-wide or task-wide suppression for `removal`, `deprecation`, `unchecked`, or
|
||||
Error Prone rules.
|
||||
- Do not weaken architecture rules or change deliberately forbidden bytecode merely to silence a
|
||||
fixture warning.
|
||||
- Do not make quarantine tests blocking; their separate sunset and reporting policy remains
|
||||
unchanged.
|
||||
|
||||
## Design Principles
|
||||
|
||||
1. Fix behavior defects at their source before applying any suppression.
|
||||
2. Use suppression only where a framework signature, identity invariant, intentional violation
|
||||
fixture, or approved compatibility seam makes the warning inapplicable.
|
||||
3. Scope every suppression to the smallest class, method, field, constructor, or fixture that
|
||||
explains it, with a nearby rationale.
|
||||
4. Replace deprecated third-party APIs with their typed current equivalents and verify behavior,
|
||||
not only compilation.
|
||||
5. Capture expected-negative diagnostics and assert them exactly; never discard stderr globally.
|
||||
6. Add blocking gates only after the current warning inventory is clean.
|
||||
|
||||
## Component Design
|
||||
|
||||
### 1. Real behavior defects
|
||||
|
||||
#### Locale-independent identifiers
|
||||
|
||||
Use `Locale.ROOT` for security roles, notification configuration keys, repository ACL names, and
|
||||
test comparisons. Add Turkish-default-locale regressions that restore the original default locale
|
||||
in `finally`:
|
||||
|
||||
- `JwtToAuthenticatedPrincipalConverter`: `admin` must always become `ROLE_ADMIN`;
|
||||
- `RoutingNotifier`: diagnostic keys for `EMAIL` must remain `app.notification.routes.email...`;
|
||||
- `RepoStatsAclMapper`: `IDEA/Repo` must normalize to `idea/repo`.
|
||||
|
||||
This is a correctness fix: the current code can generate dotless/dotted Turkish-I variants in
|
||||
authorization and operational identifiers.
|
||||
|
||||
#### Quote-aware ETag list parsing
|
||||
|
||||
Do not replace `String.split(",")` with another delimiter-only splitter. A comma is legal inside a
|
||||
quoted opaque entity tag. `ETags` will use a small scanner that:
|
||||
|
||||
- splits only on commas outside a quoted string;
|
||||
- preserves weak-tag prefixes and the existing trimming behavior;
|
||||
- treats malformed/unclosed quotes as non-matching input rather than guessing a token;
|
||||
- preserves wildcard and ordinary multi-value behavior.
|
||||
|
||||
Regressions cover a single comma-bearing tag, a mixed list containing a weak comma-bearing tag,
|
||||
ordinary lists, wildcard, stale values, blank input, and malformed quoting.
|
||||
|
||||
#### Asynchronous and cleanup failures
|
||||
|
||||
- `AsyncGracefulShutdownBehaviorTest` retains the returned `Future<?>` and observes `get()` so a
|
||||
background assertion or exception cannot disappear.
|
||||
- Outbox test cleanup methods propagate or wrap resource-destruction failures with the original
|
||||
cause instead of using empty catches.
|
||||
- Tests use fixed dates, UTF-8, and explicit locale rather than host defaults.
|
||||
|
||||
### 2. Production warning cleanup with preserved invariants
|
||||
|
||||
#### Redis primitive ownership
|
||||
|
||||
`RedisPrimitiveInvocation` intentionally requires descriptor object identity. Value equality would
|
||||
admit a descriptor created by another catalog and weaken the closed-catalog invariant. Keep the
|
||||
reference comparison, add an exact constructor-level `ReferenceEquality` suppression, and add a
|
||||
regression proving value-equal but non-identical cross-catalog descriptors are rejected.
|
||||
|
||||
Qualify both nested `ExpectedKind` types with their enclosing record names rather than renaming the
|
||||
types. This removes `SameNameButDifferent` without changing bytecode or package-local consumers.
|
||||
|
||||
#### Framework-owned generic signature
|
||||
|
||||
`RedisVersionedSession.<T>getAttribute(String)` must retain Spring Session's inherited signature.
|
||||
Apply a method-only `TypeParameterUnusedInFormals` suppression with the interface-contract reason.
|
||||
|
||||
#### Instance-owned retry context
|
||||
|
||||
`OutboundRetryPolicy` keeps its instance `ThreadLocal`. Making it static would leak call context
|
||||
between policy instances on the same thread. Add a field-only `ThreadLocalUsage` suppression and a
|
||||
regression proving policy A's context is invisible to policy B and is cleared by `endCall()`.
|
||||
|
||||
#### Array-bearing records
|
||||
|
||||
- `NotificationCiphertext` retains its public array components because it already clones inputs and
|
||||
accessors, implements content-based equality/hash code, and redacts `toString`. Add focused
|
||||
defensive-copy/equality/redaction tests and an exact record-level suppression.
|
||||
- The four internal session command/outcome records in `VersionedRedisSessionStore` remain internal
|
||||
transport envelopes. Preserve defensive copies, document that generated record equality is not
|
||||
their contract, add constructor/accessor copy tests, and suppress `ArrayRecordComponent` on each
|
||||
exact record.
|
||||
- The private test fake in `RedisVersionedSessionRepositoryTest` receives the same exact nested-type
|
||||
treatment; no public type is changed.
|
||||
|
||||
#### Mechanical behavior-neutral fixes
|
||||
|
||||
- Express 72 hours as `Duration.ofDays(3)` in application/bootstrap/sample settings and matching
|
||||
tests.
|
||||
- Add the missing public Javadoc summary in `TracingSampleRateResolver`, and render annotation names
|
||||
such as `@WebMvcTest` as inline `{@code ...}` rather than accidental block tags.
|
||||
- Add missing `@Override` annotations in sample test fakes.
|
||||
- Replace readability-only string concatenations with text blocks where the literal bytes remain
|
||||
identical.
|
||||
- Replace Boolean wrapper comparisons with boolean literals.
|
||||
- For the three test-only delimiter warnings, preserve each existing grammar explicitly: retain CSV
|
||||
empty-token filtering with a limit-bearing split or scanner, scan mapping-path segments without
|
||||
changing leading/trailing-empty behavior, and parse the single HTTP byte-range hyphen with an
|
||||
asserted `indexOf` boundary. These are not allowed to inherit the ETag scanner because their
|
||||
grammars differ.
|
||||
|
||||
### 3. Third-party API migration
|
||||
|
||||
#### Jackson 3
|
||||
|
||||
In `LocalJsonSchemaRegistry`, replace deprecated `JsonNode.isTextual()`/`textValue()` with
|
||||
`isString()`/`stringValue()`. Existing type guards remain, and JSON schema identity/reference/value
|
||||
tests prove identical acceptance and rejection behavior.
|
||||
|
||||
In `DeterministicEnvelopeWriter`, replace the deprecated convenience call with
|
||||
`jsonFactory.createGenerator(ObjectWriteContext.empty(), output, JsonEncoding.UTF8)`, the
|
||||
non-deprecated Jackson 3.0.2 overload. Preserve canonical byte output; the existing deterministic
|
||||
envelope golden tests are the behavior gate.
|
||||
|
||||
#### Lettuce
|
||||
|
||||
Convert both finite canonical scores to `BigDecimal`, build one inclusive
|
||||
`Range<? extends Number>` for each invocation, and call the typed `zcount(key, range)` and
|
||||
`zrangebyscoreWithScores(key, range, Limit.create(offset, count))` overloads. Preserve inclusive
|
||||
bounds, offset, count, and exact reply mapping. A dynamic-proxy regression verifies both typed
|
||||
overloads are selected; sorted-set primitive contract tests verify results.
|
||||
|
||||
#### AWS SDK retry
|
||||
|
||||
Replace old `RetryPolicy` and core `EqualJitterBackoffStrategy` with `StandardRetryStrategy`, the
|
||||
retries API half-jitter exponential backoff, `maxAttempts`, and
|
||||
`ClientOverrideConfiguration.Builder.retryStrategy`. Tests assert maximum attempts and normal versus
|
||||
throttling backoff configuration. The focused object-storage check must cover provider assembly;
|
||||
compile-only success is insufficient.
|
||||
|
||||
#### Testcontainers Toxiproxy
|
||||
|
||||
Use the Testcontainers 2 toxiproxy package and a typed `ToxiproxyClient`/`Proxy` with an explicit
|
||||
exposed proxy port. Fault tests must still prove cut and restore behavior against MinIO. Dependency
|
||||
and lock changes stay inside the object-storage leaf.
|
||||
|
||||
#### Remaining JDK/generic deprecations
|
||||
|
||||
- Replace deprecated `new URL(String)` test construction with `URI.create(...).toURL()`.
|
||||
- Replace the varargs `thenReturn(firstFuture, secondFuture)` stub in
|
||||
`S3ConditionalObjectControlStoreTest` with two chained single-value `thenReturn(...)` calls, so
|
||||
Mockito does not create the unchecked generic `CompletableFuture<PutObjectResponse>[]` array.
|
||||
- Resolve every `-Xlint:deprecation,unchecked` location individually; do not suppress the source
|
||||
set.
|
||||
|
||||
### 4. Legacy object-storage compatibility seam
|
||||
|
||||
The canonical object-storage ports and sample publication path already exist. The legacy runtime is
|
||||
still selected in local/test configuration and cannot be deleted solely to silence warnings.
|
||||
|
||||
Keep `@Deprecated(forRemoval = true)` on the genuinely replaced whole-byte contracts:
|
||||
|
||||
- `ObjectStoragePort`;
|
||||
- `StoredObject`;
|
||||
- `ObjectStorageSettings`.
|
||||
|
||||
Apply `removal` suppression only to exact compatibility owners:
|
||||
|
||||
- `ObjectStoragePort` for its legacy receipt return type;
|
||||
- `FilesystemObjectStorageAdapter` and `S3ObjectStorageAdapter`;
|
||||
- `UploadPosterImageUseCase`;
|
||||
- the legacy bean method in `PosterImageApiConfig`;
|
||||
- `LegacyPosterImageController`;
|
||||
- `PosterWebMapper.toStoredObjectResponse`;
|
||||
- named legacy characterization test classes and single legacy-receipt test methods.
|
||||
|
||||
The six `application.storage.migration` types and `AdoptLegacyPosterImageUseCase` are the mechanism
|
||||
used to complete data adoption and currently have no replacement. Change their lifecycle marker
|
||||
from `@Deprecated(forRemoval = true)` to plain `@Deprecated`; use exact `deprecation` suppression
|
||||
only inside adoption implementation/configuration. Keep the application-core architecture contract
|
||||
requiring `forRemoval=true` only for `ObjectStoragePort` and `StoredObject`. Keep the adapter-owned
|
||||
`ObjectStorageSettings` marker and add its lifecycle assertion in the object-storage leaf.
|
||||
|
||||
This keeps migration debt visible without falsely claiming that the migration mechanism itself is
|
||||
ready for removal.
|
||||
|
||||
### 5. Test/static-analysis/output cleanup
|
||||
|
||||
#### SpotBugs
|
||||
|
||||
Reuse one static `SecureRandom` in `RedisPrimitiveRuntimeServiceTest` rather than constructing a
|
||||
one-shot generator. After all test reports are clean, make every ordinary and custom test-source
|
||||
SpotBugs task included by `check` blocking. SpotBugs analysis errors and missing classes remain
|
||||
separately fail-closed.
|
||||
|
||||
#### Intentional architecture fixtures
|
||||
|
||||
Keep prohibited `BigDecimal(double/float)` constructor bytecode and apply method-only
|
||||
`BigDecimalLiteralDouble` suppressions. Fix unrelated warnings in allowed fixtures normally. A
|
||||
suppression must never replace the forbidden operation the ArchUnit test is supposed to detect.
|
||||
|
||||
#### Redis lab expected failures
|
||||
|
||||
Change `assert_fails` to capture stdout/stderr per case, assert a non-zero exit and the exact expected
|
||||
diagnostic, reject extra lines, and print the capture only when the assertion fails. Do not redirect
|
||||
to `/dev/null` and do not silence the Gradle `Exec` task globally.
|
||||
|
||||
#### Mockito/CDS
|
||||
|
||||
Provide `mockito-core` to test JVMs as an explicit startup `-javaagent` through a relocatable Gradle
|
||||
argument provider. This removes reliance on Java 21+ runtime self-attachment. Add test-JVM-only
|
||||
`-Xshare:off` because Mockito's bootstrap append otherwise prints the harmless CDS warning. No
|
||||
production JVM argument changes.
|
||||
|
||||
#### Skips
|
||||
|
||||
Retain exactly these five intentional app-bootstrap contract skips:
|
||||
|
||||
- `emailNotificationAdapterRunsOnlyWhenConfigured()`;
|
||||
- `slackNotificationAdapterRunsOnlyWhenConfigured()`;
|
||||
- `redisCacheAdapterRunsOnlyWhenEnabled()`;
|
||||
- `messagingBrokerAdapterRunsOnlyWhenConfigured()`;
|
||||
- `DisabledOptionalAdapterFixture.wouldFailIfItEverRan()`.
|
||||
|
||||
Qualification tasks continue to require positive discovery, at least one executed test, zero skips,
|
||||
and fresh XML, so this policy cannot turn a selected qualification lane green without execution.
|
||||
Any additional skip, or any of these five moving outside its named optional-adapter contract, fails
|
||||
the inventory check.
|
||||
|
||||
### 6. Warning-zero enforcement
|
||||
|
||||
After all existing warnings are removed:
|
||||
|
||||
- configure every leaf `JavaCompile` task with `-Werror`, `-Xlint:deprecation`, and
|
||||
`-Xlint:unchecked` in the root build policy;
|
||||
- retain Error Prone on the same compile tasks so its warnings are promoted by `-Werror`;
|
||||
- remove the root `checkstyleTest`/`spotbugsTest` warning-only policy and the app-bootstrap
|
||||
`sampleOffTest`, `functionalTest`, and `conditionalTransportTest` Checkstyle/SpotBugs
|
||||
`ignoreFailures` overrides, making every such task included by `check` blocking;
|
||||
- retain exact suppression comments as the only approved exception mechanism;
|
||||
- run Gradle with `--warning-mode=fail` in the warning-clean CI lane so Gradle API deprecations also
|
||||
fail rather than print.
|
||||
|
||||
`quarantineTest` remains non-blocking by design. Protected AWS/Docker qualifications remain separate
|
||||
environment evidence and are not converted into local unit tests.
|
||||
|
||||
## File Ownership and Expected Change Groups
|
||||
|
||||
### Root build policy
|
||||
|
||||
- `src/build.gradle`
|
||||
- `src/gradle/test-jvm-agents.gradle`, defining the relocatable Mockito `-javaagent` argument
|
||||
provider and test-only `-Xshare:off` policy, applied once by the root build
|
||||
- `.github/workflows/ci-quality-gates.yml`, adding `--warning-mode=fail` to the blocking
|
||||
`quality-gates` Gradle invocation
|
||||
|
||||
### Production leaves
|
||||
|
||||
- `src/application-core`
|
||||
- `src/adapter/inbound/web`
|
||||
- `src/adapter/outbound/cache-redis`
|
||||
- `src/adapter/outbound/fileserver`
|
||||
- `src/adapter/outbound/httpclient`
|
||||
- `src/adapter/outbound/identifier`
|
||||
- `src/adapter/outbound/messaging`
|
||||
- `src/adapter/outbound/notification`
|
||||
- `src/adapter/outbound/objectstorage`
|
||||
- `src/adapter/outbound/persistence-jpa`
|
||||
- `src/app-bootstrap`
|
||||
- `src/sample-portfolio`
|
||||
- `src/shared-contract`
|
||||
|
||||
Every focused command is derived from the owning leaf's `gradle_path` in
|
||||
`src/config/architecture/modules.json`; no production dependency edge changes are permitted unless
|
||||
the registry is deliberately updated and its architecture verifier passes.
|
||||
|
||||
### Tests and shell contract
|
||||
|
||||
- owning leaf tests adjacent to every behavior change
|
||||
- exact architecture violation fixtures under app-bootstrap test sources
|
||||
- `infra/redis-lab/test/redis-lab-contract.sh`
|
||||
|
||||
## Implementation Sequence
|
||||
|
||||
1. Add failing behavioral regressions for locale, ETag parsing, async exception observation,
|
||||
cleanup propagation, Redis descriptor identity, and retry-context isolation.
|
||||
2. Implement those behavior fixes and run owner-focused tests.
|
||||
3. Remove behavior-neutral compiler/Error Prone warnings per leaf, using only exact justified
|
||||
suppressions.
|
||||
4. Migrate Jackson, Lettuce, AWS SDK, Testcontainers, URL, and generic stubs; run their focused
|
||||
behavior/qualification tests.
|
||||
5. Correct legacy lifecycle markers and exact compatibility suppressions; run application-core,
|
||||
object-storage, sample, and architecture contracts.
|
||||
6. Clean test-only warnings, SpotBugs, Mockito/CDS, and Redis-lab output.
|
||||
7. Enable blocking compiler, Checkstyle, SpotBugs, and Gradle warning gates.
|
||||
8. Run focused checks, architecture validators, dependency locks, full tests, full check, and the
|
||||
fresh warning-clean build.
|
||||
9. Update the LLM Wiki branch note and the warning-debt error note with resolved evidence or exact
|
||||
remaining environmental blockers.
|
||||
|
||||
## Verification Strategy
|
||||
|
||||
### Focused verification
|
||||
|
||||
- Each behavior change follows RED → GREEN with the owning leaf test.
|
||||
- Static-only warning fixes use the exact `compileJava`, `compileTestJava`, Checkstyle, or SpotBugs
|
||||
task as the failing/passing executable contract.
|
||||
- Third-party API migrations run behavior tests that exercise request mapping, retry/backoff,
|
||||
sorted-set bounds, schema parsing, or network-fault cut/restore semantics.
|
||||
- Legacy suppressions are checked by architecture tests that reject old imports outside the named
|
||||
compatibility surface.
|
||||
|
||||
### Repository verification
|
||||
|
||||
Run from `src/`:
|
||||
|
||||
```bash
|
||||
./gradlew test --no-daemon --console=plain
|
||||
./gradlew check --no-daemon --console=plain
|
||||
./gradlew build --warning-mode=fail --no-daemon --console=plain
|
||||
./gradlew clean build --warning-mode=all --no-daemon --console=plain
|
||||
./gradlew verifyCleanArchitectureDependencies verifyRuntimeModuleMembership \
|
||||
verifyDependencyLocks verifyPublicPathSnapshot verifyEnvKeys \
|
||||
--no-daemon --console=plain
|
||||
```
|
||||
|
||||
Also verify the real gate matrix, wrapper contract, shell syntax, warning-report XML, skipped-test
|
||||
inventory, and `git diff --check`.
|
||||
|
||||
## Failure Handling
|
||||
|
||||
- If a suggested warning fix changes a public signature or weakens an identity/security invariant,
|
||||
retain the behavior and use an exact documented suppression backed by a regression.
|
||||
- If three attempted fixes in one warning family fail or expose cross-module coupling, stop that
|
||||
family and revisit the design instead of stacking suppressions.
|
||||
- If the AWS retry or Toxiproxy migration cannot reproduce old behavior, report that qualification
|
||||
as blocked; do not claim warning-zero by suppressing the deprecation.
|
||||
- If a warning originates only in generated code, prove the generated source owner and configure
|
||||
that exact generated boundary; do not disable warnings for handwritten sources.
|
||||
|
||||
## Risks and Mitigations
|
||||
|
||||
- **ETag grammar regression:** use quote-aware focused tests before replacing the parser.
|
||||
- **Authorization drift:** test role normalization under Turkish locale.
|
||||
- **Redis catalog weakening:** retain identity comparison and test cross-catalog rejection.
|
||||
- **AWS retry semantic drift:** assert maximum attempts and backoff classes/policies, then run the
|
||||
object-storage provider tests.
|
||||
- **Legacy data stranding:** preserve legacy activation and characterization until the separate
|
||||
data/API migration gates are approved.
|
||||
- **Hidden diagnostics:** capture-and-assert expected stderr; never discard it.
|
||||
- **Suppression creep:** exact annotations plus architecture/import checks prevent module-wide
|
||||
exemptions.
|
||||
- **Build duration:** use owner-focused RED/GREEN loops and reserve full clean builds for integration
|
||||
checkpoints and final proof.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
The work is complete only when:
|
||||
|
||||
1. All behavior regressions and focused owner checks pass.
|
||||
2. Every compiler task passes with `-Werror`, deprecation lint, unchecked lint, and Error Prone.
|
||||
3. Every ordinary/custom Checkstyle and SpotBugs task included by `check` is blocking and clean.
|
||||
4. Legacy warnings are limited to no output because exact compatibility code is explicitly and
|
||||
locally justified; no module/task-wide suppression exists.
|
||||
5. The Redis lab successful contract prints only its success summary and unexpected child
|
||||
diagnostics still fail the test with captured evidence.
|
||||
6. Test JVMs print no CDS/self-attachment warning.
|
||||
7. Full test, check, build, dependency, architecture, runtime-membership, env, public-path, wrapper,
|
||||
gate-matrix, shell, and diff validators pass.
|
||||
8. The final fresh clean-build log contains no `warning:`, deprecated/unchecked `Note:`, SpotBugs
|
||||
non-zero message, OpenJDK warning, or leaked expected-negative Redis diagnostic.
|
||||
9. LLM Wiki capture records commands, results, resolved warning counts, suppressions, and any
|
||||
environment-only qualification not executed locally.
|
||||
@@ -0,0 +1,72 @@
|
||||
# Web Security Boundary Design
|
||||
|
||||
**Date:** 2026-08-02
|
||||
**Status:** approved by the user's instruction to apply the reviewed P1/P2 work sequentially
|
||||
**Scope:** JWT/OIDC/JWKS and CORS behavior at the `adapter:inbound:web` Spring Security filter boundary
|
||||
|
||||
## Context
|
||||
|
||||
The module has unit contracts for JWT validators, exception classification, envelope writers, and
|
||||
CORS settings. It does not yet prove that a real bearer request crosses issuer discovery, JWKS
|
||||
retrieval, signature/claim validation, principal conversion, `SecurityFilterChain`, and the public
|
||||
error envelope. CORS configuration is likewise untested at the filter boundary, where preflight
|
||||
ordering relative to authentication is the important behavior.
|
||||
|
||||
These are release-boundary checks and must not silently skip because an external IdP, environment
|
||||
variable, or optional flag is absent.
|
||||
|
||||
## Decision
|
||||
|
||||
Add a dedicated `webSecurityBoundaryTest` task that reuses the ordinary test output/classpath and
|
||||
runs only JUnit tests tagged `security-boundary`. Ordinary `test` excludes that tag so each contract
|
||||
runs once. The dedicated task:
|
||||
|
||||
- fails when no tests are discovered;
|
||||
- disables up-to-date reuse;
|
||||
- fails the root suite when any test reports `SKIPPED`;
|
||||
- is required by the inbound-web `check` task;
|
||||
- uses UTC and no environment-dependent conditions or assumptions.
|
||||
|
||||
JWT tests use a JDK loopback `HttpServer` bound to `127.0.0.1` on an ephemeral port. It serves the
|
||||
minimum OIDC discovery document and JWKS response. Tests generate ephemeral RSA keys and compact
|
||||
RS256 JWTs with the already-resolved Nimbus dependency; no new library or external network is
|
||||
allowed. Each failure case uses a fresh server and Spring context to prevent decoder/JWK cache
|
||||
cross-contamination.
|
||||
|
||||
CORS tests build the production `SecurityConfig` and real `springSecurityFilterChain` with direct
|
||||
configuration properties. They issue real preflight and actual-origin MockMvc requests. A test JWT
|
||||
decoder bean is allowed here because CORS ordering—not token decoding—is the owned boundary.
|
||||
|
||||
## JWT/JWKS Contract
|
||||
|
||||
- application context startup performs zero discovery/JWKS calls (lazy decoder);
|
||||
- a correctly signed token reaches a protected controller and exposes the expected
|
||||
`AuthenticatedPrincipal` subject/roles;
|
||||
- expiry beyond the configured 60-second skew, issuer mismatch, audience mismatch, wrong
|
||||
signature, and unknown `kid` produce their exact stable 401 error codes and bounded
|
||||
`WWW-Authenticate`/`Retry-After` headers;
|
||||
- deterministic JWKS 503 produces `AUTH_JWKS_UNAVAILABLE`, HTTP 503, and `Retry-After: 30`;
|
||||
- after that first-request 503, the same lazy decoder/context retries initialization and succeeds
|
||||
once the JWKS endpoint recovers;
|
||||
- discovery metadata that is fetched successfully but is internally inconsistent produces the
|
||||
fixed 500 `INTERNAL_AUTH_MISCONFIGURATION` envelope rather than a raw initialization exception;
|
||||
- responses never contain the bearer token, issuer URL, `kid`, JWK material, or internal decoder
|
||||
diagnostics.
|
||||
|
||||
## CORS Contract
|
||||
|
||||
- an approved credentialed preflight to an authenticated endpoint succeeds before bearer
|
||||
authentication and emits exact origin/credentials/method/header/max-age policy;
|
||||
- an unapproved origin receives 403 without allow-origin or allow-credentials reflection;
|
||||
- disabled CORS emits no CORS response headers;
|
||||
- wildcard origin without credentials returns `*` and no credentials header;
|
||||
- an approved actual-origin request receives matching CORS and bounded `Vary` headers;
|
||||
- wildcard plus credentials remains a settings startup failure (already covered by settings tests).
|
||||
|
||||
## Non-Goals
|
||||
|
||||
- external IdP/TLS/rotation rehearsal;
|
||||
- browser-engine SameSite behavior;
|
||||
- Redis-backed session continuity (the next P1 batch);
|
||||
- new test libraries, Docker, or changes to production dependency direction;
|
||||
- staging, commit, amend, or push by an agent.
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user