diff --git a/docs/superpowers/specs/2026-08-17-techlog-backend-alignment-design.md b/docs/superpowers/specs/2026-08-17-techlog-backend-alignment-design.md new file mode 100644 index 0000000..bb1c9ea --- /dev/null +++ b/docs/superpowers/specs/2026-08-17-techlog-backend-alignment-design.md @@ -0,0 +1,351 @@ +# TechLog Backend 정합 설계 + +## 상태 + +- 승인일: 2026-08-17 +- 기준선: `tech-log-frontend` `main` (UI 이식 완료 상태) +- 원본 요구: `/home/donghyeon/workspace/tech-log-alignment-design/01-tech-log-frontend-alignment-design.md` +- Canonical 계약: `/home/donghyeon/workspace/tech-log-design-package/contracts/openapi/studio-v1.yaml` + - Specification Version `2.0.0` + - 계약 digest `sha256:85a65004f29880334b9a0a3b54089450a898f1a815f679b2985b87ed8723df5b` + - 설계 패키지 revision `0ec5582` +- 결정: 현재 Public/Studio UI 기준선을 고정하고, Studio 계약·전송 경계와 Asset capability를 canonical 계약에 정합시킨다. Public 조회의 HTTP 전환은 이 사이클에서 제외한다. + +## 목적 + +현재 TechLog 프론트엔드는 Studio를 세션 수명 `MockStudioGateway`로, Public을 정적 동기 catalog로 구동한다. 이 설계는 다음을 달성한다. + +1. Studio HTTP 계약을 canonical `studio-v1.yaml` 단일 출처에서 생성하고, 두 계약이 다시 갈라지지 못하게 빌드로 막는다. +2. `StudioGateway`의 모든 operation을 canonical 계약 기준 HTTP 어댑터로 구현한다. +3. Backend에 이미 존재하는 Asset/Image/SVG capability를 프론트 편집 흐름에 연결한다. +4. 위 전부를 실행 중인 Backend 없이 완료하고, Backend가 완성되면 런타임 스위치만으로 대조할 수 있게 한다. + +현재 Public UI 라우트·화면 구성과 Studio의 `작업본 → 편집 → 저장 → 검증 → Public Preview → 게시/재게시 → 게시 기록/Snapshot` 흐름은 변경하지 않는다. + +## Source of Truth 우선순위 + +| 영역 | Source of Truth | +|---|---| +| Public 화면·라우트·사용 흐름 | 현재 `tech-log-frontend` | +| Studio 화면 구조·사용 흐름 | 현재 `tech-log-frontend` | +| Studio HTTP 계약 | `studio-v1.yaml` (단일 canonical) | +| Asset lifecycle·불변 조건 | `studio-v1.yaml` | +| 전송·오류·재시도·관측 규약 | 현재 프론트 플랫폼 (`src/contracts/external-contract-runtime.ts`) | + +UI는 Backend 도메인 구조를 그대로 노출하지 않는다. 프론트의 `WorkingCopy`는 Backend Aggregate가 아니라 **편집 계약**이다. + +## 확인된 사실과 정정 + +착수 전 검증에서 원본 요구 문서 및 설계 패키지 README와 실제 코드가 어긋나는 지점을 확인했다. 아래는 구현 기준으로 채택하는 정정이다. + +### 이미 충족된 항목 + +- 설계 패키지 README는 프론트 격차로 "`RecordKind`에 `PROJECT_DECISION` 없음"을 든다. **이미 충족돼 있다** — `contracts/studio/studio-api.openapi.yaml:236`, `contracts/studio/generated.ts:200`. README가 지칭하는 대상은 구 `techlog-studio-frontend` 저장소다. +- `application/ports/studio-gateway.ts`의 operation 집합·필터·`IdempotentOptions`는 원본 요구 §4와 이미 일치한다. 포트 재설계가 아니라 구현체 교체가 필요하다. + +### 실재하는 격차 + +- 프론트 계약에 `X-CSRF-TOKEN` 선언이 없다 (canonical 6회 참조, 프론트 0회). canonical은 모든 mutating operation에 CSRF를 요구한다. +- 프론트 계약의 `Idempotency-Key` 선언이 1회로, canonical(2회, 공용 parameter)과 구조가 다르다. +- canonical에는 프론트 계약에 없는 operation이 6개 있다: `getStudioSession`, `listStudioAssets`, `uploadStudioAsset`, `getStudioAsset`, `updateStudioAsset`, `deleteStudioAsset`. canonical 총 operation은 **19개**(프론트 현재 13개). +- canonical 오류 코드는 **23개**로, 원본 요구 §10의 21개에 `IDEMPOTENCY_KEY_REUSED`와 `WARNING_ACKNOWLEDGEMENT_REQUIRED`가 추가된다. 두 코드 모두 필요하다 — 전자는 §10이 요구하는 "version conflict와 idempotency replay 구분"의 실제 코드이고, 후자는 이미 존재하는 `warning-acknowledgements.tsx`가 처리해야 하는 거절 사유다. **23개 전부를 채택한다.** +- 원본 요구는 세션/CSRF를 다루지 않는다. canonical은 CSRF 토큰을 `getStudioSession`이 발급한다고 정한다. 아래 §"세션과 CSRF"에서 경계를 정한다. + +### 플랫폼 제약 + +- `ContractContributionSource`의 `TEMPLATE_FIXTURE`는 타입상 `fixtureId: "REFERENCE_FEATURE_V1"`로 닫혀 있다(`external-contract-runtime.ts:186-190`). TechLog는 `EXTERNAL_PACKAGE`만 사용할 수 있다. +- 플랫폼 계약 런타임은 `requestBody: "NONE" | "JSON"`만 허용하고, 그 외를 구성 시점에 거절한다(`external-contract-runtime.ts:145`, `:407`). 저수준 `client.ts`도 본문을 `JSON.stringify`로 고정한다(`client.ts:719`). **`multipart/form-data`를 표현할 수 없다.** +- 기존 `browser-transfer` capability는 presigned `GET`/`PUT`과 resumable part 업로드용이다(`PresignedTransferMethod = "GET" | "PUT"`). canonical의 `POST /assets` multipart MVP 계약에 그대로 맞지 않는다. +- 설계 패키지의 `MANIFEST.sha256`은 stale하다(yaml은 2026-08-17 갱신, manifest는 08-11 기준이며 `preview-v1.yaml`을 아직 나열한다). 계약 고정은 manifest가 아니라 위 §상태에 기록한 실측 digest를 기준으로 한다. + +## 범위 + +### 포함 + +| | 범위 | +|---|---| +| **A. 계약 정합** | canonical `studio-v1.yaml` 도입, 타입 생성 자동화, digest 고정, drift 게이트 | +| **B. Studio 전송** | 계약 기여(JSON 18개 전체) + `StudioGateway` HTTP 어댑터(14개 소비) + 세션/CSRF + 오류 매핑 + 런타임 스위치 | +| **C. Asset** | `StudioAssetGateway` 포트, 5개 operation(JSON 4 + multipart 1), Asset Library/Picker/Upload UI, `/studio/assets` 라우트, evidence directive 연결, alt/decorative 규칙 정정 | + +### 제외 — Public 조회의 HTTP 전환 + +원본 요구 §11은 `adapters/static/public-query.ts`를 HTTP로 교체하도록 나열한다. 이 사이클에서 제외하고 별도 사이클로 분리한다. + +근거: + +- `PublicContentQueries`는 전 메서드가 **동기**이고, 프레젠테이션 19개 파일 31개 호출 지점이 렌더 중 직접 호출한다. async 전환은 전 Public 화면에 loading/error/empty 상태를 도입하는 작업이다. +- 방금 완료한 UI 이식의 시각·접근성 parity 기준선을 광범위하게 흔든다. +- 실행 중인 Public Backend가 없어 지금 전환해도 검증할 대상이 없고 사용자 가치도 없다. +- canonical `public-v1.yaml`(1,765줄)은 준비돼 있으므로, Public API가 실제로 서비스될 때 자체 spec/plan 사이클로 수행한다. + +이 제외는 범위 축소가 아니라 순서 결정이다. 완료 조건에서 해당 항목을 별도로 명시한다. + +## 선택한 접근 + +### A. 계약 정합 — digest로 고정된 단일 출처 + +canonical yaml을 저장소에 vendor하고, 타입을 생성하고, **계약 기여의 `EXTERNAL_PACKAGE` provenance로 canonical revision에 암호학적으로 고정**한다. + +`InstalledContractPackageIdentity`(`external-contract-runtime.ts:173`)는 이미 이 목적에 맞는 필드를 요구한다. + +```text +packageId tech-log-studio-contract +version 2.0.0 (canonical info.version, exact SemVer) +digest sha256:85a65004… (canonical studio-v1.yaml의 SHA-256) +runtimeProtocolVersion 1 +sourceRevision 0ec5582 (설계 패키지 git revision) +``` + +`assertPackageIdentity`는 shape을 검증하므로 npm 레지스트리 없이 지금 사용할 수 있다. 실제 패키지 배포로 승격할 때 같은 필드를 그대로 채운다. + +drift 방지는 저장소 관례(`generate:*` / `check:*`)를 따르는 스크립트 한 쌍으로 강제한다. + +```text +generate:tech-log-contract canonical yaml → vendor 사본 + generated.ts + digest 기록 +check:tech-log-contract 재생성 결과가 커밋 내용과 바이트 동일한지, digest가 계약 기여의 + 선언과 일치하는지 검증. 불일치 시 실패. +``` + +`openapi-typescript`를 devDependency로 고정한다. 현재 `generated.ts`는 이 도구로 만들어졌으나 도구도 스크립트도 저장소에 없어 재생성이 불가능하다 — 이것이 두 계약이 갈라진 근본 원인이다. + +`X-CSRF-TOKEN`과 공용 `Idempotency-Key` parameter는 생성 결과에 자동 반영된다. 프론트 yaml을 손으로 고치지 않는다. + +### B. Studio 전송 — 플랫폼 계약 런타임 사용 + +계약 기여는 서비스 패키지당 하나이므로, `features/tech-log/contracts/tech-log-studio-contract-contribution.ts` 한 파일에 canonical의 **JSON operation 18개 전부**(19개 중 multipart 업로드 제외)를 선언한다. Asset의 JSON operation 4개도 같은 기여에 속한다 — 같은 `studio-v1` 패키지이기 때문이다. 이 중 `StudioGateway`가 14개를, `StudioAssetGateway`가 4개를 소비한다. + +선언 형식은 `reference-feature-contract-contribution.ts`가 확립한 것을 따른다: operation별 `inputValidator`/`outputValidator`/`problemValidator`, `acceptedStatuses`, `retrySemantics`, `commandRecovery`, `commandEffect`, `projectRequest`, 그리고 byte limit·deadline·retry budget·diagnostics 이름. + +`retrySemantics`는 canonical의 안전성 구분을 그대로 반영한다. 조회는 `SAFE`, mutating operation은 `KEYED`이며 `commandRecovery.mode = "IDEMPOTENCY_REPLAY"`, retry budget은 0이다 — 발신된 KEYED 명령의 자동 재시도는 금지된다. + +`StudioGateway` 구현체는 `contractOperations` executor 위에 얹고, 포트 시그니처는 변경하지 않는다. UI는 어댑터가 mock인지 HTTP인지 알지 못한다. + +#### 세션과 CSRF + +CSRF는 전송 관심사이며 UI 관심사가 아니다. 현재 Studio에는 인증 UI가 없고(이식 시 유보), 이 사이클에서도 추가하지 않는다. + +따라서 `getStudioSession`을 포트로 노출하지 않는다. HTTP 어댑터 내부가 첫 mutating 요청 전에 세션을 조회해 CSRF 토큰을 캐시하고, `csrfHeaderName`으로 헤더를 붙인다. `401`/`403`은 기존 오류 경로로 흘려보낸다. `displayName`/`roles`는 이 사이클에서 소비하지 않는다. + +이 결정으로 완료 조건 "현재 Studio 작업 흐름이 변경되지 않는다"가 유지된다. + +#### 런타임 스위치 + +`config/runtime/*.json`에 스위치를 추가한다. + +```text +TECH_LOG_STUDIO_SOURCE: "MOCK" | "HTTP" + +local, development → MOCK (기본값) +staging, production → HTTP +``` + +`createTechLogFeatureInstalledInput`이 이 값으로 gateway factory를 고른다. mock은 삭제하지 않고 test fixture 겸 fallback으로 유지한다. Backend 완성 시 `local.json` 한 줄로 대조를 시작한다. + +기본값을 `MOCK`으로 두는 이유는 현재 앱과 이식 parity 테스트가 그대로 통과해야 하기 때문이다. + +### C. Asset — 포트 분리와 업로드 전송 경계 + +`StudioAssetGateway`를 `StudioGateway`와 별도 포트로 둔다. 파일 전송과 JSON orchestration의 실패 모델이 다르고, 향후 presigned/resumable 교체가 이 포트 뒤에서 끝나야 한다. + +```text +StudioAssetGateway + listAssets(query, options) GET /api/v1/studio/assets + uploadAsset(form, options) POST /api/v1/studio/assets (multipart) + getAsset(assetId, options) GET /api/v1/studio/assets/{assetId} + updateAssetMetadata(assetId, cmd, o) PUT /api/v1/studio/assets/{assetId} + deleteAsset(assetId, options) DELETE /api/v1/studio/assets/{assetId} +``` + +#### 업로드 전송 + +플랫폼 계약 런타임이 multipart를 표현할 수 없으므로, 업로드 한 operation만 전용 전송 seam으로 분리한다. + +```text +StudioAssetUploadTransport (좁은 인터페이스: form + 헤더 → 결과) + └ fetch + FormData 구현체 + 런타임 config의 API_BASE_URL·타임아웃 재사용 + CSRF·Idempotency-Key 헤더는 B와 동일 경로로 획득 + 오류는 B와 동일한 코드 매핑 테이블 사용 +``` + +나머지 4개 JSON operation은 A/B와 같은 계약 런타임을 통과한다. 즉 계약 런타임을 우회하는 것은 **19개 중 1개**다. + +이 경계를 명시적 seam으로 두는 이유는 §7.4의 교체 가능성 요구를 만족시키기 위해서다. 플랫폼에 `MULTIPART` 모드가 생기거나 presigned로 옮길 때 구현체 한 파일만 바뀐다. 플랫폼 파일(`external-contract-runtime.ts`, `client.ts`)은 template 동기화 대상이므로 이 사이클에서 수정하지 않는다. + +결정 사유와 우회 범위는 `docs/reviews/adapters/`에 기록한다. + +#### UI 접근점 + +Studio primary navigation을 Asset 중심 CMS로 되돌리지 않는다. 두 접근점을 둔다. + +1. **Editor contextual Asset Picker** — 작성 흐름의 기본 진입점. 선택 시 evidence directive를 삽입한다. +2. **`/studio/assets` Asset Library** — 검색·메타데이터·사용처·정리용 보조 화면. navigation에는 secondary utility link로만 노출한다. + +`/studio/assets`는 `tech-log-route-contract.ts`에 `layoutGroup: "STUDIO"`로 추가한다(현재 27개 → 28개). 라우트 registry 거버넌스 기준선을 함께 갱신한다. + +#### Evidence Figure와 Asset 연결 + +현재 content format directive를 유지한다. + +```text +:::evidence key="asset-key" alt="설명" caption="캡션" zoom="true" +``` + +변경점: + +- 정적 `evidenceAssets` 레지스트리(`adapters/static/evidence-assets.ts`) 대신 Backend Asset의 `assetKey`를 사용한다. `assetKey`는 canonical에서 immutable이며 공개 이력 이후 재사용이 금지된 안정 key다. +- Asset Picker 선택 시 directive를 자동 삽입한다. 사용자가 raw object-storage URL을 Markdown에 직접 넣지 않게 한다. +- 렌더러는 API가 제공한 Asset descriptor를 resolver로 주입받는다. 렌더러 경계는 변경하지 않는다. +- `QUARANTINED` Asset은 Public/Preview에 렌더링하지 않는다. +- 사용자 업로드 SVG 원문을 `innerHTML`로 주입하지 않는다. 검증된 `publicPath`를 ``로 렌더링한다. + +정적 `evidenceAssets`는 기존 하드코딩 Case 화면의 parity 유지를 위해 fixture로 남기고, Asset 기반 경로와 공존시킨다. + +#### alt / decorative 규칙 정정 + +현재 parser는 빈 alt를 syntax error로 차단한다. + +```text +parse-case-content.ts:505 + if (!attributes.alt) invalid(node, "evidence alt text is required"); +``` + +Asset capability와 연결하면 이 위치에서는 판단할 수 없다 — 필요 여부가 Asset의 `decorative`에 달려 있다. 규칙을 옮긴다. + +```text +parser 빈 alt를 문법 오류로 차단하지 않는다 (구조만 검증) +publish 검증 Asset decorative=false + 사용 위치 alt 비어 있음 → ERROR + Asset decorative=true → alt="" 허용 +``` + +이는 content format의 **의미 변경**이므로 parser/serializer round-trip 픽스처와 기존 검증 테스트를 함께 갱신한다. + +## 대상 아키텍처 + +```text +src/features/tech-log/ +├── contracts/ +│ ├── studio/ +│ │ ├── studio-api.openapi.yaml canonical vendor 사본 (생성물, 수동 편집 금지) +│ │ ├── generated.ts 생성물 +│ │ ├── contract.ts 타입 alias (Asset 계열 추가) +│ │ └── canonical-source.json digest·revision 기록 +│ └── tech-log-studio-contract-contribution.ts JSON operation 18개 + EXTERNAL_PACKAGE provenance +├── application/ports/ +│ ├── studio-gateway.ts 변경 없음 +│ └── studio-asset-gateway.ts 신규 +├── adapters/ +│ ├── http/ +│ │ ├── http-studio-gateway.ts contractOperations 위 구현 +│ │ ├── http-studio-asset-gateway.ts JSON 4 + 업로드 위임 +│ │ ├── asset-upload-transport.ts multipart seam +│ │ ├── studio-session-csrf.ts CSRF 토큰 획득·캐시 +│ │ └── studio-error-mapping.ts 23 코드 → 플랫폼 FailureKind +│ ├── mock/ 유지 (fixture·fallback) +│ └── static/ 유지 (Public, 이 사이클 범위 외) +└── presentation/studio/ + ├── pages/assets-page.tsx 신규 + └── components/ + ├── asset-library.tsx 신규 + ├── asset-picker.tsx 신규 + └── asset-upload-dialog.tsx 신규 +``` + +기존 유지 대상: `presentation/public/**`, `presentation/studio/pages/**`, `document-*`, `validation-*`, `public-preview-screen`, `publish-screen`, `publication-*`, `presentation/shared/public-render/**`. + +## 오류·동시성 계약 + +23개 canonical 코드를 전부 처리한다. 매핑은 `studio-error-mapping.ts` 한 곳에 둔다. + +```text +AUTHENTICATION_REQUIRED STUDIO_ACCESS_DENIED +DOCUMENT_NOT_FOUND VERSION_CONFLICT +REQUEST_VALIDATION_FAILED VALIDATION_FAILED +VALIDATION_STALE PREVIEW_NOT_FOUND +PREVIEW_STALE PREVIEW_EXPIRED +PUBLICATION_NOT_FOUND PUBLICATION_CONFLICT +PUBLICATION_EVENT_NOT_FOUND PUBLICATION_SNAPSHOT_NOT_FOUND +IDEMPOTENCY_KEY_REUSED WARNING_ACKNOWLEDGEMENT_REQUIRED +ASSET_NOT_FOUND ASSET_NOT_READY +ASSET_IN_USE ASSET_QUARANTINED +PAYLOAD_TOO_LARGE UNSUPPORTED_MEDIA_TYPE +STUDIO_UNAVAILABLE +``` + +규칙: + +- 모든 mutation은 `Idempotency-Key`를 보낸다. 명령 하나당 새 key를 만들고, 그 명령의 안전한 재시도에만 같은 key를 재사용한다. +- `VERSION_CONFLICT`(낙관적 잠금 실패)와 `IDEMPOTENCY_KEY_REUSED`(같은 key·다른 요청)를 혼동하지 않는다. 전자는 사용자 데이터 손실 없는 충돌 화면으로, 후자는 클라이언트 결함으로 다룬다. +- `Idempotency-Replayed` 응답 헤더를 replay 판별에 사용한다. +- workflow 상태(`publicationStatus`, `hasUnpublishedChanges`, `nextAction`)는 서버가 계산한 값을 그대로 신뢰한다. 프론트에서 재계산하지 않는다. +- 업로드 상태는 `선택 실패 / 업로드 중 / 전송 실패 / READY / REJECTED / QUARANTINED / 크기 초과 / 미지원 형식`을 구분한다. 업로드 전송 성공과 서버 검증 성공을 분리한다. +- Asset은 `READY`일 때만 Preview/Publish에 사용한다. + +## 테스트 전략 + +TDD로 진행한다. 각 단위는 red → green → 게이트 순서를 지킨다. + +**계약(A)** + +- `check:tech-log-contract`가 vendor 사본·`generated.ts`·digest 불일치를 잡는다 (의도적 변조로 red 확인). +- canonical 19개 operationId가 전부 덮이는지 parity 검증: 18개는 계약 기여에, `uploadStudioAsset`은 업로드 전송 seam에 존재해야 한다. 어느 쪽에도 없는 operationId가 있으면 실패한다. +- `EXTERNAL_PACKAGE` identity가 `assertPackageIdentity`를 통과하고 `contractSet`에 나타나는지 확인. + +**전송(B)** — MSW로 canonical 응답·오류를 재현 + +- 불완전 draft 저장 성공, `expectedVersion` 충돌 → `VERSION_CONFLICT`. +- idempotency replay(`Idempotency-Replayed: true`)와 `IDEMPOTENCY_KEY_REUSED` 구분. +- workflow 전이: `INVALID → FIX_VALIDATION`, `VALID/WARNINGS → CREATE_PREVIEW`, `CURRENT → PUBLISH`, 게시 후 `NONE`, 편집 후 `VALIDATE` 복귀, expired preview 재생성. +- Publication: 최초 `PUBLISHED`, 재게시 `REPUBLISHED`, 취소 `UNPUBLISHED`, 과거 Snapshot 불변성. +- 23개 코드 전부의 UI 관측 가능한 처리. +- CSRF 토큰 획득 실패·만료 경로. +- 런타임 스위치: `MOCK`/`HTTP` 각각에서 gateway 종류가 선택되는지. + +**Asset(C)** + +- PNG/JPEG/WebP/SVG 업로드 성공, 미지원 형식 `UNSUPPORTED_MEDIA_TYPE`, 크기 초과 `PAYLOAD_TOO_LARGE`. +- `decorative=false` + 빈 alt → publish ERROR / `decorative=true` + `alt=""` 허용. +- `QUARANTINED` Asset의 Public·Preview 렌더링 차단. +- 사용 중 Asset hard delete 차단 → `ASSET_IN_USE`, `ARCHIVED` 전환 경로. +- Asset Picker가 evidence directive를 정확한 문법으로 삽입. +- parser가 빈 alt를 더 이상 syntax error로 차단하지 않음 + round-trip 픽스처 갱신. + +**렌더러 불변** + +동일 픽스처에 대해 `Instant Preview`, `Server Public Preview`, `Published Public`, `Publication Snapshot`의 semantic output이 동일해야 한다. Asset resolver도 같은 렌더러 경계로 주입한다. + +**회귀** + +기존 이식 parity 스위트(시각·접근성·라우트·아키텍처 경계)가 전부 통과해야 한다. 기본 스위치가 `MOCK`이므로 이 스위트는 영향받지 않아야 한다. + +## 위험과 완화 + +| 위험 | 완화 | +|---|---| +| 실행 중 Backend 없음 → 실응답 미검증 | canonical 계약 기준 MSW 검증. 스위치로 대조 지점을 남긴다. 완료 조건에 "실서버 대조 미포함"을 명시한다. | +| canonical yaml이 계속 변경 중 (오늘도 수정됨) | digest·revision을 커밋에 고정하고 drift 게이트로 감지. canonical 갱신은 의도적 재생성 커밋으로만 반영. | +| 계약 런타임 우회(업로드 1개)가 거버넌스 위반으로 보일 수 있음 | 좁은 seam으로 격리, 사유·범위를 adapter review 문서에 기록, 나머지 18개는 런타임 통과. | +| content format 의미 변경(alt)이 기존 픽스처를 깨뜨림 | parser 변경과 픽스처·검증 갱신을 한 단위로 묶어 red→green으로 수행. | +| 라우트 1개 추가가 registry 기준선을 깨뜨림 | 거버넌스 기준선 갱신을 같은 단위에 포함. | +| `openapi-typescript` 도입이 생성물 diff를 크게 만듦 | 첫 생성 결과를 별도 커밋으로 분리해 리뷰 가능하게 한다. | + +## 완료 조건 + +1. 현재 Public UI·라우트가 변경되지 않는다. +2. 현재 Studio 작업 흐름이 변경되지 않는다. +3. Studio 계약이 canonical `studio-v1.yaml`에서 생성되고, digest 고정과 drift 게이트가 동작한다. +4. `StudioGateway`의 모든 operation이 HTTP 어댑터로 구현되고 MSW 계약 테스트로 검증된다. +5. WorkingCopy 저장이 Public Projection을 변경하지 않는다. +6. Validation/Preview/Publish가 version과 dependency revision으로 묶인다. +7. Publication Event와 Snapshot을 현재 UI에서 조회할 수 있고 과거 Snapshot이 불변이다. +8. Image/SVG를 업로드하고 Case content에 Asset 기반 evidence로 삽입할 수 있다. +9. Asset은 `READY`일 때만 Preview/Publish에 사용된다. `QUARANTINED`는 렌더링되지 않는다. +10. 23개 오류 코드와 idempotency/version 충돌 구분이 처리된다. +11. 프론트가 Backend 도메인 Aggregate를 복제하지 않는다. +12. 런타임 스위치로 `MOCK`/`HTTP`를 전환할 수 있고, 기본 `MOCK`에서 기존 parity 스위트가 전부 통과한다. + +명시적 비완료 항목: + +- 실행 중 Backend와의 실응답 대조. Backend Studio 구현 완료 후 별도로 수행한다. +- Public 조회의 HTTP 전환. `public-v1.yaml` 기준 별도 spec/plan 사이클로 수행한다.