merge: integrate object storage production capability

This commit is contained in:
donghyeon-ka
2026-08-01 00:57:40 +09:00
445 changed files with 32429 additions and 796 deletions
@@ -0,0 +1,81 @@
# Object Storage Batch A Checkpoint
- Date: 2026-07-28
- Branch: `codex/objectstorage-production-capability`
- Worktree:
`/home/donghyeon/workspace/clean-architecture-backend-template-objectstorage`
- Claimed level: R0 application contract only
- Provider readiness advanced: no
## Implemented scope
- Characterized the legacy caller-key overwrite, whole-object materialization, locator exposure,
eager filesystem directory creation, optional S3 bucket provisioning, and Poster transaction/API
coupling without changing those behaviors.
- Added provider-neutral identities, opaque checked references/handles, bounded streaming
callbacks, content identity, digest/range values, requests, receipts, outcomes, and narrow ports
under `dev.caskeleton.application.objectstorage`.
- Required an `ObjectOperationKey` on mutation requests and separated normal publication,
scan-maintenance, purge-maintenance, direct, and staged privilege surfaces.
- Added recursive contract-purity tests and an ArchUnit freeze for the one existing sample legacy
import.
- Marked the legacy `ObjectStoragePort` and `StoredObject` as removal boundaries without adapting
new semantic calls back to raw keys.
No provider-neutral kernel, canonical namespace/control codec, local R1 provider, S3/MinIO
qualification, sample migration, or R2 readiness claim is included.
## TDD evidence
The planned RED checks failed only for the intentionally missing types or removal annotations:
- `ObjectStorageIdentityContractTest`: missing identity types before Task 2 implementation.
- `ObjectContentContractTest` and `ObjectStorageValueContractTest`: missing content/value types
before Task 3 implementation.
- `ObjectStoragePortContractTest`: missing request/receipt/port family before Task 4 implementation.
- `ObjectStorageArchitectureContractTest`: missing legacy removal annotations before Task 5
implementation.
An initial ArchUnit DSL compilation error was a test-authoring error, not accepted as a RED result;
the rule was corrected and rerun.
## GREEN verification
All commands ran from `src/` and completed with `BUILD SUCCESSFUL`:
```bash
./gradlew :application-core:resolveAndLockAll --write-locks
./gradlew :application-core:verifyDependencyLocks --console=plain
./gradlew :application-core:test --tests '*ObjectStorageIdentityContractTest' --console=plain
./gradlew :application-core:test \
--tests '*ObjectContentContractTest' \
--tests '*ObjectStorageValueContractTest' --console=plain
./gradlew :application-core:test --tests '*ObjectStoragePortContractTest' --console=plain
./gradlew :application-core:test \
--tests '*ObjectStorageArchitectureContractTest' --console=plain
./gradlew :app-bootstrap:test --tests '*CleanArchitectureTest' --console=plain
./gradlew :application-core:check --console=plain
./gradlew verifyCleanArchitectureDependencies --console=plain
./gradlew :adapter:outbound:objectstorage:test :sample-portfolio:test --console=plain
```
The final combined legacy focused suites completed in 27 seconds. Deprecation-for-removal warnings
are expected evidence that legacy consumers remain visible; they are not suppressed.
## LLM Wiki capture
The canonical vault required by repository policy,
`/home/donghyeon/workspace/ai-tool/llm-wiki-private/`, and its parent
`/home/donghyeon/workspace/ai-tool/` do not exist in this environment. Therefore the required
`raw/branch-notes/codex-objectstorage-production-capability.md` could not be created or updated.
No similarly named non-canonical clone was used. This exact access block is recorded in both the
plan and design headers and here at the Batch A boundary.
## Remaining gates and risks
- External broker and REST consumers and deployed legacy data were not inspected; Gate A remains
blocked for legacy removal or public API versioning.
- The new contracts have no provider implementation yet.
- The current legacy adapter retains whole-object and raw-locator behavior by design until the
later migration batch.
- No readiness registry row is promoted by this checkpoint.
@@ -0,0 +1,109 @@
# Object Storage Batch B Checkpoint
- Date: 2026-07-28
- Branch: `codex/objectstorage-production-capability`
- Worktree:
`/home/donghyeon/workspace/clean-architecture-backend-template-objectstorage`
- Evidence grade: repository-local non-skipping unit/contract/application-context tests
- Advanced cards: local managed single upload R1, local managed download R1
- R2 or production-provider readiness advanced: no
## Implemented scope
- Added deterministic data/control namespaces, opaque reference/handle codecs, canonical request
fingerprints, frozen binding/policy revisions, and bounded operation epochs.
- Added six strict canonical JSON control-record families with fixed field order, outer SHA-256
envelopes, schema/size checks, corruption rejection, and checked-in golden digests.
- Added provider-neutral publication, scan, reference, direct-session, multipart, and pending-effect
state transitions with same-operation replay and conflicting-intent rejection.
- Added a provider contract and `filesystem-local-dev` implementation with bounded streaming,
immutable exclusive create, SHA-256 verification, exact inspect/version, full/range transfer,
conditional retirement, create resolution, restrictive permissions, and path/symlink
confinement.
- Added single-process exact-version control CAS and restart/corruption/fault characterization.
Logical control keys use `.record` physical leaves locally so object-store-valid prefix/leaf key
pairs cannot collide as filesystem file/directory paths.
- Added constructor-bound `app.object-storage` settings and compile-before-construction
provider/destination/route/policy binding. The capability is disabled by default and
`filesystem-local-dev` is rejected for `prod`/`production`.
- Added disabled, unselected, invalid, selected-success, selected-construction-failure, close,
legacy-only, and namespace-separated dual-run composition tests.
- Added semantic routing evidence for publish, replay without producer invocation, inspect,
full transfer, absent reference, and exact retained route lookup.
- Added the exact nine-card readiness registry. Only local managed single upload/download are R1;
direct, multipart, quarantine, retention, and production reconciliation remain R0.
## TDD and defect evidence
Planned RED checks failed for the intentionally absent codec/kernel/provider/settings/readiness
types before each implementation. Additional tests found and drove these corrections:
- Local control keys may legally have both a leaf and a child in object storage, while a filesystem
cannot have both `reference` and `reference/lifecycle`; local physical `.record` mapping fixed the
collision without changing logical keys.
- `ObjectInspectionPort.inspect` initially threw for an absent known-route reference; it now
returns `Optional.empty()` while incomplete/corrupt evidence still fails closed.
- The application purity test initially scanned its own test output after a full `check`; it now
derives the production class root from a production contract type.
- The general B7 ArchUnit rule initially classified objectstorage provider-internal SPI/control
return values as public adapter responses. The existing negative fixture remains active, while a
dedicated non-empty rule now checks the actual objectstorage `*Adapter` semantic boundaries.
No skipped Docker or external-service test is used as Batch B readiness evidence.
## GREEN verification
All commands ran from `src/` unless noted and completed with `BUILD SUCCESSFUL` after the documented
RED/fix cycles:
```bash
./gradlew :adapter:outbound:objectstorage:verifyDependencyLocks --console=plain
./gradlew :adapter:outbound:objectstorage:test \
--tests '*ObjectNamespaceCodecTest' \
--tests '*ObjectRequestFingerprintCodecTest' \
--tests '*ObjectOperationEpochTest' --console=plain
./gradlew :adapter:outbound:objectstorage:test \
--tests '*ObjectControlRecordCodecTest' \
--tests '*ObjectOperationStateMachineTest' \
--tests '*ObjectOperationKernelTest' --console=plain
./gradlew :adapter:outbound:objectstorage:test \
--tests '*ObjectStorageProviderContract' \
--tests '*LocalDevObjectStorageProviderTest' \
--tests '*LocalDevObjectStorageRecoveryTest' --console=plain
./gradlew :adapter:outbound:objectstorage:test \
--tests '*ObjectStorageBindingCompilerTest' \
--tests '*ObjectStorageCapabilityConfigTest' \
--tests '*RoutingObjectStorageAdapterTest' --console=plain
./gradlew :adapter:outbound:objectstorage:test \
--tests '*ObjectStorageReadinessRegistryTest' --console=plain
./gradlew :sample-portfolio:test --console=plain
./gradlew :application-core:check \
:adapter:outbound:objectstorage:check --console=plain
./gradlew :app-bootstrap:test --tests '*CleanArchitectureTest' --console=plain
./gradlew verifyCleanArchitectureDependencies --console=plain
```
The final combined application/objectstorage checkpoint completed in 23 seconds. The focused
Clean Architecture suite and dependency verification also passed.
## LLM Wiki capture
The canonical vault required by repository policy,
`/home/donghyeon/workspace/ai-tool/llm-wiki-private/`, and its parent
`/home/donghyeon/workspace/ai-tool/` do not exist in this environment. Therefore the required
`raw/branch-notes/codex-objectstorage-production-capability.md` and any derived raw documents could
not be created or updated. No similarly named non-canonical clone was used. This exact access block
is recorded in the plan/design status and at this Batch B boundary.
## Remaining gates and risks
- `filesystem-local-dev` has no multi-node linearizability or power-loss durability evidence and is
forbidden in production profiles.
- The canonical S3/MinIO provider contribution, async bounded transport, provider qualification,
response-loss fault tests, and protected AWS evidence are not implemented.
- Direct grants, multipart, quarantine/scan, retention/legal hold, privileged purge, reapers, and
production reconciliation remain R0.
- The sample Poster workflow still uses the deprecated whole-`byte[]` port and transaction-coupled
legacy choreography. It is explicitly activated only in sample local/test configuration.
- External API/broker consumers and deployed legacy data remain uninspected, so Gate A still blocks
destructive migration or legacy removal.
@@ -0,0 +1,84 @@
# Object Storage Batch C Checkpoint
- Date: 2026-07-28
- Branch: `codex/objectstorage-production-capability`
- Worktree:
`/home/donghyeon/workspace/clean-architecture-backend-template-objectstorage`
- Evidence grade: repository-local tests plus digest-pinned single-node MinIO/Toxiproxy tests
- AWS execution: not authorized; source set compiled only
- Production-provider readiness advanced: no
## Implemented scope
- Added exact AWS S3 and MinIO provider bindings, bounded evidence descriptors, qualifier/error
mapping, secret references, endpoint/owner/addressing validation, and selected-only lifecycle
construction.
- Added bounded async request/response bridges and the managed S3 put, inspect, full/range download,
checksum, exact-version, cancellation, and content-length paths.
- Added canonical conditional S3 control storage and operation response-loss resolution. Provider
ETags remain adapter-private and are never exposed as logical versions.
- Added low-level managed multipart planning, sharded immutable part ledgers, initiate-before-I/O
state, explicit create/upload/list/complete/abort calls, and exact completion verification.
- Added non-skipping MinIO contract/fault lanes, an AWS compile-only qualification lane, a protected
workflow, and gate-matrix coverage.
The exact MinIO image is
`minio/minio@sha256:4c4a4876193f030c81f57aabb22bcb9a73462010eb61fcab66908e03e5484af8`.
The exact Toxiproxy image is
`ghcr.io/shopify/toxiproxy@sha256:9378ed52a28bc50edc1350f936f518f31fa95f0d15917d6eb40b8e376d1a214e`.
## Exact MinIO finding
Real-provider tests proved an asymmetric conditional profile:
- `PutObject If-None-Match: *` was accepted but overwrote an existing object.
- stale `PutObject If-Match` was rejected with HTTP 412.
- `CompleteMultipartUpload If-None-Match: *` was accepted and overwrote an existing object.
- checksum, HEAD, and range behavior passed the exercised contract.
Because immutable create and create-if-absent control CAS cannot be proven, the exact MinIO managed
and direct mutation profiles remain `UNSUPPORTED`. The implementation does not emulate missing
atomicity with HEAD followed by an unconditional write and does not promote a readiness card.
## TDD and verification
The task-focused RED runs first failed on the planned absent binding, bridge, conditional store,
multipart, and qualification types. Provider qualification then found the real MinIO conditional
behavior above; the descriptor and negative contract were changed instead of weakening the
contract.
Commands completed with `BUILD SUCCESSFUL`:
```bash
cd src
./gradlew :adapter:outbound:objectstorage:test \
--tests '*S3ProviderBindingTest' \
--tests '*S3ProviderQualifierTest' \
--tests '*S3ProviderCompositionTest' --console=plain
./gradlew :adapter:outbound:objectstorage:objectStorageMinioContractTest --console=plain
./gradlew :adapter:outbound:objectstorage:objectStorageMinioFaultTest --console=plain
./gradlew :adapter:outbound:objectstorage:objectStorageAwsQualificationTestClasses --console=plain
./gradlew :adapter:outbound:objectstorage:check --console=plain
./gradlew :adapter:outbound:objectstorage:verifyDependencyLocks \
verifyCleanArchitectureDependencies --console=plain
bash ../.github/scripts/verify-gate-matrix.sh
```
The gate matrix reports 22 gates: 21 verified and the protected AWS qualification gate explicitly
`delegated-pending`.
## LLM Wiki capture
The canonical vault `/home/donghyeon/workspace/ai-tool/llm-wiki-private/` and its parent
`/home/donghyeon/workspace/ai-tool/` do not exist in this environment. The required
`raw/branch-notes/codex-objectstorage-production-capability.md` and derived raw documents could not
be created or updated. No similarly named non-canonical clone was used.
## Remaining risks
- No AWS request was executed, so there is no observed AWS provider claim.
- The pinned MinIO topology is a local single-node container and is not production TLS,
multi-node, durability, or linearizability evidence.
- The detailed managed multipart fault matrix is not exhaustive enough for R2.
- No sample migration, public API, scan/publication choreography, retention, purge, or reaper is
included in this checkpoint.
@@ -0,0 +1,104 @@
# Object Storage Batch D Checkpoint
- Date: 2026-07-28
- Branch: `codex/objectstorage-production-capability`
- Worktree:
`/home/donghyeon/workspace/clean-architecture-backend-template-objectstorage`
- Scope: direct-transfer provider/application primitives only
- Public endpoint: none
- Readiness advanced: no; all direct cards remain R0
## Implemented scope
- Added direct single-upload session policy, durable prepared/issued transitions, bearer
redaction, exact completion verification, published-version download resolution, and an
S3-presigner lifecycle owned by the selected provider.
- Added direct multipart durable session and part-grant families, opaque acknowledgement tokens,
sharded part records, admission-close/expiry fencing, exact ledger validation, completion/abort
states, response-loss resolution, and persisted terminal exact-version replay.
- Added direct S3 initiate/discovery, exact-part presign, `ListParts` acknowledgement, conditional
complete followed by exact HEAD verification, and abort resolution.
- Registered direct single and multipart delegates only when their exact compiled capability is
selected. One presigner is constructed and closed exactly once.
- Added golden canonical envelopes for the direct session, direct multipart session, and direct
multipart grant families.
- Fixed `MultipartCompleteRequest` null validation so valid immutable `List.of(...)` input no longer
throws from `contains(null)`.
## Qualification truth
The exact MinIO release cannot prove create-only PUT or create-only multipart completion, so both
direct profiles are explicitly `UNSUPPORTED`. The direct MinIO contract/fault lanes are negative
admission tests: they prove no bearer or multipart mutation enters an unsupported profile. No test
skip is used as positive evidence.
The AWS managed/direct source sets compile, but no AWS call was made and no AWS evidence row was
published. No inbound controller, authorization surface, CORS runtime configuration, or public
direct API exists.
## Verification
Commands completed with `BUILD SUCCESSFUL`:
```bash
cd src
./gradlew :adapter:outbound:objectstorage:test \
--tests '*DirectTransferCoordinatorTest' \
--tests '*PresignedGrantRedactionTest' \
--tests '*S3DirectTransferProviderTest' \
--tests '*ObjectControlRecordCodecTest' \
--tests '*S3ProviderCompositionTest' --console=plain
./gradlew :adapter:outbound:objectstorage:test \
--tests '*DirectMultipartCoordinatorTest' \
--tests '*DirectMultipartRaceTest' \
--tests '*S3DirectMultipartProviderTest' \
--tests '*ObjectControlRecordCodecTest' \
--tests '*S3ProviderCompositionTest' --console=plain
./gradlew \
:adapter:outbound:objectstorage:objectStorageMinioContractTest \
:adapter:outbound:objectstorage:objectStorageMinioFaultTest \
--tests '*DirectTransfer*' --console=plain
./gradlew :adapter:outbound:objectstorage:test \
--tests '*DirectTransferCorsContractTest' --console=plain
./gradlew :adapter:outbound:objectstorage:objectStorageAwsQualificationTestClasses --console=plain
./gradlew :adapter:outbound:objectstorage:check --console=plain
./gradlew :adapter:outbound:objectstorage:verifyDependencyLocks \
verifyCleanArchitectureDependencies --console=plain
./gradlew \
:adapter:outbound:objectstorage:objectStorageMinioContractTest \
:adapter:outbound:objectstorage:objectStorageMinioFaultTest --console=plain
bash ../.github/scripts/verify-gate-matrix.sh
./gradlew test --console=plain
./gradlew check --console=plain
```
The module `check` includes unit tests, Checkstyle, Spotless, SpotBugs, architecture, configuration
processor, environment-key, and repository-wide policy checks. Existing test-only compiler
warnings remain non-failing. The final repository-wide test run completed 79 tasks and the final
repository-wide check completed 214 tasks.
## Deliberate limitations
- Issued bearer material is process-local. A restart fails closed instead of reconstructing or
reissuing an already-issued bearer.
- The signing clock/window is stored and bounded, but AWS SDK presigner query timing is not driven
by the injected application clock.
- The direct multipart recovery/race matrix covers its principal fences and completion response
loss but is not exhaustive enough for an R2 claim.
- Retention/Object Lock grant headers and a provider-enforced direct-single hard size ceiling are
not qualified.
- No public endpoint exists, so CORS evidence is a pure contract and no direct card may exceed R0
in the current registry.
## Approval Gate A
Tasks 2024 remain blocked until the user explicitly approves scanner ownership, the sample's
first publication profile, the additive asynchronous API/status contract, and digest transport.
No scan/publication/sample endpoint implementation was started.
## LLM Wiki capture
The canonical vault `/home/donghyeon/workspace/ai-tool/llm-wiki-private/` and its parent
`/home/donghyeon/workspace/ai-tool/` do not exist in this environment. The required
`raw/branch-notes/codex-objectstorage-production-capability.md` and derived raw documents could not
be created or updated. No similarly named non-canonical clone was used.
@@ -0,0 +1,94 @@
# Object Storage Batch E Pause Checkpoint
- Recorded: 2026-07-29 (Asia/Seoul)
- Branch: `codex/objectstorage-production-capability`
- Worktree:
`/home/donghyeon/workspace/clean-architecture-backend-template-objectstorage`
- Status: implementation in progress; intentionally paused at the user's request
- Evidence grade: local unit/integration/architecture evidence only; no AWS R2 evidence
## Implemented at this checkpoint
- Staged integrity verification, fake-scanner routing, publication handoff fencing, and stable
replay receipts.
- Additive Poster V8 dual-read schema (renumbered from branch-local V7 during JPA integration),
upload/retirement intents, HMAC-sanitized idempotency scope,
PostgreSQL atomic claim SPI, and forward-only migration qualification lane.
- Short-transaction Poster image publication flow and additive locator-free `202` API under the
AIP-122-compatible `/posters/{id}/imagePublications` collection.
- Exact-reference/version logical retirement enqueue, lease/fence takeover, response-loss retry,
Poster deletion survival, and disabled-by-default worker composition.
- Isolated legacy migration contracts, report-only inspection, two-distinct-approver Ed25519
approval verification, nonce replay boundary, and explicit maintenance-only composition.
## Verification completed
The following focused command passed after the final architecture fixes:
```bash
cd src
./gradlew \
:sample-portfolio:spotlessApply \
:sample-portfolio:test --tests '*PosterImagePublicationControllerWireTest' \
:app-bootstrap:test --tests '*CleanArchitectureTest' \
--console=plain
```
The following focused suites also passed during this checkpoint:
```bash
./gradlew :adapter:outbound:objectstorage:test \
--tests '*LegacyObjectAdoptionServiceTest' \
--tests '*LegacyAdoptionApprovalVerifierTest' \
--tests '*ObjectStorageLegacyMigrationConfigTest' --console=plain
./gradlew :sample-portfolio:test \
--tests '*DeletePosterImageRetirementTest' \
--tests '*PosterImageRetirementCrashMatrixTest' \
--tests '*PosterImageRetirementConfigTest' \
--tests '*LegacyPosterImageUploadCharacterizationTest' --console=plain
./gradlew :sample-portfolio:test \
--tests '*SampleApplicationContextTest' \
:sample-portfolio:posterImageMigrationTest --console=plain
```
The migration lane included
`PosterImageRetirementQualificationTest`, which proved that an exact retirement row survives
deletion of its Poster row.
## Failures found and resolved
- `spotlessJavaCheck` initially found formatting drift in newly changed application-core and
persistence files. The owner-module Spotless apply tasks fixed it.
- `SampleApplicationContextTest` initially failed because Spring's persistence exception advisor
could not CGLIB-proxy the final `PosterImageAttachmentCasRepository`. Removing `final` fixed the
context; the focused context suite then passed.
- `CleanArchitectureTest` initially rejected an application-core return type from the sample domain
and the kebab-case `image-publications` path. Conversion moved back to the application use case,
and the endpoint changed to the repository's AIP-122-compatible `imagePublications` segment. The
complete focused architecture suite then passed.
## Not yet re-run / not complete
- The combined Batch E checkpoint command stopped on the two architecture failures above before all
requested root tasks could complete. The focused failing suites passed after the fixes, but
`:sample-portfolio:check`, `verifyPublicPathSnapshot`, and the full combined Batch E command have
not been re-run after those final fixes.
- The complete repository `./gradlew test` and `./gradlew check` have not been re-run after the
Batch E additions.
- The legacy adoption runner/configuration is not yet wired to a production legacy inspector,
permission-checked trust-key loader, or durable control-record replay-store implementation.
- Tasks 2530 (Batch F) have not started in this continuation.
- Actual AWS qualification is blocked by Approval Gate B: no approved account, bucket/namespaces,
workload roles, signed deployment attestation, or mutation/test authority was supplied.
- No readiness card was promoted. Local/MinIO ceilings and unsupported conditional behavior remain
unchanged.
## Wiki capture
At this isolated-branch checkpoint, the then-selected private vault
`/home/donghyeon/workspace/ai-tool/llm-wiki-private/` was absent, so capture was blocked. The final
main integration was later captured in the user-designated public vault at
`raw/branch-notes/chore-main-worktree-capability-integration.md`, with the derived error note
`raw/errors/multi-worktree-contract-drift-2026-07-31.md`.
@@ -0,0 +1,73 @@
# Object Storage Phase 0 Inventory
- Captured: 2026-07-28
- Branch: `codex/objectstorage-production-capability`
- Scope: repository-local source, tests, configuration, migrations, and documentation
- Evidence grade: repository-local only; deployed data, broker subscribers, and external REST
consumers were not inspected
## Commands
```bash
rg -n 'application\.storage|ObjectStoragePort|StoredObject|ca-skeleton\.objectstorage|file://|s3://' \
src docs
rg -n 'image_key|posters/.*/image' src/sample-portfolio
rg -n 'poster\.image-attached|StoredObjectResponse|PosterResponse|imageKey' \
src/sample-portfolio docs
```
The commands completed successfully in the isolated worktree. Results are classified below.
Documentation hits in the Object Storage design/plan describe the migration and are not runtime
consumers. The `s3://bucket/key-1` fixture in
`IdempotencyStoreAdapterTest` belongs to the generic idempotency response-reference test and is not
an Object Storage legacy-port consumer.
## Repository-local runtime inventory
| Contract/data | Producer | Repository-local consumers | Classification |
| --- | --- | --- | --- |
| `ObjectStoragePort` / `StoredObject` | `application-core/application/storage` | filesystem and S3 adapters, `UploadPosterImageUseCase`, `PosterController`/`PosterWebMapper` | legacy runtime contract |
| `ca-skeleton.objectstorage.*` | `ObjectStorageSettings` / `ObjectStorageConfig` | sample runtime through its objectstorage runtime dependency | legacy runtime configuration |
| `file://` receipt | `FilesystemObjectStorageAdapter` | `StoredObjectResponse.location` through `PosterWebMapper` | public legacy locator |
| `s3://bucket/key` receipt | `S3ObjectStorageAdapter` | `StoredObjectResponse.location` through `PosterWebMapper` | public legacy locator |
| `/posters/{id}/image` | `PosterController` | repository tests and the generated/public HTTP contract | legacy inbound API |
| `StoredObjectResponse` | `PosterController` / `PosterWebMapper` | HTTP caller, with `key`, `size`, `contentType`, and `location` | legacy response DTO |
| `PosterResponse.imageKey` | `PosterWebMapper` | list/get/create/update/publish/archive HTTP responses | legacy general response field |
| `poster.image-attached` | `PosterEventPublisher` | no subscriber found in this repository | versionless broker event; external consumers unknown |
| `poster.image-attached.imageKey` | `PosterImageAttached` and publisher JSON | no subscriber found in this repository | raw locator-shaped event field |
| `poster.image_key` | Flyway V6, `PosterEntity`, persistence mapper | `Poster` aggregate and repository adapter | stored-data schema |
| `posters/{id}/image` key | `UploadPosterImageUseCase` | aggregate `imageKey`, event payload, DB row, HTTP response | deterministic overwriteable legacy key |
## Executable characterization
The following tests pin the current behavior without approving it as the target design:
- `LegacyObjectStorageBehaviorTest`
- caller-selected keys overwrite;
- `get` returns `Optional<byte[]>` and materializes the whole object;
- receipts expose `file://` and `s3://` locators.
- `LegacyObjectStorageConfigTest`
- missing backend configuration selects filesystem;
- context creation creates the filesystem directory before the first write;
- `autoCreateBucket=true` probes and creates a missing bucket during S3 bean construction.
- `LegacyPosterImageUploadCharacterizationTest`
- remote storage is called while `TransactionPort.inWrite` is active;
- the controller calls `MultipartFile.getBytes`;
- the response exposes raw key and provider location;
- Poster deletion leaves the legacy object untouched.
## External inventory gap and Gate A
Repository search does not prove that the following have no deployed consumers:
- `POST /posters/{id}/image`;
- `StoredObjectResponse.key` and `.location`;
- `PosterResponse.imageKey`;
- broker event type `poster.image-attached` and its `imageKey` payload;
- rows already stored in `poster.image_key`;
- filesystem/S3 objects already written under `posters/{id}/image`.
No deployed database, object namespace, access log, API client catalog, broker consumer group, schema
registry, or owning team approval was inspected. Therefore removal, in-place field rename, event
payload replacement, or legacy-object deletion remains blocked. Approval Gate A must obtain owner
and consumer evidence and choose an additive/versioned migration contract.
@@ -0,0 +1,114 @@
schema_version: 1
claims:
- card_id: object-storage-managed-upload-single
provider_type: filesystem-local-dev
provider_version: jdk-21
destination_profile: local-managed-integrity
claimed_level: R1
evidence_revision: batch-b-local-r1
evidence_expires_on: ""
required_tasks:
- ":adapter:outbound:objectstorage:check"
limitations:
- single-process control CAS only; no multi-node linearizability
- fsync and atomic move tests do not prove power-loss durability
- local development provider is forbidden in production profiles
- card_id: object-storage-managed-upload-multipart
provider_type: filesystem-local-dev
provider_version: jdk-21
destination_profile: local-unimplemented
claimed_level: R0
evidence_revision: batch-b-contract-r0
evidence_expires_on: ""
required_tasks:
- ":application-core:check"
limitations:
- multipart publication protocol is not implemented
- card_id: object-storage-managed-download
provider_type: filesystem-local-dev
provider_version: jdk-21
destination_profile: local-managed-integrity
claimed_level: R1
evidence_revision: batch-b-local-r1
evidence_expires_on: ""
required_tasks:
- ":adapter:outbound:objectstorage:check"
limitations:
- local functional full and range reads are not production-provider qualification
- no multi-node or power-loss durability claim
- card_id: object-storage-direct-upload-single
provider_type: filesystem-local-dev
provider_version: jdk-21
destination_profile: local-unimplemented
claimed_level: R0
evidence_revision: batch-b-contract-r0
evidence_expires_on: ""
required_tasks:
- ":application-core:check"
limitations:
- direct grant provider and public inbound endpoint are not implemented
- card_id: object-storage-direct-upload-multipart
provider_type: filesystem-local-dev
provider_version: jdk-21
destination_profile: local-unimplemented
claimed_level: R0
evidence_revision: batch-b-contract-r0
evidence_expires_on: ""
required_tasks:
- ":application-core:check"
limitations:
- direct multipart session and public inbound endpoint are not implemented
- card_id: object-storage-direct-download
provider_type: filesystem-local-dev
provider_version: jdk-21
destination_profile: local-unimplemented
claimed_level: R0
evidence_revision: batch-b-contract-r0
evidence_expires_on: ""
required_tasks:
- ":application-core:check"
limitations:
- direct download grant and public inbound endpoint are not implemented
- card_id: object-storage-quarantine-publication
provider_type: filesystem-local-dev
provider_version: jdk-21
destination_profile: local-unimplemented
claimed_level: R0
evidence_revision: batch-b-contract-r0
evidence_expires_on: ""
required_tasks:
- ":application-core:check"
limitations:
- scanner handoff and verdict fencing are not implemented
- card_id: object-storage-retention
provider_type: filesystem-local-dev
provider_version: jdk-21
destination_profile: local-unimplemented
claimed_level: R0
evidence_revision: batch-b-contract-r0
evidence_expires_on: ""
required_tasks:
- ":application-core:check"
limitations:
- retention and legal-hold provider enforcement are not implemented
- privileged purge composition remains intentionally empty
- card_id: object-storage-reconciliation
provider_type: filesystem-local-dev
provider_version: jdk-21
destination_profile: local-unimplemented
claimed_level: R0
evidence_revision: batch-b-contract-r0
evidence_expires_on: ""
required_tasks:
- ":application-core:check"
limitations:
- local create resolution is single-process functional evidence only
- production response-loss and multi-node reconciliation are not implemented
@@ -1,10 +1,10 @@
# Object Storage Production Capability Implementation Plan
- 작성일: 2026-07-28
- 상태: 구현 계획 작성 완료, 모든 task 미착수, R2 미구현
- 상태: Batch AD 및 Batch E pause checkpoint를 main에 통합, Batch E 전체 gate와 R2완료
- 범위: 상세 설계 Phase 06; Phase 7/R3는 별도 승인 계획
- LLM Wiki capture: 정본 vault
`/home/donghyeon/workspace/ai-tool/llm-wiki-private/` 부재로 차단; 비정본 clone 대체 사용 안 함
- LLM Wiki capture: public vault
`raw/branch-notes/chore-main-worktree-capability-integration.md`에 main 통합 기록 완료
> **For agentic workers:** REQUIRED SUB-SKILLS: use
> `superpowers:subagent-driven-development` to execute independent tasks,
@@ -43,6 +43,27 @@ provider-leaf split, a production malware-scanner implementation, and browser AP
outside the approved contracts require follow-up plans. Writing this plan changes no runtime
behavior and advances no readiness card.
**Implementation checkpoint (2026-07-28):** The Batch AB application/kernel/local-provider
baseline and the Batch CD S3/MinIO managed/direct primitive slices are implemented and verified in
`codex/objectstorage-production-capability`. Exact MinIO qualification proved that create-only PUT
and multipart completion conditions are ignored, so its managed/direct mutation profiles remain
`UNSUPPORTED`. AWS lanes compile only and have no observed evidence. Only the exact local managed
single upload/download cards remain R1; every direct, multipart, quarantine, retention, and
production reconciliation card remains R0. The exhaustive R2 fault/race matrices named by
unchecked steps remain follow-up evidence, not implied completion. The legacy provider and Poster
consumer remain isolated behind explicit sample-local/test activation. Evidence:
[Batch A](../../evidence/object-storage/batch-a-checkpoint.md),
[Batch B](../../evidence/object-storage/batch-b-checkpoint.md), and
[Batch C](../../evidence/object-storage/batch-c-progress.md),
[Batch D](../../evidence/object-storage/batch-d-checkpoint.md).
Batch E의 개별 focused fix 증거는
[Batch E pause checkpoint](../../evidence/object-storage/batch-e-pause-checkpoint.md)에 기록됐다.
당시 Batch E combined/root gate는 재실행되지 않았으므로 Batch E 전체 완료나 R2를 주장하지
않는다. 격리 브랜치에서 잘못 선택했던 private vault 경로 대신, main 통합 작업은 사용자가
지정한 public vault의 `raw/branch-notes/chore-main-worktree-capability-integration.md`
캡처했다.
---
## Execution rules
@@ -154,7 +175,7 @@ Task 30; a batch is not closed if any output is missing.
- Create:
`docs/evidence/object-storage/phase-0-inventory.md`
- [ ] **Step 1: Record current executable behavior**
- [x] **Step 1: Record current executable behavior**
Prove the current contract exactly as-is:
@@ -169,7 +190,7 @@ Prove the current contract exactly as-is:
- the response exposes raw key/location;
- Poster deletion does not retire the object.
- [ ] **Step 2: Verify the characterization baseline**
- [x] **Step 2: Verify the characterization baseline**
Run:
@@ -185,7 +206,7 @@ cd src
Expected: PASS against the current implementation. This is a baseline, not R1/R2 evidence.
- [ ] **Step 3: Inventory runtime and data dependencies**
- [x] **Step 3: Inventory runtime and data dependencies**
In the evidence document record command output, not an unsupported repository-wide conclusion:
@@ -204,7 +225,7 @@ Classify every hit as legacy runtime, test, documentation, stored-data schema, o
Record whether real deployed consumers/data were inspected; if they were not, state that external
inventory is still required.
- [ ] **Step 4: Run unchanged focused suites**
- [x] **Step 4: Run unchanged focused suites**
Run:
@@ -232,7 +253,7 @@ Expected: PASS. No source behavior changes belong to this task.
- Test:
`src/application-core/src/test/java/dev/caskeleton/application/objectstorage/ObjectStorageIdentityContractTest.java`
- [ ] **Step 1: Add the approved test-only property engine**
- [x] **Step 1: Add the approved test-only property engine**
Add `testImplementation 'net.jqwik:jqwik:1.9.1'`, matching the existing sample test version, and
update only the application-core lockfile:
@@ -245,7 +266,7 @@ cd src
Expected: PASS with test-only jqwik entries and no production dependency.
- [ ] **Step 2: Write the failing identity contract**
- [x] **Step 2: Write the failing identity contract**
Test null/blank/control-character/oversize rejection, canonical round trips, operation-key
composition, part range `1..10_000`, prefix separation, route-token grammar, reference check-digit
@@ -253,7 +274,7 @@ tampering, and provider-locator non-disclosure. Include jqwik properties for arb
input and a fixed golden vector for each prefix. Route existence/retirement is adapter binding
state and is deliberately not tested or imported in `application-core`.
- [ ] **Step 3: Verify RED**
- [x] **Step 3: Verify RED**
Run:
@@ -265,7 +286,7 @@ cd src
Expected: compilation failure because the new identity types do not exist.
- [ ] **Step 4: Implement the minimum values**
- [x] **Step 4: Implement the minimum values**
Use immutable final classes or records with constructor validation. `ObjectOperationKey` contains
exactly destination, epoch, and operation ID. Public reference/handle types expose canonical text
@@ -274,7 +295,7 @@ validation in framework-free Java (`MessageDigest`). The adapter-owned codec in
minting path from a retained route token plus generated `ObjectId`; application values do not
consult a route registry.
- [ ] **Step 5: Verify GREEN**
- [x] **Step 5: Verify GREEN**
Run the command from Step 3 and
`./gradlew :application-core:verifyDependencyLocks --console=plain`. Expected: PASS.
@@ -299,7 +320,7 @@ Run the command from Step 3 and
- Test:
`src/application-core/src/test/java/dev/caskeleton/application/objectstorage/ObjectStorageValueContractTest.java`
- [ ] **Step 1: Write failing callback and value tests**
- [x] **Step 1: Write failing callback and value tests**
Prove:
@@ -313,7 +334,7 @@ Prove:
- range offset/length arithmetic rejects zero, negative, overflow, and over-budget delivery;
- media types are canonical, bounded, and contain no control characters.
- [ ] **Step 2: Verify RED**
- [x] **Step 2: Verify RED**
Run:
@@ -326,14 +347,14 @@ cd src
Expected: compilation failure because the callback/value types do not exist.
- [ ] **Step 3: Implement the minimum contract**
- [x] **Step 3: Implement the minimum contract**
Reuse `CallBudget`; do not create serializable wall-clock deadlines. Callback types may declare only
application exceptions and Java primitive/array types. Do not expose `InputStream`, `OutputStream`,
`ByteBuffer`, Reactor, Flow, servlet, Spring, or AWS types. Document that callbacks are synchronous
and blocking and that adapters must not invoke application callbacks on SDK event-loop threads.
- [ ] **Step 4: Verify GREEN**
- [x] **Step 4: Verify GREEN**
Run the command from Step 2. Expected: PASS.
@@ -376,7 +397,7 @@ Run the command from Step 2. Expected: PASS.
- Test:
`src/application-core/src/test/java/dev/caskeleton/application/objectstorage/ObjectStoragePortContractTest.java`
- [ ] **Step 1: Write the failing port-shape contract**
- [x] **Step 1: Write the failing port-shape contract**
Use reflection and construction tests to prove:
@@ -393,7 +414,7 @@ Use reflection and construction tests to prove:
- receipts expose no locator, ETag, upload ID, URL, credential, or provider enum;
- requirements can strengthen but never lower destination policy.
- [ ] **Step 2: Verify RED**
- [x] **Step 2: Verify RED**
Run:
@@ -405,7 +426,7 @@ cd src
Expected: compilation failure because the semantic port family does not exist.
- [ ] **Step 3: Implement the minimum framework-free API**
- [x] **Step 3: Implement the minimum framework-free API**
Follow design §9 exactly. Keep one public top-level type per file. Use immutable collections and
defensive copies where required. Grants contain a bounded URI, signed header names/values,
@@ -414,7 +435,7 @@ expiration, and opaque session identity, but their `toString` must redact the UR
Document the separate scanner-workflow composition boundary on `ObjectScanMaintenancePort`; the
normal staged port owns only stage, integrity verification, finalize, and abort.
- [ ] **Step 4: Verify GREEN**
- [x] **Step 4: Verify GREEN**
Run the command from Step 2. Expected: PASS.
@@ -431,7 +452,7 @@ Run the command from Step 2. Expected: PASS.
- Modify:
`src/app-bootstrap/src/test/java/dev/caskeleton/bootstrap/architecture/CleanArchitectureTest.java`
- [ ] **Step 1: Write failing purity tests**
- [x] **Step 1: Write failing purity tests**
Assert recursively through fields, methods, constructors, record components, generic arguments, and
annotations that `dev.caskeleton.application.objectstorage..` has no Spring, AWS SDK, servlet,
@@ -442,7 +463,7 @@ boundary. Add an ArchUnit rule that new sample business code may not import
into an explicitly named legacy-only package, and permit the later exact
`application.storage.migration` adoption use case as an admin-only compatibility exception.
- [ ] **Step 2: Verify RED**
- [x] **Step 2: Verify RED**
Run:
@@ -458,7 +479,7 @@ Expected: the new sample-import rule initially identifies the current legacy con
fixture explicitly records it as the single frozen violation. No unrelated architecture violation
may be accepted.
- [ ] **Step 3: Deprecate without adapting new calls back to raw keys**
- [x] **Step 3: Deprecate without adapting new calls back to raw keys**
Mark both legacy types `@Deprecated(forRemoval = true)` and document:
@@ -473,13 +494,13 @@ Task 23 must move the remaining compatibility surface into an allowlisted `..pos
slice; no non-legacy sample package may import the old port after that cutover. Do not create a
semantic-to-legacy adapter that throws away operation identity or guarantees.
- [ ] **Step 4: Verify GREEN**
- [x] **Step 4: Verify GREEN**
Run the command from Step 2. Expected: PASS with exactly the named frozen legacy violation and zero
provider/framework leaks. The later migration exception must be bounded/redacted and visible only
to the named administrative use case.
- [ ] **Step 5: Run Batch A checkpoint**
- [x] **Step 5: Run Batch A checkpoint**
Run:
@@ -523,7 +544,7 @@ object-storage provider.
- Test:
`src/adapter/outbound/objectstorage/src/test/java/dev/caskeleton/adapter/outbound/objectstorage/kernel/ObjectOperationEpochTest.java`
- [ ] **Step 1: Add the approved test-only property engine**
- [x] **Step 1: Add the approved test-only property engine**
Add `testImplementation 'net.jqwik:jqwik:1.9.1'` and update only this leaf's lockfile:
@@ -535,7 +556,7 @@ cd src
Expected: PASS with no production jqwik dependency.
- [ ] **Step 2: Write failing codec and epoch tests**
- [x] **Step 2: Write failing codec and epoch tests**
Use golden vectors and property tests for:
@@ -555,7 +576,7 @@ Use golden vectors and property tests for:
- epoch `WARM -> ACTIVE -> DRAINING -> SEALED -> COMPACTED` transitions, no token reuse, and
`OPERATION_EXPIRED` after seal/compaction.
- [ ] **Step 3: Verify RED**
- [x] **Step 3: Verify RED**
Run:
@@ -569,14 +590,14 @@ cd src
Expected: compilation failure because the codec/kernel types do not exist.
- [ ] **Step 4: Implement deterministic codecs**
- [x] **Step 4: Implement deterministic codecs**
Keep all physical key construction in these codecs. Route lookup uses retained binding revision,
never a current-provider default. The fingerprint includes exact content identity for R2 and an
explicit `R1_UNVERIFIED_CONTENT` marker for compatibility; it never hashes content by materializing
the object. Operation epoch records include finite replay/retention/compaction bounds.
- [ ] **Step 5: Verify GREEN**
- [x] **Step 5: Verify GREEN**
Run the command from Step 3 and
`./gradlew :adapter:outbound:objectstorage:verifyDependencyLocks --console=plain`. Expected: PASS.
@@ -610,7 +631,7 @@ Run the command from Step 3 and
- Test:
`src/adapter/outbound/objectstorage/src/test/java/dev/caskeleton/adapter/outbound/objectstorage/kernel/ObjectOperationKernelTest.java`
- [ ] **Step 1: Write failing strict-codec tests**
- [x] **Step 1: Write failing strict-codec tests**
For every record family introduced through Task 7 prove:
@@ -629,7 +650,7 @@ size/digest/media type, encryption/retention evidence, and immutable data versio
small reference pointer CASes only the current manifest revision. No mutable pointer is treated as
the manifest itself.
- [ ] **Step 2: Write failing transition-table tests**
- [x] **Step 2: Write failing transition-table tests**
Cover every allowed and forbidden transition from design §12, including:
@@ -641,7 +662,7 @@ Cover every allowed and forbidden transition from design §12, including:
- stale fence/revision rejection;
- unknown/newer state never auto-deleted or downgraded.
- [ ] **Step 3: Verify RED**
- [x] **Step 3: Verify RED**
Run:
@@ -655,7 +676,7 @@ cd src
Expected: compilation failure because the control-plane and state-machine types do not exist.
- [ ] **Step 4: Implement the minimum kernel**
- [x] **Step 4: Implement the minimum kernel**
The kernel accepts a compiled policy snapshot and an `ObjectControlStore`; it does not import a
provider SDK. Reserve writes the frozen snapshot before provider mutation. Every mutation writes a
@@ -665,7 +686,7 @@ discriminator; no reflective or `Map<String,Object>` binding and no undecided JS
allowed. A later task that adds a durable family must modify this codec, add checked-in golden
bytes, and prove old/new reader compatibility before the selected write version changes.
- [ ] **Step 5: Verify GREEN**
- [x] **Step 5: Verify GREEN**
Run the command from Step 3. Expected: PASS.
@@ -689,7 +710,7 @@ Run the command from Step 3. Expected: PASS.
- Test:
`src/adapter/outbound/objectstorage/src/test/java/dev/caskeleton/adapter/outbound/objectstorage/filesystem/LocalDevObjectStorageRecoveryTest.java`
- [ ] **Step 1: Write the reusable provider contract**
- [x] **Step 1: Write the reusable provider contract**
The abstract suite must be executable for each provider and cover:
@@ -707,7 +728,7 @@ The abstract suite must be executable for each provider and cover:
Unsupported optional capabilities must assert descriptor `UNSUPPORTED`, not skip.
- [ ] **Step 2: Write failing local security/recovery tests**
- [x] **Step 2: Write failing local security/recovery tests**
Use `@TempDir` and injected filesystem/fault collaborators to test:
@@ -720,7 +741,7 @@ Use `@TempDir` and injected filesystem/fault collaborators to test:
- corrupt/newer records quarantined, never absent/deleted;
- file descriptor and temporary-file cleanup.
- [ ] **Step 3: Verify RED**
- [x] **Step 3: Verify RED**
Run:
@@ -733,7 +754,7 @@ cd src
Expected: compilation failure because the local provider/kernel integration does not exist.
- [ ] **Step 4: Implement bounded local R1**
- [x] **Step 4: Implement bounded local R1**
Use adapter-generated immutable data names, `CREATE_NEW`, bounded chunks, streaming SHA-256, staged
temporary files, force/atomic move only where the host proves it, and strict relative path checks.
@@ -741,7 +762,7 @@ The local control store serializes per operation in one process. When portabilit
durability cannot be proven, return the truthful R1 descriptor; never claim multi-node CAS or
power-loss durability. This provider is rejected in production profiles.
- [ ] **Step 5: Verify GREEN**
- [x] **Step 5: Verify GREEN**
Run the command from Step 3. Expected: PASS.
@@ -770,7 +791,7 @@ Run the command from Step 3. Expected: PASS.
- Modify:
`src/sample-portfolio/src/main/resources/application.yml`
- [ ] **Step 1: Write failing binding tests**
- [x] **Step 1: Write failing binding tests**
Bind `app.object-storage` and reject:
@@ -788,7 +809,7 @@ Bind `app.object-storage` and reject:
Prove a valid local profile compiles one exact route and immutable policy snapshot.
- [ ] **Step 2: Write failing composition tests**
- [x] **Step 2: Write failing composition tests**
Use `ApplicationContextRunner` to prove:
@@ -805,7 +826,7 @@ Use `ApplicationContextRunner` to prove:
- a namespace-separated canonical `legacy` subgroup plus the new capability may run together only
in the explicit migration/sample-local profile.
- [ ] **Step 3: Verify RED**
- [x] **Step 3: Verify RED**
Run:
@@ -819,7 +840,7 @@ cd src
Expected: compilation/test failure because canonical settings/composition do not exist and current
legacy configuration activates filesystem by default.
- [ ] **Step 4: Implement exact binding and activation**
- [x] **Step 4: Implement exact binding and activation**
Use immutable constructor-bound settings and typed `Duration`/`DataSize`/enums. Compile settings
before constructing any provider. Contributions are side-effect-free descriptors; the assembler
@@ -838,11 +859,11 @@ Until Task 22 migrates the consumer, the sample-local YAML explicitly enables th
filesystem seam and may also select `filesystem-local-dev` at `./.data/object-storage-v1` for the
new capability. No production YAML receives a local fallback.
- [ ] **Step 5: Verify GREEN**
- [x] **Step 5: Verify GREEN**
Run the command from Step 3. Expected: PASS.
- [ ] **Step 6: Prove the stable contribution seam**
- [x] **Step 6: Prove the stable contribution seam**
Add an `ApplicationContextRunner` matrix with a counting fake contribution: disabled, unselected,
invalid binding, selected success, selected construction failure, and close. Prove construction
@@ -874,7 +895,7 @@ unverified post-GREEN addition.
- Modify:
`docs/superpowers/specs/2026-07-28-objectstorage-production-capability-design.md`
- [ ] **Step 1: Write the failing registry/schema test**
- [x] **Step 1: Write the failing registry/schema test**
Validate the frozen schema and exact nine card IDs. Reject:
@@ -886,7 +907,7 @@ Validate the frozen schema and exact nine card IDs. Reject:
- local-dev above R1;
- a limitation-free row when a provider descriptor reports limitations.
- [ ] **Step 2: Verify RED**
- [x] **Step 2: Verify RED**
Run:
@@ -898,13 +919,13 @@ cd src
Expected: failure because the registry and runtime card types do not exist.
- [ ] **Step 3: Add only evidenced claims**
- [x] **Step 3: Add only evidenced claims**
Initial entries may claim R0 for contract-only cards and R1 for local managed single upload/download
only after Tasks 69 pass. Direct, multipart, quarantine, retention, and production reconciliation
remain R0/unimplemented. Document that local process recovery is not multi-node CAS or R2.
- [ ] **Step 4: Run Batch B checkpoint**
- [x] **Step 4: Run Batch B checkpoint**
Run:
@@ -919,7 +940,7 @@ cd src
Expected: PASS.
- [ ] **Step 5: Verify rollback**
- [x] **Step 5: Verify rollback**
Start a context with the canonical capability disabled and legacy disabled. Assert there are no
storage beans or side effects. Start the explicit legacy profile and prove old data remains
@@ -945,7 +966,7 @@ readable. No migration or deletion occurs at startup.
- Test:
`src/adapter/outbound/objectstorage/src/test/java/dev/caskeleton/adapter/outbound/objectstorage/s3/S3AsyncClientFactoryTest.java`
- [ ] **Step 1: Write the failing pinned-API characterization**
- [x] **Step 1: Write the failing pinned-API characterization**
Compile and assert the exact `2.30.0` API used by later tasks:
@@ -960,7 +981,7 @@ Add boundary tests at `Integer.MAX_VALUE`, `Integer.MAX_VALUE + 1L`, part count
minimum S3 non-final part size. Record the full-object multipart checksum profile as unsupported
above the SDK integer boundary unless an independently approved path exists.
- [ ] **Step 2: Write failing client-policy tests**
- [x] **Step 2: Write failing client-policy tests**
Reject missing/non-positive/contradictory:
@@ -974,7 +995,7 @@ Reject missing/non-positive/contradictory:
Assert the factory uses `S3AsyncClient` plus explicit `NettyNioAsyncHttpClient`, not CRT or the sync
client, and owns close order.
- [ ] **Step 3: Verify RED**
- [x] **Step 3: Verify RED**
Run:
@@ -988,14 +1009,14 @@ cd src
Expected: compilation failure because the client factory and compile-scoped Netty async client do
not exist.
- [ ] **Step 4: Add only the required dependency and implementation**
- [x] **Step 4: Add only the required dependency and implementation**
Add `software.amazon.awssdk:netty-nio-client` under the existing AWS BOM. Keep
`software.amazon.awssdk:s3` and the BOM at `2.30.0`; do not add CRT or Transfer Manager. Configure
finite client and HTTP timeouts/pools/retry from the compiled policy. Default-chain credentials are
resolved only after the provider is selected.
- [ ] **Step 5: Regenerate and verify locks**
- [x] **Step 5: Regenerate and verify locks**
Run:
@@ -1752,6 +1773,13 @@ Approval Gate A.
## Approval Gate A — Scanner ownership and sample public contract
**Approved 2026-07-29:** The user authorized continued implementation using the recommended
defaults. The deep design now records the exact decisions: test-fake scanner only,
`integrity-verified-reference`, additive POST/GET with stable `202`, Base64 SHA-256 multipart
field, required keyed idempotency with atomic TX1 reservation/intent, existing `poster:write`,
legacy compatibility profile, new V2 event without v1 removal, intentionally scoped OpenAPI
snapshot, and additive forward-only V8 dual read.
Do not execute Tasks 2024 until the approved design records all of the following:
- staged scan fencing may be implemented with a test fake, but no production scanner/provider is
@@ -1759,7 +1787,7 @@ Do not execute Tasks 2024 until the approved design records all of the follow
- the sample's first migrated profile is either `integrity-verified-reference` or an explicitly
named scan-gated profile with a real scanner owner;
- the additive endpoint proposal
`POST /posters/{id}/image-publications`, status, authorization, request size/media policy, and
`POST /posters/{id}/imagePublications`, status, authorization, request size/media policy, and
locator-free response fields;
- the asynchronous response contract: POST always returns the same bounded `202` reservation
(opaque publication operation plus status link) replayed by `IdempotencyExecutor`; a separately
@@ -1783,8 +1811,8 @@ Do not execute Tasks 2024 until the approved design records all of the follow
window, consumer migration evidence, rollback, and zero-consumer proof before v1 removal. Never
rename `imageKey` to `reference` under the same unversioned event contract;
- intentional OpenAPI snapshot approval;
- the additive V7 schema and forward-only rollback window.
- the compatibility model during V7: existing legacy attachments remain readable/publishable,
- the additive V8 schema and forward-only rollback window.
- the compatibility model during V8: existing legacy attachments remain readable/publishable,
legacy writes are restricted to the compatibility controller/profile, and the canonical profile
writes only READY opaque references.
@@ -1902,7 +1930,7 @@ scanner, `object-storage-quarantine-publication` remains below R2.
- Modify:
`src/sample-portfolio/gradle.lockfile`
- Create:
`src/sample-portfolio/src/main/resources/db/sample-migration/V7__poster_image_publication.sql`
`src/sample-portfolio/src/main/resources/db/sample-migration/V8__poster_image_publication.sql`
- Create under
`src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/application/posterimage/`:
`PosterImageUploadIntent.java`, `PosterImageUploadIntentState.java`,
@@ -1954,7 +1982,7 @@ scanner, `object-storage-quarantine-publication` remains below R2.
- Test:
`src/sample-portfolio/src/test/java/dev/caskeleton/sample/portfolio/bootstrap/objectstorage/PosterImageIdempotencyConfigTest.java`
- Create:
`src/sample-portfolio/src/posterImageMigrationTest/java/dev/caskeleton/sample/portfolio/qualification/PosterImageV7MigrationQualificationTest.java`
`src/sample-portfolio/src/posterImageMigrationTest/java/dev/caskeleton/sample/portfolio/qualification/PosterImageV8MigrationQualificationTest.java`
- Create:
`src/sample-portfolio/src/posterImageMigrationTest/java/dev/caskeleton/sample/portfolio/qualification/PosterImageIdempotencyRotationQualificationTest.java`
- Modify:
@@ -1986,7 +2014,7 @@ Expected: the task/configuration exists and locks are valid; no migration behavi
Run real PostgreSQL/Flyway and prove:
- fresh V1V7 and V6→V7 upgrade;
- fresh V1V8 and V7→V8 upgrade;
- V6 is unchanged;
- existing `poster.image_key` rows remain readable through the internal dual-read mapper;
- new READY attachments store only bounded opaque `image_reference`;
@@ -2004,7 +2032,7 @@ Run real PostgreSQL/Flyway and prove:
- replacement/delete work uses a separate durable `poster_image_retirement_intent` row and cannot
be inferred from a deleted Poster or overloaded upload intent.
The V7 intent table must include stable operation identity/epoch/destination, request fingerprint,
The V8 intent table must include stable operation identity/epoch/destination, request fingerprint,
bounded idempotency-scope digest, expected poster version, exact content identity/media type, state,
stage handle, handoff claim/fence/expiry, published reference/version, replaced reference,
retry/error timestamps, and optimistic revision. Index the unique scope, operation, worker claims,
@@ -2047,7 +2075,7 @@ cd src
./gradlew :sample-portfolio:posterImageMigrationTest --console=plain
```
Expected: compilation/migration failures because V7, the intent model, key-ring binding, and
Expected: compilation/migration failures because V8, the intent model, key-ring binding, and
non-skipping qualification lane do not exist. Docker/PostgreSQL absence is an actionable failure,
not a skip or passing default test.
@@ -2084,7 +2112,7 @@ Expected: PASS, and the `posterImageMigrationTest` result XML reports zero skipp
- [ ] **Step 6: Verify rollback window**
The non-skipping `PosterImageV7MigrationQualificationTest` must run a V6-compatible SQL/JPA
The non-skipping `PosterImageV8MigrationQualificationTest` must run a V7-compatible SQL/JPA
projection against the expanded schema, including old INSERT/UPDATE/read behavior. Document that
rollback is binary-only while old code ignores additive columns; there is no down migration,
column drop, intent deletion, or object mutation.
@@ -2313,7 +2341,7 @@ idempotency package. Preserve the existing application port contract and prove w
expired-find→reclaim→complete with new fingerprint/expiry/COMPLETED state, and unrelated managed
entity preservation in non-skipping real PostgreSQL. Do not emulate with a process lock. Update the
owner README/CLAUDE vendor SPI table. The same qualification queries both `idempotency_record` and
V7 intent tables and proves neither contains the raw Idempotency-Key, tenant, or principal; stored
V8 intent tables and proves neither contains the raw Idempotency-Key, tenant, or principal; stored
aliases/digests retain tenant separation.
Reconciliation settings are constructor-bound, disabled by default, and bound batch size,
@@ -2671,7 +2699,7 @@ migration-port/verifier/replay-store/runner beans and zero side effects. The clo
registers approval replay records with golden/new-reader/old-reader compatibility fixtures.
Retirement settings are constructor-bound and disabled by default, with bounded claim/renew,
batch/page, delay, retry/backoff, concurrency, and shutdown grace. The job claims the dedicated V7
batch/page, delay, retry/backoff, concurrency, and shutdown grace. The job claims the dedicated V8
retirement table and calls only logical exact-reference retirement outside DB transactions. A
disabled context creates no scheduler/thread; stale fence/takeover is rejected and response loss
remains operation-keyed exact-reference/version reconciliation, not blind retry. TX3 replacement and
@@ -3419,7 +3447,7 @@ gates.
Before the implementation completion response, read the canonical vault instructions and update:
```text
/home/donghyeon/workspace/ai-tool/llm-wiki-private/raw/branch-notes/<branch-name>.md
/home/donghyeon/workspace/ai-tools/llm-wiki/raw/branch-notes/<branch-name>.md
```
Record files, decisions, test commands/results, failures/blocks, evidence grade, and derivative
@@ -1,13 +1,15 @@
# Object Storage Production Capability Deep Design
- 작성일: 2026-07-28
- 상태: 상세 설계 및 구현 계획 완료, 구현 미착수, R2 미구현
- 상태: Approval Gate A 승인 반영, Batch AD 및 Batch E pause checkpoint를 main에 통합, R2 미구현
- 독립 재리뷰: 완료 — 아키텍처 및 문서 실행성 blocker 0, high 0
- LLM Wiki capture: 정본 vault
`/home/donghyeon/workspace/ai-tool/llm-wiki-private/` 부재로 차단; 비정본 clone 대체 사용 안 함
- LLM Wiki capture: public vault
`raw/branch-notes/chore-main-worktree-capability-integration.md`에 main 통합 기록 완료
- 기준: Java 21, Spring Boot 4.0.0, Gradle 멀티모듈 Clean Architecture
- 대상 leaf: `adapter-outbound-objectstorage`
- 현재 구현 수준: whole-object `byte[]` 기반 local filesystem/S3-MinIO 예제, R0~R1 일부
- 현재 구현 수준: provider-neutral kernel + `filesystem-local-dev` managed single R1,
S3 managed/direct primitives, exact MinIO mutation profiles `UNSUPPORTED`,
legacy whole-object filesystem/S3-MinIO 격리
- 상위 문서:
[Production Capability Platform Design](2026-07-26-production-capability-platform-design.md)
- 구현 계획:
@@ -19,39 +21,100 @@
## 0. 구현 상태
2026-07-28 기준 구현된 범위는 다음뿐이다.
### Approval Gate A 결정 — 2026-07-29
사용자의 구현 계속 승인에 따라 Batch E의 public/sample 계약을 다음과 같이 고정한다.
- production scanner는 주장하지 않는다. 스캔 경계는 test fake만 제공하고 첫 sample
destination profile은 `integrity-verified-reference`로 한다.
- canonical surface는 additive
`POST /posters/{id}/imagePublications`와 별도 authorized GET status resource다. 저장소의
AIP-122 경로 규칙에 따라 collection segment에는 kebab-case를 사용하지 않는다.
POST는 항상 동일한 bounded `202` reservation을 반환하며 READY 결과를 같은 idempotency
response에 섞지 않는다.
- 정확한 full-file SHA-256은 canonical Base64 형식의 bounded multipart field로 받고 TX1 전에
semantic fingerprint에 포함한 뒤 staging 중 다시 검증한다.
- `Idempotency-Key`는 필수다. 첫 committed UploadIntent가 stable operation ID를 할당하며
generic idempotency reservation과 intent는 같은 TX1에서 원자적으로 commit한다.
- sample authorization은 기존 `poster:write` permission을 유지한다. 존재하지 않는 Poster
owner 속성을 발명하지 않는다.
- legacy `/posters/{id}/image`는 명시적 compatibility profile에서만 유지한다. canonical과
legacy controller는 동시에 활성화하지 않는다.
- 기존 `poster.image-attached`/`imageKey` 이벤트는 변경하거나 제거하지 않는다. canonical
흐름은 새 versioned V2 event를 사용하며 외부 zero-consumer를 주장하지 않는다.
- OpenAPI 승인은 새 publication/status surface에만 한정한다. 기존 DTO의 locator-shaped
필드는 compatibility window 동안 그대로 둔다.
- 통합 타임라인의 V8은 additive, forward-only다. V7 Poster schema의 `image_key`와 legacy
reader를 유지하고 canonical writer는
READY opaque reference만 기록한다. rollback은 구 binary가 additive columns를 무시하는
binary-only window이며 down migration이나 자동 object mutation은 없다.
2026-07-28 Batch D 체크포인트 기준 다음 범위가 구현되었다.
- `application-core`의 framework/provider-neutral object-storage identity, opaque reference,
bounded content callback, digest/range, request/receipt/outcome, capability requirement;
- managed, staged, direct single/multipart, scan-maintenance, purge-maintenance를 분리한 narrow port;
- 모든 mutation의 `ObjectOperationKey`, strict reference grammar/checksum, transient grant redaction;
- application contract의 framework/provider/transport/persistent-locator purity 검사;
- legacy port/receipt의 removal-boundary deprecation과 sample legacy import freeze;
- deterministic `data/v1`/`control/v1` namespace, reference/handle/fingerprint/policy codec,
operation epoch와 immutable route/policy snapshot;
- strict canonical JSON control envelope와 operation/manifest/reference/multipart record,
reserve/replay/conflict/pending-effect state kernel;
- bounded immutable local create, exact inspect/version, full/range transfer, conditional retire,
create resolution, symlink/path confinement, restrictive permissions와 single-process CAS;
- constructor-bound `app.object-storage` settings, compile-before-construction provider
contribution/assembler, destination/retained-route routers, disabled/unselected/invalid/close
side-effect matrix;
- explicit canonical legacy subgroup와 old alias isolation, sample local/test explicit activation;
- exact nine-card readiness registry. `filesystem-local-dev` managed single upload/download만 R1,
나머지는 R0;
- pinned AWS SDK `2.30.0` conditional/checksum/expected-owner API characterization과 explicit
`NettyNioAsyncHttpClient` policy/factory/lifecycle;
- exact AWS/MinIO binding, bounded async transport, managed S3 single/multipart/control primitives,
response-loss resolution, selected-only provider composition;
- durable direct single/multipart session and grant records, S3 presigner lifecycle, exact part
acknowledgement, completion/abort fences, terminal exact-version replay;
- digest-pinned MinIO contract/fault lanes and protected AWS compile-only lane. Exact MinIO는
create-only PUT/complete 조건을 무시하므로 managed/direct mutation profile을
`UNSUPPORTED`로 유지하며 readiness를 올리지 않는다.
증거는 [Batch A checkpoint](../../evidence/object-storage/batch-a-checkpoint.md),
[Batch B checkpoint](../../evidence/object-storage/batch-b-checkpoint.md),
[Batch C checkpoint](../../evidence/object-storage/batch-c-progress.md),
[Batch D checkpoint](../../evidence/object-storage/batch-d-checkpoint.md),
[Batch E pause checkpoint](../../evidence/object-storage/batch-e-pause-checkpoint.md)에 기록했다.
격리 브랜치에서는 private vault 경로를 잘못 선택해 capture가 차단됐지만, main 통합 작업은
사용자가 지정한 public vault의
`raw/branch-notes/chore-main-worktree-capability-integration.md`에 캡처했다.
legacy runtime 범위는 별도로 유지된다.
- `application-core`의 범용 `ObjectStoragePort`;
- caller가 지정한 raw key에 `byte[]`를 put/get/delete/exists하는 계약;
- local filesystem adapter;
- synchronous AWS SDK v2 `S3Client`를 사용하는 S3/MinIO adapter;
- 설정 한 개로 filesystem 또는 S3 backend를 선택하는 Spring composition;
- 명시적 legacy-only filesystem 또는 S3 Spring composition;
- filesystem unit test, mocked S3 unit test, Testcontainers MinIO integration test;
- module registry가 허용한 `application-core`, `shared-contract` 의존성.
아직 구현되지 않은 범위:
아직 구현되지 않은 runtime/provider 범위:
- streaming upload/download와 range read;
- immutable object reference와 version token;
- stable operation ID, request fingerprint, durable operation record;
- conditional create/update/delete와 unknown-outcome reconciliation;
- checksum 생성·전송·검증 계약;
- metadata/head 계약;
- staged upload, quarantine, scan, publication;
- presigned upload/download;
- multipart start/part/complete/abort/recovery;
- public presigned upload/download endpoint와 inbound authorization;
- R2 수준의 exhaustive multipart/direct response-loss·race·restart qualification;
- lifecycle, versioning, retention, legal hold 검증;
- production credential, encryption, TLS, expected-owner 정책;
- bounded timeout, connection pool, retry amplification, graceful shutdown;
- provider capability qualification과 exact readiness card;
- multi-node linearizable control CAS와 production unknown-outcome reconciliation;
- production credential, encryption, TLS, retention/Object Lock qualification;
- orphan/abandoned multipart/retired object reaper;
- database와 object storage 사이의 crash-safe workflow;
- 운영 metric, trace, audit, runbook;
- AWS sandbox fault/security evidence.
따라서 현재 MinIO round-trip test가 통과하더라도 S3 production readiness를 의미하지 않는다.
이 문서의 상태가 “상세 설계 완료”인 것은 구현, R2 qualification 또는 운영 준비 완료를
뜻하지 않는다.
Batch B의 R1은 exact local functional topology 증거다. Multi-node CAS, power-loss durability,
S3/MinIO qualification, R2 또는 운영 준비 완료를 뜻하지 않는다.
## 1. 설계 판정
@@ -3955,8 +4018,10 @@ authorization endpoint의 ephemeral response이며 stored object DTO에 영구
정본 구현 순서와 아래 결정의 현재 freeze/approval gate는
[Object Storage Production Capability Implementation Plan](../plans/2026-07-28-objectstorage-production-capability.md)에
기록한다. 계획 작성은 완료됐지만 모든 구현 task는 아직 미착수이며, 계획 승인이 public API,
scanner provider 또는 AWS/IaC 외부 변경 권한을 자동으로 부여하지 않는다.
기록한다. Tasks 124의 구현은 Batch E pause checkpoint까지 진행됐지만 Batch E 전체
combined/root gate는 아직 통과하지 않았다. Tasks 25–30은 미착수다. 이 구현 진행이나
계획 승인이 public API, scanner provider 또는 AWS/IaC 외부 변경 권한을 자동으로 부여하지
않는다.
구현 계획은 다음 결정을 task 단위로 명시해야 한다.