Files
clean-architecture-frontend…/docs/superpowers/plans/2026-09-16-indexeddb-kernel-promotion.md
T
DongHyeonkaandClaude Opus 5 c10a709f2c docs: record how the IndexedDB kernel promotion actually executed
사양 추정이 전부 빗나갔다. 4벌 감소는 -693이 아니라 -322이고 커널은 470이
아니라 1132줄이라, 소스 순증감이 -223이 아니라 +810이다. 원인은 사양이
translate/실패 헬퍼 비용을 안 셌고 이행된 파일의 주석이 크게 늘었기
때문이다. 작업의 근거는 처음부터 줄 수가 아니었지만, 줄 수가 준다는 기대가
틀렸다는 것은 기록해 둔다.

사양이 틀린 것으로 판명된 항목 3건과, 보존하지 못한 동작 1건(RT-2)을
복원하지 않기로 한 근거, 테스트로 덮지 못한 경로 1건을 남긴다.

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

29 KiB
Raw Blame History

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 콜백으로 주입하므로 mapIndexedDbExceptionmapBrowserDataException이 서로 다른 답을 내는 현 상태가 보존된다.

Tech Stack: TypeScript, IndexedDB, Vitest, tests/helpers/memory-indexeddb.ts(가짜 IDB)

Spec: docs/superpowers/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-inventorygit 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로 정당화하지 않는다

2026-09-16 갱신 — 사양의 추정이 빗나갔다. 커널을 실제로 구현하니 2파일 1,132줄(코드 751줄)이다. 사양 추정 470줄의 2.4배다. 사양 §3.1이 "470 LOC라 2분할한다"고 쓴 논거도 사실이 아니었다(2분할 자체는 테스트 셋업이 갈린다는 별도 근거로 유지). 그래서 아래 표의 순증감이 뒤집힌다.

사양 추정 실제
4벌에서 삭제 ~974 (미측정, 이행 후 확정)
4벌에 추가 (커널 호출부·콜백) ~281 (미측정)
4벌 순감 693 (미측정)
커널 신규 2파일 +470 +1,132
신규 커널 테스트 2파일 (미기재) +1,602
레포 순증감 (소스만) 223 약 +439

이 리팩토링은 줄 수를 줄이지 않는다. 늘린다. 소스 약 +439줄, 테스트까지 하면 약 +2,041줄이다. 사양은 "절감이 작다"고 썼지만 실제로는 절감이 아니라 증가다.

그래서 이 작업의 근거는 오로지 하나다: 트랜잭션 상태기계가 4개에서 1개가 되는 것. 줄 수로 정당화하려는 시도는 이제 불가능하다. 근거가 성립하는 이유는 오늘 그 4개가 이미 서로 다른 답을 내고 있기 때문이다:

상황 RT MT OP CP
트랜잭션 안 개별 요청 실패 본다 기록만 안 본다 즉시 abort
값 없이 완료 UNAVAILABLE UNAVAILABLE UNAVAILABLE CORRUPT_DATA
deleteDatabase 없음 없음 없음 있음

리뷰가 "이미 갈라졌다"고 판정한 근거가 이 표다.

그리고 이행을 멈추면 최악이다. 커널만 넣고 사본을 안 옮기면 +1,132줄의 쓰이지 않는 코드가 남는다. 이 레포가 이미 abortable-operation.ts로 겪고 있는 병(커널은 있는데 24개 파일이 안 씀)을 하나 더 만드는 것이다. 되돌리려면 지금이 가장 싸다 — 커널 커밋 하나를 revert하면 끝이고 사본은 아직 안 건드렸다.


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/128130/130)

Interfaces:

  • Produces: spec §3.2·§3.3의 전체 export 시그니처. Task 2~5가 이것만 쓴다.

  • Consumes: 기존 커널 snapshotAbortTimers(platform/abortable-operation.ts), contracts/result.tsResult

  • Step 1: 기존 커널 관례를 읽는다

src/adapters/platform/의 5파일을 읽고 주석 스타일(왜 이 규칙이 있는지를 근거와 함께 적는 방식), 에러 처리, 의존성 주입 방식을 파악한다. 새 파일은 그 관례를 따른다.

  • Step 2: 가짜 IDB의 오류 배선을 확인한다

