Files
tech-log-frontend/docs/superpowers/specs/2026-08-17-techlog-backend-alignment-design.md
T
DongHyeonkaandClaude Opus 5 12329ec9c0 docs: define TechLog backend alignment
Studio 계약을 tech-log-design-package의 canonical studio-v1.yaml 단일
출처로 정합시키고, Asset capability를 편집 흐름에 연결하는 설계를 확정한다.

- 계약 drift는 EXTERNAL_PACKAGE provenance의 digest 고정으로 막는다.
- Studio 전송은 플랫폼 계약 런타임을 통과한다. multipart 업로드 1개만
  전용 seam으로 분리한다 — 런타임이 JSON 본문만 표현할 수 있기 때문이다.
- CSRF는 전송 관심사로 어댑터 내부에 둔다. Studio 인증 UI는 추가하지 않는다.
- Public 조회의 HTTP 전환은 별도 사이클로 분리한다. 동기 포트를 async로
  바꾸는 작업이 19개 파일 31개 호출 지점과 이식 parity 기준선을 흔든다.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-17 23:24:29 +09:00

352 lines
23 KiB
Markdown

# 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``<img src>`로 렌더링한다.
정적 `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 사이클로 수행한다.