feat: redis, fileserver, httpclient 런타임 시점 구현 추가

This commit is contained in:
donghyeon-ka
2026-07-28 14:26:54 +09:00
parent 7363b2aa1e
commit b3add0162d
257 changed files with 30430 additions and 1357 deletions
+120
View File
@@ -540,6 +540,18 @@ env_keys:
compatibility_impact: behavior-change
required_test: outbound-contract:global-call-timeout-bounded
- name: APP_OUTBOUND_HTTP_MAXIMUM_IN_FLIGHT_CALLS
type: int
default: 128
allowed_values: null
classification: public-config
required: false
reload_policy: restart-only
owner_branch: httpclient-production-capability
validation: int_range_1_10000
compatibility_impact: additive
required_test: outbound-contract:maximum-in-flight-calls-bounded
- name: APP_OUTBOUND_HTTP_RETRY_ENABLED
# source: feature-outbound-http-client-baseline 2026-05-22
# "retry 기본값은 disabled이며, 활성화 시 retryable registry error와 low-cardinality retry metric이 필수"
@@ -1230,6 +1242,18 @@ env_keys:
compatibility_impact: behavior-change
required_test: adapter-contract:redis-disabled-default
- name: APP_CACHE_REDIS_CLIENT_MODE
type: enum
default: managed
allowed_values: [managed, external]
classification: public-config
required: false
reload_policy: restart-only
owner_branch: redis-production-capability
validation: enum_strict
compatibility_impact: additive
required_test: adapter-contract:redis-client-mode-explicit
- name: APP_CACHE_REDIS_HOST
# source: feature-cache-consistency-contract — Redis adapter (활성화 시 endpoint 필요)
type: string
@@ -1256,6 +1280,102 @@ env_keys:
compatibility_impact: behavior-change
required_test: cache-contract:redis-port-bound
- name: APP_CACHE_REDIS_PASSWORD
type: string
default: null
allowed_values: null
classification: secret
required: false
reload_policy: restart-only
owner_branch: codex-phase-a-ci-recovery
validation: must_not_be_local_dev_sentinel_in_prod
compatibility_impact: additive
required_test: secrets-contract:redis-password-no-leak
- name: APP_CACHE_REDIS_KEY_HMAC_SECRET
type: string
default: null
allowed_values: null
classification: secret
required: false
reload_policy: restart-only
owner_branch: codex-phase-a-ci-recovery
validation: base64_min_32_bytes_when_redis_enabled
compatibility_impact: additive
required_test: cache-contract:redis-hmac-secret-bounded
- name: APP_CACHE_REDIS_COMMAND_TIMEOUT
type: duration
default: 2s
allowed_values: null
classification: public-config
required: false
reload_policy: restart-only
owner_branch: codex-phase-a-ci-recovery
validation: spring_duration_shorthand_non_zero_le_30s
compatibility_impact: additive
required_test: cache-contract:redis-command-timeout-bounded
- name: APP_CACHE_REDIS_MAXIMUM_QUEUED_COMMANDS
type: int
default: 8
allowed_values: null
classification: public-config
required: false
reload_policy: restart-only
owner_branch: codex-phase-a-ci-recovery
validation: int_1_to_4096
compatibility_impact: additive
required_test: cache-contract:redis-command-queue-bounded
- name: APP_CACHE_REDIS_MAXIMUM_IN_FLIGHT_BYTES
type: int
default: 16777216
allowed_values: null
classification: public-config
required: false
reload_policy: restart-only
owner_branch: codex-phase-a-ci-recovery
validation: covers_maximum_value_and_le_268435456
compatibility_impact: additive
required_test: cache-contract:redis-command-byte-admission-bounded
- name: APP_CACHE_REDIS_NAMESPACE_ENVIRONMENT
type: string
default: local
allowed_values: null
classification: public-config
required: false
reload_policy: restart-only
owner_branch: codex-phase-a-ci-recovery
validation: lowercase_slug
compatibility_impact: behavior-change
required_test: cache-contract:redis-namespace-environment-bound
- name: APP_CACHE_REDIS_SEMANTIC_REGION
type: string
default: default
allowed_values: null
classification: public-config
required: false
reload_policy: restart-only
owner_branch: codex-phase-a-ci-recovery
validation: lowercase_slug
compatibility_impact: behavior-change
required_test: cache-contract:redis-semantic-region-bound
- name: APP_CACHE_REDIS_MAXIMUM_VALUE_BYTES
type: int
default: 1048576
allowed_values: null
classification: public-config
required: false
reload_policy: restart-only
owner_branch: codex-phase-a-ci-recovery
validation: int_1_to_16777216
compatibility_impact: additive
required_test: cache-contract:redis-value-size-bounded
- name: APP_CACHE_DEFAULT_TTL
# source: feature-cache-consistency-contract 2026-05-22
# "TTL | explicit per key family | no-cache for sensitive data | immortal cache forbidden"
@@ -89,6 +89,18 @@ secrets:
compatibility_impact: breaking
required_test: secrets-contract:redis-password-no-leak
- name: APP_CACHE_REDIS_KEY_HMAC_SECRET
# Stable cache-key HMAC material. It is distinct from the Redis authentication credential.
classification: secret
source: secret-manager
rotation_policy: dual-read-restart-only
prod_default: null
dev_sentinel_prefix: __LOCAL_DEV_
owner_branch: codex-phase-a-ci-recovery
masking_rule: full
compatibility_impact: breaking
required_test: secrets-contract:redis-key-hmac-no-leak
- name: APP_PRIVACY_PSEUDONYMIZATION_SALT
# source: feature-data-retention-privacy-contract 2026-05-22
# "pseudonymization key = HMAC-SHA-256 with rotating salt. salt rotation interval = 90일.
+4
View File
@@ -0,0 +1,4 @@
# feature-security-operational-baseline D5 — deny-by-default public path snapshot.
# SSOT: SECURITY_PUBLIC_PATHS (src/.env) -> SecurityConfig permitAll(); anyRequest authenticated.
# Regenerate after review with: ./gradlew verifyPublicPathSnapshot -PapprovePublicPathChange
/api/healthcheck
@@ -1,3 +1,7 @@
> **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`
@@ -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.
@@ -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.
@@ -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 01/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,79 @@
# 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 binding
composition, exact readiness tuple registry, Apache HC5 pool, active cancellation, 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`
- [ ] Write RED tests for expiry, remaining time, finite bounds, and parent/child intersection.
- [ ] Implement without Spring, wall-clock timestamps, scheduler, or HTTP types.
- [ ] Verify GREEN.
### 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`
- [ ] Write RED tests for ID/uniqueness/cross-field operation invariants.
- [ ] Write RED tests rejecting absolute, scheme-relative, traversal, user-info, query/fragment, and
multi-segment variables.
- [ ] Implement closed immutable descriptors and one-pass path-segment encoding.
- [ ] Verify GREEN.
### 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`
- [ ] Reproduce streaming 5xx body delivery and logical-call-only circuit-breaker counting.
- [ ] Make streaming validate status before exposing the body and discard error bodies.
- [ ] Put circuit breaker around each physical attempt and retry around the attempt loop.
- [ ] Set JDK redirects to `NEVER` explicitly and validate legacy base URI/relative request targets.
- [ ] Verify focused regressions and the full legacy test suite.
### 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`
- [ ] Mark the implemented foundation and fixed legacy defects.
- [ ] Keep total deadline/cancellation, canonical zero-binding composition, Apache pool, fixed
egress, TLS/auth, bounded decoded streaming, and R2 cards unimplemented.
- [ ] Run:
```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,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`
- [ ] Write a failing test for hit/negative/miss/unavailable distinctions and immutable metadata.
- [ ] Verify RED with `./gradlew :application-core:test --tests '*CacheRegionContractTest'`.
- [ ] Implement only framework-free values and ports.
- [ ] Verify GREEN.
### 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`
- [ ] Write a failing test proving namespace isolation, one stable hash tag, bounded key bytes, and
absence of raw sensitive resource identifiers.
- [ ] Verify RED.
- [ ] Implement SHA-256 for opaque IDs and HMAC-SHA-256 for sensitive scopes using defensive secret
copies and length-prefixed component encoding.
- [ ] 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`
- [ ] Write failing catalog and facade tests.
- [ ] Verify RED.
- [ ] Implement exact resource digest, key/argument bounds, typed status mapping, and no generic
application-facing execution surface.
- [ ] 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`
- [ ] Mark only contract/key/program foundation as implemented and all real runtime/capability
promotion as unimplemented.
- [ ] Run:
```bash
cd src
./gradlew :application-core:check :adapter:outbound:cache-redis:check --console=plain
./gradlew verifyCleanArchitectureDependencies --console=plain
```
- [ ] 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.
@@ -1,3 +1,7 @@
> **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
@@ -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,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,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
@@ -0,0 +1,415 @@
# Fileserver R2 Control Plane and Provider Selection Design
- Date: 2026-07-28
- Status: 승인된 설계, 구현 전
- 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`: startup에서 생성된 bounded destination route allowlist 값;
- `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 하나를 참조한다.
- 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 설정과
동시에 활성화되면 startup을 실패시킨다. 암묵 migration이나 precedence를 두지 않는다.
## 7. Startup capability compilation
application traffic을 받기 전에 destination별 effective descriptor를 한 번 compile한다.
`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를 받지 않는다.
### 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는 자동 덮어쓰지 않는다.
## 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가 아니다.
## 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 완성 |
| data digest mismatch | `QUARANTINED`, integrity failure |
| 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
```
모순이 있으면 임의 성공이나 삭제 대신 quarantine evidence를 기록한다.
## 11. Compatibility
- R1 journal schema v1은 읽을 수 있어야 한다.
- R1 terminal receipt는 기존 `PROCESS_LOCAL_SYNC` 보장 그대로 복원한다.
- 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가 아니면 정리;
- sealed 이후 filesystem timeout/IO/root identity change: indeterminate;
- published data와 metadata 불일치: integrity/quarantine;
- 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;
- 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;
- target collision no overwrite;
- data/manifest/reference digest mismatch quarantine.
### 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는 허용하지 않는다.
### 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다.