tests/helpers/memory-indexeddb.ts를 읽는다. 커널은 요청 레벨 onerror를 새로 보게 되므로 가짜가 request.error를 채우는지가 전제다.

확인됨(2026-09-16): 채운다. :167-170request.error = asException(error)queueMicrotaskonerror 발화, :309-311이 같은 일을 동기로 한다. 두 경로의 타이밍이 다르므로 테스트에서 주의한다.

  • Step 3: 실패하는 테스트를 먼저 쓴다

최소한 아래를 고정한다. 각각 먼저 실패하는 것을 확인한 뒤 구현한다.

연결(tests/unit/indexeddb-connection.test.ts):

  • open 성공 / onerror / native throw
  • onblocked — deadline 미설정 시 BLOCKED, deadline 경과 시 BLOCKED_DEADLINE
  • upgrade APPLIED
  • upgrade REJECTEDversionchange 트랜잭션이 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:11AbortTerminalReason 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: 게이트
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:unitci-artifact-contract.test.ts 외 실패 0.

  • Step 7: 커밋
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 안 함)로 바꾸면 요청 실패 후에도 뒤 요청이 커밋됨. compareAndSwapget→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: 게이트 + 표적 테스트
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: 커밋
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-290BLOCKED/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: 게이트 + 표적 테스트
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: 커밋
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_INDEXunique: true(L1000-1004)이므로 **중복 put은 요청 레벨 ConstraintErrorCONFLICT**가 되고, 오늘은 transaction.error가 무엇이냐에 따라 달라진다.

이행 전에 중복 put 테스트를 tests/unit/indexeddb-opfs-journal.test.ts에 추가해 현재 답을 기록하고, 이행 후 달라진 답을 의도된 변경으로 승인한다. 가짜 IDB가 이 경로를 표현하지 못하면 tests/helpers/memory-indexeddb.ts를 먼저 보강한다.

  • Step 2: OP-2를 확인한다

오늘 OP의 runTransactionsucceed 이후 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: 게이트 + 표적 테스트
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: 커밋
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: 게이트 + 전체 테스트
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: 커밋
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>"

완료 판정

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 요청 배선:

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벌 통일mapIndexedDbExceptionmapBrowserDataException이 같은 에러에 다른 답을 내는 것은 별건이다. 이 계획은 그 차이를 translate 주입으로 보존한다.
  • deleteDatabase를 3벌에 추가 — CP에만 있는 것이 의도다(spec §2.3).
  • 대형 파일 분할 — 커널 이행으로 RT가 2902 → 2665가 되지만 여전히 크다. 분할은 별도 계획이다.

실행 기록 (2026-09-16 — 5개 태스크 전부 완료)

커밋 5개: cb62bfb(커널) · bb6080b(CP) · 3366a81(MT) · 217c1dd(OP) · a91e78f(RT).

실측 LOC — 사양 추정은 전부 빗나갔다

사본 사양 추정 실제(전체 줄) 실제(코드 줄)
CP indexeddb-checkpoint-store.ts 142 712 → 612 = 100
MT indexeddb-maintenance.ts 188 1558 → 1430 = 128
OP indexeddb-opfs-journal.ts 127 1817 → 1881 = +64 1744 → 1724 = 20
RT indexeddb-runtime.ts 235 2902 → 2744 = 158 2807 → 2563 = 244
4벌 합 693 322
커널 2파일 +470 +1,132 +751
소스 순증감 223 +810
커널 테스트 2파일 (미기재) +1,602

공통 원인: 사양이 translate/실패 헬퍼 어댑터 함수 비용을 세지 않았다. CP는 4개(~37줄), MT는 6개가 필요했다. 그리고 이행된 파일들의 주석이 크게 늘었다(OP 6→90줄, RT 12→111줄) — 이 레포 관례상 부풀림이 아니라 개선이지만, 줄 수 예측은 무너뜨린다.

결론은 바뀌지 않는다. 이 작업의 근거는 처음부터 줄 수가 아니라 상태기계 통합이었고, 그건 달성됐다. 다만 줄 수가 준다는 기대는 완전히 틀렸다는 것을 기록해 둔다.

