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
@@ -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 단위로 명시해야 한다.