Files
clean-architecture-frontend…/docs/superpowers/plans/2026-09-16-indexeddb-kernel-promotion.md
T
DongHyeonkaandClaude Opus 5 0f157494cd refactor: drop the surplus kernel re-export, and plan the IndexedDB kernel
telemetry가 platform/bounded-capacity.ts의 assertBoundedCapacity를 다시
내보내고 있었다. 같은 파일이 이미 그 심볼을 직접 import해서 쓰고 있고 이
재수출의 소비자는 없었다. 남겨두면 커널로 가는 두 번째 문이 되고, 어댑터가
어댑터를 통해 커널에 닿는 경로를 정당화하게 된다.

같이 담긴 IndexedDB 커널 이행 계획은 커널 -> CP -> MT -> OP -> RT 순서와
각 사본의 동작 변화 지점을 줄 번호로 특정한다. LOC 절감은 -223으로 작고,
이 작업의 근거는 줄 수가 아니라 트랜잭션 상태기계를 4개에서 1개로 만드는
것이라는 점을 명시했다.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-16 19:01:38 +09:00

388 lines
22 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# IndexedDB 커널 승격 Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** 어댑터 4곳이 각자 구현한 IndexedDB 연결·트랜잭션 메커니즘을 `src/adapters/platform/`의 커널 2파일로 모으고, 4벌이 서로 다른 답을 내던 지점을 하나로 만든다.
**Architecture:** 커널은 **메커니즘만** 갖는다 — open 요청을 Promise로 바꾸기, blocked 데드라인, upgrade/error/success 라우팅, 늦게 도착한 연결 닫기, 트랜잭션 상태기계, 커서 펌프. 데이터베이스 이름·스키마·마이그레이션·governance·**실패 분류(taxonomy)**는 각 서브시스템에 남는다. 실패 매핑은 `translate` 콜백으로 주입하므로 `mapIndexedDbException``mapBrowserDataException`이 서로 다른 답을 내는 현 상태가 보존된다.
**Tech Stack:** TypeScript, IndexedDB, Vitest, `tests/helpers/memory-indexeddb.ts`(가짜 IDB)
**Spec:** [`docs/superpowers/specs/2026-09-16-indexeddb-kernel-promotion-design.md`](../specs/2026-09-16-indexeddb-kernel-promotion-design.md)
## Global Constraints
- **`src/adapters/platform/`은 이미 dependency-cruiser의 kernel carve-out이다.** 커널에 파일을 추가하는 데 규칙 변경이 필요 없다(spec §5.3).
- **`IDBFactory`는 필수 주입 파라미터다.** `globalThis.indexedDB`를 읽으면 안 된다 — `eslint.config.ts:40-63`이 모든 브라우저 루트에서 그 속성을 막고, 예외는 `platform/browser-lifecycle.ts` **한 파일**에만 부여돼 있다. 주입받으면 `eslint.config.ts`를 건드릴 필요가 없다(spec §5.1).
- **소스 파일을 추가하면 `docs/reviews/adapters/INVENTORY.md`에 행을 추가하고 하단 합계를 고친다.** `check:adapter-inventory``git ls-files src/adapters`와 집합을 정확히 대조한다.
- **`check:adapter-inventory`에 abort 래칫이 있다.** `addEventListener("abort")`를 쓰는 어댑터 파일이 24개를 넘으면 실패한다. `platform/`은 세지 않는다.
- **실패 매핑 4벌을 통일하지 마라.** `mapIndexedDbException`(RT/MT/OP)과 `mapBrowserDataException`(CP)은 같은 에러에 다른 답을 낸다 — `ConstraintError` recovery가 NONE vs REOPEN, `QuotaExceeded` retryable이 false vs true, `NotFound`가 MIGRATION_FAILED vs NOT_FOUND. 통일은 별건이고 이 계획의 범위가 아니다.
- **`deleteDatabase`를 나머지 3벌에 추가하지 마라.** CP에만 있는 것이 의도다(spec §2.3).
- **이 환경의 알려진 제약:** `tests/unit/ci-artifact-contract.test.ts` 16건이 `bwrap: loopback: Failed RTM_NEWADDR: Operation not permitted`로 실패한다. `develop` `5434760` 기준선에서도 동일하다. **판정 기준은 그 파일 외의 실패가 0인지**다.
- 커밋 메시지 끝에 붙일 것: `Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>`
## 왜 이 작업을 하는가 — LOC로 정당화하지 않는다
| | LOC |
|---|---:|
| 4벌에서 삭제 | ~974 |
| 4벌에 추가 (커널 호출부·콜백) | ~281 |
| 커널 신규 2파일 | +~470 |
| **레포 순증감** | **~223** |
**절감은 작다.** 성과는 줄 수가 아니라 **트랜잭션 상태기계가 4개에서 1개가 되는 것**이다. 오늘 그 4개는 이미 서로 다른 답을 낸다:
| 상황 | RT | MT | OP | CP |
|---|---|---|---|---|
| 트랜잭션 안 개별 요청 실패 | 본다 | 기록만 | **안 본다** | 즉시 abort |
| 값 없이 완료 | UNAVAILABLE | UNAVAILABLE | UNAVAILABLE | **CORRUPT_DATA** |
| `deleteDatabase` | 없음 | 없음 | 없음 | **있음** |
리뷰가 "이미 갈라졌다"고 판정한 근거가 이 표다.
---
### Task 1: 커널 2파일 + 단위 테스트
사본은 **손대지 않는다.** 코드 추가만 하므로 런타임 동작이 바뀌지 않는다.
**Files:**
- Create: `src/adapters/platform/indexeddb-connection.ts` (~240 LOC)
- Create: `src/adapters/platform/indexeddb-transaction.ts` (~230 LOC)
- Create: `tests/unit/indexeddb-connection.test.ts`
- Create: `tests/unit/indexeddb-transaction.test.ts`
- Modify: `docs/reviews/adapters/INVENTORY.md` (행 2개 + 합계 `128/128``130/130`)
**Interfaces:**
- Produces: spec §3.2·§3.3의 전체 export 시그니처. Task 2~5가 이것만 쓴다.
- Consumes: 기존 커널 `snapshotAbortTimers`(`platform/abortable-operation.ts`), `contracts/result.ts``Result`
- [ ] **Step 1: 기존 커널 관례를 읽는다**
`src/adapters/platform/`의 5파일을 읽고 주석 스타일(왜 이 규칙이 있는지를 근거와 함께 적는 방식), 에러 처리, 의존성 주입 방식을 파악한다. 새 파일은 그 관례를 따른다.
- [ ] **Step 2: 가짜 IDB의 오류 배선을 확인한다**
`tests/helpers/memory-indexeddb.ts`를 읽는다. 커널은 요청 레벨 `onerror`를 새로 보게 되므로 가짜가 `request.error`를 채우는지가 전제다.
확인됨(2026-09-16): 채운다. `:167-170``request.error = asException(error)``queueMicrotask``onerror` 발화, `:309-311`이 같은 일을 **동기로** 한다. **두 경로의 타이밍이 다르므로** 테스트에서 주의한다.
- [ ] **Step 3: 실패하는 테스트를 먼저 쓴다**
최소한 아래를 고정한다. 각각 먼저 실패하는 것을 확인한 뒤 구현한다.
연결(`tests/unit/indexeddb-connection.test.ts`):
- open 성공 / `onerror` / native throw
- `onblocked` — deadline 미설정 시 `BLOCKED`, deadline 경과 시 `BLOCKED_DEADLINE`
- upgrade `APPLIED`
- upgrade `REJECTED`**versionchange 트랜잭션이 abort되어 스키마가 커밋되지 않는 것까지** 확인
- upgrade가 throw → `REJECTED` + `detail`에 thrown value
- `newVersion`이 null → `upgrade` 실행 **전에** `UPGRADE_REJECTED`
- admission `ADMIT` / `REJECT` / `FAIL` — 거부 시 연결이 **닫히는지**
- 호출자가 포기한 뒤 늦게 도착한 연결이 닫히는지
- `CALLER_ABORT`
- `translate`가 각 cause에 대해 호출되는지
트랜잭션(`tests/unit/indexeddb-transaction.test.ts`):
- 커밋 / abort
- `succeed()` 없이 완료 → `NO_VALUE_PRODUCED`
- 첫 결과가 이긴다(`succeed` 이후 `fail` 무시)
- 커서 순회와 `SUSPEND` 스텝(중첩 요청 체인)
- 예산 콜백이 "삭제 행 기준"과 "스캔 행 기준" 양쪽을 표현할 수 있는지
- **CP-4 필수 요건:** `abort()`가 throw하면 caller-abort 표시를 세우지 않고 transaction 이벤트가 결과를 정한다
- [ ] **Step 4: 구현한다 — 좁은 커널 함정을 피한다**
기존 커널 `abortable-operation.ts:11``AbortTerminalReason` 3멤버를 **반환 타입**에 박아서 5종이 필요한 `http-execution-v3`가 아예 못 썼다. 같은 실수를 반복하면 이 작업은 실패다.
구현 후 아래를 확인한다:
- `IndexedDbFailureCause`가 어떤 공개 **반환 타입**에도 나타나지 않는가 (`translate`의 입력으로만 쓰이는가)
- spec §3.4의 4개 사본 예시(RT/MT/OP/CP)가 **전부** 수용되는가
하나라도 수용되지 않으면 **구현을 멈추고 보고한다.** spec에 억지로 맞추지 않는다.
- [ ] **Step 5: INVENTORY 갱신**
`docs/reviews/adapters/INVENTORY.md`에 두 행을 알파벳 위치에 넣고 번호를 다시 매긴다. 링크는 같은 그룹(`platform/`) 기존 행과 동일하게 `[Network/state](./01-network-and-state.md)`. 하단 합계를 `130/130`으로.
- [ ] **Step 6: 게이트**
```bash
corepack pnpm check:types:app && corepack pnpm check:types:test \
&& corepack pnpm lint && corepack pnpm check:architecture \
&& corepack pnpm check:adapter-inventory
corepack pnpm test:unit
```
Expected: 앞 묶음 전부 PASS. `test:unit``ci-artifact-contract.test.ts` 외 실패 0.
- [ ] **Step 7: 커밋**
```bash
git add src/adapters/platform tests/unit/indexeddb-*.test.ts docs/reviews/adapters/INVENTORY.md
git commit -m "$(cat <<'EOF'
feat: add the shared IndexedDB connection and transaction kernel
네 어댑터가 각자 구현한 open/blocked/upgrade/트랜잭션 메커니즘을 커널로
모은다. 사본은 아직 이행하지 않았으므로 런타임 동작은 그대로다.
실패 분류는 커널에 넣지 않고 translate 콜백으로 주입한다. RT/MT/OP의
mapIndexedDbException과 CP의 mapBrowserDataException이 같은 에러에 다른
답을 내며, 그 차이를 통일하는 것은 별건이기 때문이다.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
EOF
)"
```
---
### Task 2: CP 이행 — `indexeddb-checkpoint-store.ts` (712 → 약 570)
**CP를 먼저 하는 이유:** 가장 작고, 동작 변화 지점이 가장 명확하며, 커널의 CP-4 요건(abort가 throw하면 caller-abort를 세우지 않는다)을 조기에 검증한다.
**Files:**
- Modify: `src/adapters/browser-transfer/resumable-upload/indexeddb-checkpoint-store.ts`
- Modify: `tests/unit/resumable-upload-checkpoint.test.ts` (동작 변화 지점 고정)
**Interfaces:**
- Consumes: Task 1의 `openIndexedDbDatabase`, `runIndexedDbTransaction`, `deleteIndexedDbDatabase`
- Produces: 없음 (공개 포트 형태 불변)
- [ ] **Step 1: 동작 변화 지점을 테스트로 먼저 고정한다**
구현 전에 아래 5개가 현재 동작대로 통과하는지 확인한다. 이행 후에도 같아야 한다.
| # | 지점 | 현재 동작 | 깨지면 |
|---|---|---|---|
| CP-1 | 값 없는 완료 | `CORRUPT_DATA/RECONCILE` (L541-547) | 번역기가 `NO_VALUE_PRODUCED → CORRUPT_DATA`를 명시 매핑 안 하면 UNAVAILABLE로 바뀜 |
| CP-2 | `durability` | 옵션 bag 없음 (L512) | 커널 기본이나 `"strict"`를 넣으면 체크포인트 쓰기가 조용히 느려짐 (성능 회귀) |
| CP-3 | `nativeFailure` | 기록 후 **즉시 abort** (L577-588) | `requestFailed`(abort 안 함)로 바꾸면 요청 실패 후에도 뒤 요청이 커밋됨. `compareAndSwap``get→put` 체인(L321-343)에서 특히 위험 |
| CP-4 | `abort()`가 throw | `failure`**되돌린다** (L528, L536) | 커밋된 체크포인트를 ABORTED로 보고 |
| CP-5 | `deletePartition` blocked 타이머 | 네이티브 `setTimeout` (L450) | 주입형 `timers`로 바뀜. `tests/unit/resumable-upload-checkpoint.test.ts:194`가 이 경로를 봄 |
기존 단언 위치: `:142` CONFLICT/RECONCILE, `:190` UNAVAILABLE/RESUME, `:194` blocked PENDING, `:238` false-abort 금지.
- [ ] **Step 2: 삭제 대상 블록을 커널 호출로 바꾼다**
| 블록 | 줄 | 대체 |
|---|---|---|
| `TransactionContext` 타입 | L491-495 | 커널 타입 |
| `openAndBind`의 open 요청 Promise 배선 | L163-175, L198-217 | `openIndexedDbDatabase` (upgrade 본문 L176-197은 `upgrade` 콜백으로, `bindScope` 호출은 `admit`으로) |
| `runCheckpointTransaction` | L497-596 | `runIndexedDbTransaction` |
| `bindScope`의 트랜잭션 배선 | L602-623 | `runIndexedDbTransaction` (검증 로직 L624-652는 `queue`로 그대로) |
| `deletePartition`의 blocked/settle 배선 | L431-462, L472-483 | `deleteIndexedDbDatabase` |
**남길 것:** `PENDING_DELETIONS` 레지스트리(L31-41) + 생성 시 검사(L116-122), `uploadCheckpointDatabaseName`(L73-83), `sameScopeBinding`(L656-672), `snapshotScope`(L674-691), `snapshotCheckpoint`(L693-711).
- [ ] **Step 3: 게이트 + 표적 테스트**
```bash
corepack pnpm check:types:app && corepack pnpm lint && corepack pnpm check:architecture
npx vitest run tests/unit/resumable-upload-checkpoint.test.ts --reporter=default
```
Expected: 전부 PASS. CP-1~CP-5가 이행 전과 같은 답을 내야 한다.
- [ ] **Step 4: 커밋**
```bash
git add src/adapters/browser-transfer tests/unit/resumable-upload-checkpoint.test.ts
git commit -m "refactor: move the upload checkpoint store onto the IndexedDB kernel
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>"
```
---
### Task 3: MT 이행 — `indexeddb-maintenance.ts` (1558 → 약 1370)
**MT를 두 번째로 하는 이유:** 커널 사용자 중 연결 핸들을 안 쓰는 유일한 사본이라 `openIndexedDbDatabase` 단독 사용 경로를 검증한다.
**Files:**
- Modify: `src/adapters/storage/indexeddb/indexeddb-maintenance.ts`
- Modify: `tests/unit/indexeddb-maintenance.test.ts`
**Interfaces:**
- Consumes: Task 1의 `openIndexedDbDatabase`, `openIndexedDbTransaction`, `runIndexedDbTransaction`, `walkIndexedDbCursor`
- [ ] **Step 1: 두 개의 함정을 먼저 이해한다**
**MT-1 (가장 위험).** `blockedTimeoutMs`**넘기지 마라.** 안 넘겨야 오늘 동작(blocked 이벤트 즉시 `BLOCKED`, L485-492)이 유지된다. 넘기면 배치가 최대 그 시간만큼 매달린다. `tests/unit/indexeddb-maintenance.test.ts:289-290``BLOCKED/retryable:true/RELOAD_OTHER_CONTEXTS`**즉시** 받길 기대하므로 값을 넣으면 타임아웃으로 실패한다.
**MT-2.** `upgrade` 콜백을 **생략하라.** 생략해야 오늘의 "upgrade는 곧 실패"(L477-484)가 유지된다. 커널은 생략을 `UPGRADE_REJECTED`로 해석한다. 실수로 `upgrade: () => ({kind:"APPLIED"})`를 넣으면 **잘못된 스키마로 열린다.**
- [ ] **Step 2: 삭제 대상 블록을 커널 호출로 바꾼다**
| 블록 | 줄 | 대체 |
|---|---|---|
| `TransactionContext` 타입 | L97-101 | 커널 타입 |
| `openExactVersion`의 배선분 | L434-508, L551-585 | `openIndexedDbDatabase` (L509-550을 `admit`으로 이식) |
| `createTransaction` | L587-604 | `openIndexedDbTransaction` |
| `runTransaction` | L606-705 | `runIndexedDbTransaction` |
| 커서 2곳의 deadline/maxRows/abort 보일러플레이트 | L799-833, L1451-1479 | `walkIndexedDbCursor` |
**MT-3.** `database.onversionchange = () => database.close()`(L543)를 `admit` 안으로 옮긴다. `admit`은 성공 경로에서만 실행되므로 등록 시점이 오늘과 같다.
**남길 것:** 체크포인트 상태기계(`readCheckpoint` L707-756, `commitPrepared` L969-1251), `prepareRecords`(L863-967), `clock`/`epochClock`(L401-421), `countBucket`(L239-245), 저장 술어(L118-223).
- [ ] **Step 3: 게이트 + 표적 테스트**
```bash
corepack pnpm check:types:app && corepack pnpm lint && corepack pnpm check:architecture
npx vitest run tests/unit/indexeddb-maintenance.test.ts --reporter=default
```
Expected: 전부 PASS. 특히 `:289-290`**즉시** BLOCKED를 받아야 한다(타임아웃이 아니라).
- [ ] **Step 4: 커밋**
```bash
git add src/adapters/storage tests/unit/indexeddb-maintenance.test.ts
git commit -m "refactor: move IndexedDB maintenance onto the kernel
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>"
```
---
### Task 4: OP 이행 — `indexeddb-opfs-journal.ts` (1817 → 약 1690)
**이 계획에서 동작 변화가 가장 큰 태스크다.**
**Files:**
- Modify: `src/adapters/storage/opfs/indexeddb-opfs-journal.ts`
- Modify: `tests/unit/indexeddb-opfs-journal.test.ts`
- 가능성: `tests/helpers/memory-indexeddb.ts` 보강
**Interfaces:**
- Consumes: Task 1의 `openIndexedDbDatabase`, `createIndexedDbConnection`, `runIndexedDbTransaction`, `openIndexedDbTransaction`, `walkIndexedDbCursor`
- [ ] **Step 1: OP-1을 테스트로 먼저 드러낸다 — 이 태스크의 핵심**
오늘 OP는 트랜잭션 안의 개별 요청에 `.onerror`**하나도** 안 단다(파일 전체에서 request `.onerror`는 L1042의 open 요청 하나뿐). 요청 실패는 `transaction.onabort`로만 흘러 `mapIndexedDbException(transaction.error)`(L1158)가 된다.
커널을 쓰면 **요청 자신의 오류가 보고된다.** 구체적으로 `LOGICAL_KEY_INDEX``unique: true`(L1000-1004)이므로 **중복 put은 요청 레벨 `ConstraintError``CONFLICT`**가 되고, 오늘은 `transaction.error`가 무엇이냐에 따라 달라진다.
이행 **전에** 중복 put 테스트를 `tests/unit/indexeddb-opfs-journal.test.ts`에 추가해 현재 답을 기록하고, 이행 후 달라진 답을 의도된 변경으로 승인한다. 가짜 IDB가 이 경로를 표현하지 못하면 `tests/helpers/memory-indexeddb.ts`를 먼저 보강한다.
- [ ] **Step 2: OP-2를 확인한다**
오늘 OP의 `runTransaction``succeed` 이후 `fail`이 와도 `explicitFailure`가 이기지만(L1133-1141, `hasValue`는 true 유지) `oncomplete`는 값을 반환한다(L1143-1153). 커널의 "첫 결과가 이긴다" 규칙을 따르면 **`succeed` 후의 `fail`이 무시된다.**
현재 OP 코드에 그 순서가 실제로 발생하는 경로가 있는지 **확인하라**(spec은 미확인으로 남겼다). 없으면 변화 없음으로 기록하고 넘어간다.
- [ ] **Step 3: 삭제 대상 블록을 커널 호출로 바꾼다**
| 블록 | 줄 | 대체 |
|---|---|---|
| `TransactionContext` 타입 | L48-51 | 커널 타입 |
| scheduler 기본값 인라인 | L144-153 | `snapshotAbortTimers` |
| `openDatabase`의 배선분 | L961-996, L1031-1072 | `openIndexedDbDatabase` + `createIndexedDbConnection` (upgrade 본문 L997-1029는 `upgrade` 콜백으로 그대로 이동) |
| `runTransaction` | L1098-1174 | `runIndexedDbTransaction` |
| `strictReadwriteTransaction` | L1176-1190 | `openIndexedDbTransaction(…, "strict")` |
| 커서 2곳의 limit 루프 | L604-633, L677-712 | `walkIndexedDbCursor` |
**OP-4.** `signal`**넣지 않는다.** 오늘 없는 취소를 새로 만들지 않는다.
- [ ] **Step 4: 게이트 + 표적 테스트**
```bash
corepack pnpm check:types:app && corepack pnpm lint && corepack pnpm check:architecture
npx vitest run tests/unit/indexeddb-opfs-journal.test.ts tests/unit/opfs-byte-store.test.ts --reporter=default
```
Expected: PASS. OP-1로 인한 답 변화는 Step 1에서 승인한 것만 있어야 한다.
- [ ] **Step 5: 커밋**
```bash
git add src/adapters/storage tests
git commit -m "refactor: move the OPFS journal onto the IndexedDB kernel
요청 레벨 오류를 처음으로 보게 된다. unique 인덱스 위반이 트랜잭션 abort
사유가 아니라 요청 자신의 ConstraintError로 보고된다.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>"
```
---
### Task 5: RT 이행 — `indexeddb-runtime.ts` (2902 → 약 2665)
**가장 크고 마지막이다.** 다른 셋이 커널을 전부 검증한 뒤에 옮긴다.
**Files:**
- Modify: `src/adapters/storage/indexeddb/indexeddb-runtime.ts`
- Modify: `tests/unit/indexeddb-runtime.test.ts`
- [ ] **Step 1: RT-1을 번역기에 명시한다**
`close()`가 진행 중 open을 끝내는 원인이 `unavailable(operation)`(L743)에서 `translate({kind:"CLOSED"})`로 바뀐다. 번역기가 **`CLOSED → unavailable`을 명시 매핑**해야 오늘 동작이 유지된다. 빠뜨리면 `close()` 중 open이 ABORTED로 보고된다.
기존 실패 코드 단언 위치: `tests/unit/indexeddb-runtime.test.ts:339-340, :371-372, :454, :534, :549-550, :734-735`.
- [ ] **Step 2: 삭제 대상 블록을 커널 호출로 바꾼다**
| 블록 | 줄 | 대체 |
|---|---|---|
| `defaultScheduler` | L108-117 | `snapshotAbortTimers(scheduler)` |
| `TransactionContext` 타입 | L85-89 | `IndexedDbTransactionContext` |
| `waitForOpeningAttempt` | L659-682 | `connection.acquire(signal)` |
| `startOpeningAttempt`의 배선분 | L684-745, L776-843, L872-921 중 배선분 | `openIndexedDbDatabase` (upgrade/admit 콜백 본문은 그대로) |
| `createTransaction` | L957-974 | `openIndexedDbTransaction` |
| `runTransaction` | L976-1074 | `runIndexedDbTransaction` |
| 커서 5곳의 보일러플레이트 | L1374-1382, L1454-1471, L2364-2389, L2531-2548, L2573-2590, L2644-2661, L2718-2735 | `walkIndexedDbCursor` + `budget.admit` |
**RT-2.** `settleNativeRequest`/`activeOpeningGeneration`(L595, L691-697)이 사라진다. 커널의 settle-once와 단일 비행이 같은 역할을 한다. **의미는 같지만 경합 순서가 달라질 수 있다** — 동시 open 테스트를 주의해서 본다.
**RT-3.** `openingRequest` 필드(L593)는 오늘도 L712·L2878의 대입 외에 읽는 곳이 없다. 삭제한다.
**남길 것:** 코덱/영수증/보존/예산(bytes)/governance/migration 목록/상태 브로드캐스트/`monotonicClock`/`countBucket`/저장 레코드 술어/`purgePartitionRecords`의 스토어 순서 로직 — 전부 정책이다.
- [ ] **Step 3: 게이트 + 전체 테스트**
```bash
corepack pnpm check:types && corepack pnpm lint && corepack pnpm check:architecture \
&& corepack pnpm check:adapter-inventory
corepack pnpm test:unit
corepack pnpm test:integration
```
Expected: `ci-artifact-contract.test.ts` 외 실패 0.
- [ ] **Step 4: 커밋**
```bash
git add src/adapters/storage tests/unit/indexeddb-runtime.test.ts
git commit -m "refactor: move the IndexedDB runtime onto the kernel
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>"
```
---
## 완료 판정
```bash
corepack pnpm check:types
corepack pnpm lint
corepack pnpm check:architecture
corepack pnpm check:adapter-inventory
corepack pnpm check:bundle
corepack pnpm test:unit # ci-artifact-contract.test.ts 외 실패 0
corepack pnpm test:integration
corepack pnpm test:component
corepack pnpm test:browser-file-storage-removal # error TS 0건
corepack pnpm test:realtime-removal # error TS 0건
```
그리고 아래가 **0**이어야 한다 — 커널 밖에 남은 open 요청 배선:
```bash
grep -rn "createObjectStore\|onupgradeneeded" src/adapters --include='*.ts' \
| grep -v "src/adapters/platform/" | grep -v "upgrade" | wc -l
```
**실브라우저 확인이 필요하다.** 이 계획은 가짜 IDB 위에서만 검증된다. `tests/browser-capabilities/indexeddb-runtime.spec.ts`(1033 LOC), `opfs-runtime.spec.ts`(221), `resumable-upload.spec.ts`(526)를 실브라우저에서 돌려야 blocked/versionchange 실동작 회귀를 잡는다.
## 이 계획이 하지 않는 것
- **실패 매핑 4벌 통일** — `mapIndexedDbException``mapBrowserDataException`이 같은 에러에 다른 답을 내는 것은 별건이다. 이 계획은 그 차이를 `translate` 주입으로 **보존**한다.
- **`deleteDatabase`를 3벌에 추가** — CP에만 있는 것이 의도다(spec §2.3).
- 대형 파일 분할 — 커널 이행으로 RT가 2902 → 2665가 되지만 여전히 크다. 분할은 별도 계획이다.