부수 성과

  • abort 래칫이 24 → 21로 세 칸 조여졌다. CP·MT·RT가 각각 자기 abort 리스너를 지웠다. 래칫이 설계대로 작동했다.
  • 가짜 IndexedDB가 unique 인덱스를 전혀 강제하지 않는 것을 찾아 고쳤다(tests/helpers/memory-indexeddb.ts). 보강 전에는 중복 beginok:true로 성공했다 — 브라우저가 거부할 상태를 테스트가 조용히 허용하고 있었다. 이 발견이 커널 이행 자체보다 가치가 클 수 있다.
  • 사양이 빠뜨린 함정 하나를 CP에서 막았다: bindScopesucceed()에 해당하는 것이 없어 그대로 옮기면 정상 바인딩이 CORRUPT_DATA로 보고된다. 이후 MT·OP·RT는 성공 출구를 전수 대조했다.

사양이 틀린 것으로 판명된 항목 3건

  1. OP-1 — 사양은 "이 리팩토링에서 가장 큰 동작 변화"로 지목하며 중복 put의 답이 달라진다고 봤다. 틀렸다. 이행 전후 모두 CONFLICT / retryable:false / recovery:NONE이다. 요청 오류를 아무도 처리하지 않으면 스토어가 바로 그 에러로 abort해서 transaction.error === request.error이기 때문이다. 바뀐 것은 답이 아니라 출처다.
  2. RT-2 — 사양은 "의미 동일"이라고 썼다. 틀렸다. 아래 참조.
  3. §3.1의 파일 분할 논거 — "470 LOC라 1파일은 폴더 관례를 깬다"고 썼으나 실제 구현은 1,132줄이다. 2분할 자체는 유지할 값이 있지만(테스트 셋업이 갈린다) 그 논거는 사실이 아니었다.

보존하지 못한 동작 1건 — RT-2 (의도적으로 남김)

blocked 데드라인 이후 재시도가 새 factory.open()을 띄운다. 이행 전에는 안 띄웠다. 커널에 "settle 이후에도 살아 있는 요청"을 알려줄 훅이 없기 때문이다.

복원하지 않기로 했다. 근거:

  • 이행 전후 모두 10초 blocked 데드라인이 있고, 차이는 두 번째 네이티브 open을 띄우는지뿐이다.
  • 늦게 도착한 연결은 커널이 closeQuietly로 닫으므로 연결 누수도 데이터 위험도 없다.
  • 재시도가 10초 데드라인에 게이트되므로 쌓이는 속도가 제한적이다.
  • 반면 지금 커널을 고치면 이미 검증이 끝난 4개 사본을 전부 재검증해야 한다. 이익 대비 위험이 맞지 않는다.

복원하려면 openIndexedDbDatabaseonSettled를 추가하면 된다(deleteIndexedDbDatabase에는 이미 있다). 다만 그러면 "blocked 데드라인 실패가 버려진 요청이 끝날 때까지 다음 acquire를 막는가"라는 설계 질문이 따라온다 — 막는다면 다른 탭이 영영 안 닫힐 때 재시도가 영구 차단되어 지금보다 나쁘다. 착수 전 그 답부터 정해야 한다.

테스트로 덮지 못한 경로 1건

RT의 POLICY_REJECTED upgrade 거절 경로. queueIndexedDbUpgradeBindingonRejected가 비동기라 .then 후처리로 보존했으나, 가짜 IDB의 upgrade 트랜잭션이 동기라 단위 테스트로 검증할 수 없다. 레포 전체에 이 경로를 덮는 테스트가 없다. 실브라우저 확인이 필요하다.

검증 결과

PASS: check:types lint check:architecture check:adapter-inventory(21/21 래칫) check:bundle(181114/204800, 이행 전과 동일) test:integration(81) test:component(130)

test:unit: 실패 파일은 tests/unit/ci-artifact-contract.test.ts 하나뿐(16~18건, 실행마다 흔들림 — bwrap: loopback: Failed RTM_NEWADDR). develop 5434760 기준선에서도 동일함을 stash 후 실행해 확인했다. 그 파일 밖 실패 0.

실브라우저 미검증: tests/browser-capabilities/indexeddb-runtime.spec.ts(1033) · opfs-runtime.spec.ts(221) · resumable-upload.spec.ts(526). 전부 가짜 IDB 위에서만 검증됐다. blocked/versionchange 실동작과 위 upgrade 경로는 여기서만 잡힌다. CI 또는 로컬 브라우저 실행이 남은 과제다.