docs: record the barrel boundary and IndexedDB kernel designs with a plan
모듈별 구조 리뷰에서 나온 두 설계를 spec으로 남기고, 그중 배럴 경계 작업의 실행 계획을 쓴다. 배럴 규칙은 실제 import 그래프에 돌려 위반 15건이 치환 대상 15줄과 일치함을 확인했고, 재수출할 심볼 82개는 실제 export와 대조했다. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
5434760ddf
commit
a42d96185f
@@ -0,0 +1,673 @@
|
||||
# 어댑터 배럴 공개 경계 확립 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:** 어댑터 16개 그룹 전부에 `index.ts` 공개 경계를 세우고, 그룹 바깥은 배럴만 import하도록 게이트로 강제한다.
|
||||
|
||||
**Architecture:** 각 어댑터 그룹의 `index.ts`가 그 그룹의 공개 표면이 된다. 그룹 내부 파일끼리, 그리고 어댑터→커널(`platform/**`) 간선은 지금처럼 파일 직접 import를 유지한다 — 커널의 정체성은 파일이고 `check:adapter-inventory`가 그것을 파일 경로로 검증하기 때문이다. `.dependency-cruiser.json`에 규칙 하나를 추가하고 회귀 fixture로 그 거부를 고정한다.
|
||||
|
||||
**Tech Stack:** TypeScript (NodeNext/Bundler), dependency-cruiser, 자체 TypeScript 인지 import 그래프(`scripts/check-architecture.ts`), Vite, Vitest
|
||||
|
||||
**Spec:** [`docs/superpowers/specs/2026-09-16-adapter-barrel-boundary-design.md`](../specs/2026-09-16-adapter-barrel-boundary-design.md)
|
||||
|
||||
## Global Constraints
|
||||
|
||||
- 기준선(2026-09-16, `develop` `5434760`): `check:architecture` PASS(297 모듈 / 897 의존 / 미해결 0), `check:types:app` PASS, `check:adapter-inventory` PASS(120 파일). **이 셋을 깨면 안 된다.**
|
||||
- `docs/reviews/adapters/INVENTORY.md`는 `git ls-files src/adapters`와 **집합이 정확히 일치**해야 한다. 소스 파일을 추가/이동하면 같은 커밋에서 이 표와 하단 합계를 고친다.
|
||||
- 어댑터→커널 import는 **파일 직접 경로를 유지**한다. `scripts/check-adapter-inventory.ts:63-83`이 4개 소비자에게 `platform/abortable-operation.ts`로 해석되는 specifier를 정규식으로 요구한다.
|
||||
- `src/adapters/storage/index.ts`는 `indexeddb/`·`opfs/` 서브배럴을 **재수출하지 않는다.** `scripts/test-browser-file-storage-runtime-removal.ts:23-34`가 그 두 폴더만 삭제한 뒤 잔존 import를 예외로 잡는다.
|
||||
- `src/adapters/service-worker/index.ts`는 `service-worker-entry.ts`를 참조하지 않는다. `tsconfig.app.json:18`이 제외한 파일이다.
|
||||
- 번들 예산: `config/performance/budgets.json`의 `bundle.initialJsGzipBytes = 204800`.
|
||||
- 프로젝트 소스는 TypeScript/TSX만. `allowJs` 비활성. 로컬 import는 명시적 `.ts`/`.tsx` 확장자 필수.
|
||||
- 커밋 메시지 끝에 붙일 것:
|
||||
`Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>`
|
||||
|
||||
---
|
||||
|
||||
### Task 1: 배럴 8개 생성 + INVENTORY 등록
|
||||
|
||||
없는 8개 그룹에 `index.ts`를 만든다. 이 태스크는 **파일 추가만** 한다 — 기존 import는 건드리지 않으므로 런타임 동작이 바뀌지 않는다.
|
||||
|
||||
**Files:**
|
||||
- Create: `src/adapters/auth/index.ts`
|
||||
- Create: `src/adapters/diagnostics/index.ts`
|
||||
- Create: `src/adapters/telemetry/index.ts`
|
||||
- Create: `src/adapters/platform/index.ts`
|
||||
- Create: `src/adapters/query-cache/index.ts`
|
||||
- Create: `src/adapters/service-worker/index.ts`
|
||||
- Create: `src/adapters/storage/index.ts`
|
||||
- Create: `src/adapters/http/index.ts`
|
||||
- Modify: `docs/reviews/adapters/INVENTORY.md` (행 8개 추가 + 합계)
|
||||
|
||||
**Interfaces:**
|
||||
- Produces: 위 8개 배럴이 내보내는 심볼 82개. Task 2가 이 경로들로 import를 바꾼다. 심볼 존재는 spec §7에서 82/82 기계 대조 완료(누락 0, 오타 0).
|
||||
- Consumes: 없음 (첫 태스크)
|
||||
|
||||
- [ ] **Step 1: 배럴 5개 생성 (단일 파일 그룹 + query-cache)**
|
||||
|
||||
`src/adapters/auth/index.ts`:
|
||||
```ts
|
||||
export {
|
||||
createAnonymousSessionAdapter,
|
||||
createDemoSessionAdapter,
|
||||
createExternalAuthSessionAdapter,
|
||||
createUnavailableSessionAdapter,
|
||||
DEMO_AUTHORIZATION_MARKER,
|
||||
type DemoSessionAdapter,
|
||||
type ExternalSessionOwner,
|
||||
} from "./external-session-adapter.ts";
|
||||
```
|
||||
|
||||
`src/adapters/diagnostics/index.ts`:
|
||||
```ts
|
||||
export {
|
||||
createDiagnosticsAdapter,
|
||||
getLastBootEvidence,
|
||||
MAX_DIAGNOSTIC_ENTRIES,
|
||||
noOpDiagnostics,
|
||||
recordBootFailure,
|
||||
} from "./bounded-diagnostics.ts";
|
||||
```
|
||||
|
||||
`src/adapters/telemetry/index.ts`:
|
||||
```ts
|
||||
export {
|
||||
createTelemetryAdapter,
|
||||
MAX_TELEMETRY_QUEUE,
|
||||
noOpTelemetry,
|
||||
type TelemetryAdapter,
|
||||
type TelemetryAdapterOptions,
|
||||
} from "./best-effort-telemetry.ts";
|
||||
```
|
||||
|
||||
`src/adapters/storage/index.ts`:
|
||||
```ts
|
||||
/**
|
||||
* 최상위 storage 배럴은 Web Storage 어댑터만 내보낸다.
|
||||
*
|
||||
* `./indexeddb/index.ts`와 `./opfs/index.ts`를 여기서 재수출하지 말 것.
|
||||
* `scripts/test-browser-file-storage-runtime-removal.ts:23-34`가 그 두 폴더만
|
||||
* 삭제한 뒤 잔존 import를 예외로 잡는다 — 재수출하면 제거 드릴이 죽는다.
|
||||
* 두 서브배럴이 각자 제거 가능한 런타임의 경계다.
|
||||
*/
|
||||
export {
|
||||
createBrowserStorageAdapter,
|
||||
type BrowserStorageDependencies,
|
||||
} from "./browser-storage-adapter.ts";
|
||||
```
|
||||
|
||||
`src/adapters/query-cache/index.ts`:
|
||||
```ts
|
||||
export {
|
||||
createConditionalValidatorStore,
|
||||
type ConditionalValidatorBinding,
|
||||
type ConditionalValidatorStore,
|
||||
} from "./conditional-validator-store.ts";
|
||||
export { createCursorPaginationRuntime } from "./cursor-pagination-runtime.ts";
|
||||
export {
|
||||
createServerStateScopeRuntime,
|
||||
type ScopeResetParticipant,
|
||||
type ServerStateScopeDependencies,
|
||||
} from "./server-state-scope-runtime.ts";
|
||||
export {
|
||||
createTanStackCacheCoordinator,
|
||||
type TanStackCacheCoordinatorDependencies,
|
||||
} from "./tanstack-cache-coordinator.ts";
|
||||
export {
|
||||
createQueryCacheAdapter,
|
||||
createQueryClient,
|
||||
QUERY_CACHE_DEFAULTS,
|
||||
type QueryCacheDependencies,
|
||||
} from "./tanstack-query-cache.ts";
|
||||
```
|
||||
|
||||
- [ ] **Step 2: 배럴 3개 생성 (주석이 계약인 것들)**
|
||||
|
||||
`src/adapters/platform/index.ts`:
|
||||
```ts
|
||||
/**
|
||||
* 어댑터 커널의 공개 경계.
|
||||
*
|
||||
* `src/adapters/**` 안에서는 이 배럴을 쓰지 않는다. 커널 프리미티브는 파일
|
||||
* 경로로 직접 import한다 — `scripts/check-adapter-inventory.ts:63-83`이
|
||||
* `platform/abortable-operation.ts`로 해석되는 specifier를 네 소비자에게
|
||||
* 요구하고, `.dependency-cruiser.json`의 kernel carve-out도 폴더 단위다.
|
||||
* 이 배럴은 bootstrap·features·tests 같은 그룹 바깥 소비자를 위한 문이다.
|
||||
*/
|
||||
export {
|
||||
compensateLateHandle,
|
||||
createAbortableOperation,
|
||||
snapshotAbortTimers,
|
||||
type AbortableOperation,
|
||||
type AbortableOperationInput,
|
||||
type AbortRace,
|
||||
type AbortTerminalReason,
|
||||
type AbortTimerSnapshot,
|
||||
} from "./abortable-operation.ts";
|
||||
export { assertBoundedCapacity } from "./bounded-capacity.ts";
|
||||
export {
|
||||
createBrowserLifecycleRuntime,
|
||||
type BrowserLifecycleEvent,
|
||||
type BrowserLifecycleRuntime,
|
||||
type BrowserLifecycleSnapshot,
|
||||
} from "./browser-lifecycle.ts";
|
||||
export {
|
||||
createBrowserMutationIntentFactory,
|
||||
type BrowserMutationIntentFactoryDependencies,
|
||||
} from "./browser-mutation-intent-factory.ts";
|
||||
export { systemClock } from "./system-clock.ts";
|
||||
```
|
||||
|
||||
`src/adapters/service-worker/index.ts`:
|
||||
```ts
|
||||
/**
|
||||
* `service-worker-entry.ts`는 여기서 참조하지 않는다. `tsconfig.app.json:18`이
|
||||
* 제외한 파일이라 참조하면 app 타입체크가 제외 대상을 끌어들인다. 그 파일은
|
||||
* export가 0개이므로 넣을 것도 없다.
|
||||
*/
|
||||
export {
|
||||
createServiceWorkerRuntime,
|
||||
type WorkerClientLike,
|
||||
type WorkerRuntimeConfig,
|
||||
type WorkerScopeLike,
|
||||
} from "./service-worker-lifecycle.ts";
|
||||
export {
|
||||
createServiceWorkerPageController,
|
||||
type ActivationBlocker,
|
||||
type PageControllerDependencies,
|
||||
} from "./service-worker-page-controller.ts";
|
||||
export {
|
||||
createNonceRegistry,
|
||||
createServiceWorkerMessage,
|
||||
parseServiceWorkerMessage,
|
||||
type ParsedMessage,
|
||||
} from "./service-worker-protocol.ts";
|
||||
```
|
||||
|
||||
`src/adapters/http/index.ts`:
|
||||
```ts
|
||||
/**
|
||||
* §7–§8. 권장 경로는 V3 계약 실행기(`createContractHttpExecutor`)다. 설치된
|
||||
* 계약과 타입 입력을 받아 상한·전체 데드라인·재시도 권한·효과 확실성 판정을
|
||||
* 런타임이 소유한다.
|
||||
*/
|
||||
export {
|
||||
createContractHttpExecutor,
|
||||
type AuthIntegrationFailureReason,
|
||||
type AuthOperationContext,
|
||||
type CancellationOwner,
|
||||
type ContractHttpExecutor,
|
||||
type ContractHttpExecutorDependencies,
|
||||
type HttpContractViolation,
|
||||
type HttpContractViolationKind,
|
||||
type HttpEffectCertainty,
|
||||
type HttpExecutionContext,
|
||||
type HttpExecutionObservation,
|
||||
type HttpExecutionOutcome,
|
||||
type HttpTransportFailure,
|
||||
type SafeResponseMetadata,
|
||||
} from "./http-execution-v3.ts";
|
||||
/** V3 `attachCredentials` 콜백이 반환해야 하는 결과 타입. */
|
||||
export type { CredentialPatchOutcome } from "./http-contract-bridge.ts";
|
||||
/**
|
||||
* V2 legacy. operationId + `LegacyHttpInput`으로 호출하는 범용 클라이언트다.
|
||||
* 새 코드는 위의 V3 실행기를 쓴다. 남아 있는 이유는 계약이 아직 없는
|
||||
* 오퍼레이션을 위한 이행 경로이기 때문이다.
|
||||
*/
|
||||
export {
|
||||
createHttpClient,
|
||||
type HttpClient,
|
||||
type HttpClientDependencies,
|
||||
type HttpFailure,
|
||||
type HttpResult,
|
||||
type LegacyHttpInput,
|
||||
type Scheduler,
|
||||
} from "./client.ts";
|
||||
/** V2 `HttpClient.execute`의 첫 인자 타입. */
|
||||
export type { OperationRequestInput } from "./request-builder.ts";
|
||||
```
|
||||
|
||||
- [ ] **Step 3: 타입체크로 심볼 존재를 검증한다**
|
||||
|
||||
Run: `corepack pnpm check:types:app`
|
||||
Expected: PASS. 실패하면 존재하지 않는 심볼을 재수출한 것이다 — 에러가 지목한 이름을 해당 소스 파일에서 확인하고 배럴에서 빼라. spec §7의 대조표와 대조할 것.
|
||||
|
||||
- [ ] **Step 4: 게이트가 새 파일 8개를 거부하는 것을 확인한다 (의도된 실패)**
|
||||
|
||||
Run: `git add -A && corepack pnpm check:adapter-inventory`
|
||||
Expected: **FAIL.** `git ls-files src/adapters`가 128개를 보고하는데 INVENTORY.md는 120행이므로 불일치를 보고한다. 이 실패를 본 뒤 Step 5로 간다. (실패하지 않으면 `git add`가 안 된 것이다.)
|
||||
|
||||
- [ ] **Step 5: INVENTORY.md에 행 8개 추가**
|
||||
|
||||
`docs/reviews/adapters/INVENTORY.md`의 표에 경로 알파벳 순서 위치로 끼워 넣고 번호를 다시 매긴다. 상세 리뷰 링크는 같은 그룹의 기존 행과 동일하게 쓴다.
|
||||
|
||||
| 추가할 경로 | 상세 리뷰 링크 |
|
||||
|---|---|
|
||||
| `src/adapters/auth/index.ts` | `[Network/state](./01-network-and-state.md)` |
|
||||
| `src/adapters/diagnostics/index.ts` | `[Network/state](./01-network-and-state.md)` |
|
||||
| `src/adapters/http/index.ts` | `[Network/state](./01-network-and-state.md)` |
|
||||
| `src/adapters/platform/index.ts` | `[Network/state](./01-network-and-state.md)` |
|
||||
| `src/adapters/query-cache/index.ts` | `[Network/state](./01-network-and-state.md)` |
|
||||
| `src/adapters/service-worker/index.ts` | `[Worker/push](./05-service-worker-and-web-push.md)` |
|
||||
| `src/adapters/storage/index.ts` | `[Storage/files](./03-storage-and-browser-files.md)` |
|
||||
| `src/adapters/telemetry/index.ts` | `[Network/state](./01-network-and-state.md)` |
|
||||
|
||||
그리고 `docs/reviews/adapters/INVENTORY.md:132`의 `합계: **120/120**` → `합계: **128/128**`.
|
||||
|
||||
- [ ] **Step 6: 게이트 3종 통과 확인**
|
||||
|
||||
Run: `corepack pnpm check:adapter-inventory && corepack pnpm check:types:app && corepack pnpm check:architecture`
|
||||
Expected: 셋 다 PASS. `check:adapter-inventory`가 `128 files PASS`를 출력한다.
|
||||
|
||||
- [ ] **Step 7: 워커 realm 타입체크 — 실행 중 발견한 필수 단계**
|
||||
|
||||
> 2026-09-16 실행 중 발견. 계획 초안에는 없었다.
|
||||
> `tsconfig.service-worker.json`은 `src/adapters/service-worker` 폴더를 통째로
|
||||
> WebWorker lib로 컴파일하면서 페이지 realm 파일 2개만 `exclude`로 뺀다.
|
||||
> 새 `index.ts`가 그 폴더 안에서 `service-worker-page-controller.ts`를
|
||||
> import하므로, 제외하지 않으면 페이지 realm 파일이 WebWorker lib 컴파일에
|
||||
> 끌려 들어와 `Cannot find name 'document'`로 실패한다.
|
||||
> (M6 리뷰의 "realm 경계가 폴더가 아니라 tsconfig exclude 2줄로만 표현된다"가
|
||||
> 그대로 발현된 것이다.)
|
||||
|
||||
`tsconfig.service-worker.json`의 `exclude` 배열 맨 앞에 추가한다:
|
||||
|
||||
```json
|
||||
"src/adapters/service-worker/index.ts",
|
||||
```
|
||||
|
||||
타입 커버리지는 `tsconfig.app.json`이 이 배럴을 포함하므로 유지된다. 워커 진입점
|
||||
`service-worker-entry.ts`는 배럴을 쓰지 않고 파일을 직접 import하므로 워커 번들에
|
||||
영향이 없다.
|
||||
|
||||
Run: `corepack pnpm check:types:service-worker`
|
||||
Expected: PASS
|
||||
|
||||
- [ ] **Step 8: 제거 드릴이 살아 있는지 확인한다**
|
||||
|
||||
`storage/index.ts`를 새로 만들었으므로 제거 드릴을 돌려 서브폴더 삭제가 여전히 성립하는지 본다.
|
||||
|
||||
Run: `corepack pnpm test:browser-file-storage-removal`
|
||||
Expected: 드릴 출력에 `error TS`가 0건이어야 한다. `storage/index.ts`가 `indexeddb/`나 `opfs/`를 참조하면 `still imported`로 죽는다 — Step 1의 주석대로 재수출을 제거하라.
|
||||
|
||||
> **이 환경의 알려진 제약.** `tests/unit/ci-artifact-contract.test.ts`의 16건은
|
||||
> `bwrap: loopback: Failed RTM_NEWADDR: Operation not permitted`로 실패한다.
|
||||
> 샌드박스가 네트워크 네임스페이스를 못 만들기 때문이며 `develop` `5434760`
|
||||
> 기준선에서도 동일하게 16건 실패한다(2026-09-16 확인). 이 드릴은 내부적으로
|
||||
> `test:unit`을 돌리므로 그 16건 때문에 항상 exit 1이 된다.
|
||||
> **판정 기준은 드릴의 exit code가 아니라 `error TS` 0건과 `still imported` 부재다.**
|
||||
> CI 환경에서는 전체 PASS를 확인할 것.
|
||||
|
||||
- [ ] **Step 9: 커밋**
|
||||
|
||||
```bash
|
||||
git add src/adapters/*/index.ts docs/reviews/adapters/INVENTORY.md tsconfig.service-worker.json
|
||||
git commit -m "$(cat <<'EOF'
|
||||
feat: give every adapter group a public barrel
|
||||
|
||||
각 어댑터 그룹의 공개 표면을 index.ts로 선언한다. 소비자는 아직 바꾸지
|
||||
않았으므로 런타임 동작은 그대로다. storage 배럴은 서브배럴을 재수출하지
|
||||
않는다 — 제거 드릴이 indexeddb/와 opfs/만 삭제하기 때문이다.
|
||||
|
||||
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||||
EOF
|
||||
)"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 2: 그룹 바깥 소비자 15줄을 배럴 경로로 치환
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/bootstrap/runtime-adapters.ts:6-29` (12줄, 블록 병합 포함)
|
||||
- Modify: `src/bootstrap/main.tsx:3`
|
||||
- Modify: `src/bootstrap/optional-runtime-host.ts:4`
|
||||
- Modify: `src/bootstrap/register-service-worker.ts:5`
|
||||
- Modify: `src/features/reference-feature/adapters/create-reference-feature-input.ts:9`
|
||||
- Modify: `.storybook/preview.tsx:5-6` (게이트 범위 밖, 일관성 목적)
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: Task 1이 만든 8개 배럴의 심볼 82개
|
||||
- Produces: `src/` 안에 어댑터 내부 파일을 직접 겨누는 import 0건. Task 3의 게이트 규칙이 이 상태를 전제로 통과한다.
|
||||
|
||||
- [ ] **Step 1: 치환 전 위반 건수를 기록한다**
|
||||
|
||||
Run:
|
||||
```bash
|
||||
grep -rn 'from "[^"]*adapters/[^"]*"' src/ | grep -v '^src/adapters/' | grep -v 'index\.ts"' | wc -l
|
||||
```
|
||||
Expected: `15`. 이 숫자가 Step 4에서 `0`이 되어야 한다.
|
||||
|
||||
- [ ] **Step 2: `runtime-adapters.ts`의 import 블록을 다시 쓴다**
|
||||
|
||||
`src/bootstrap/runtime-adapters.ts`의 `:6`~`:29` 구간이 한 덩어리다. 아래에서 위로 편집하거나 블록 전체를 한 번에 교체한다 — 위에서부터 고치면 줄 번호가 밀린다.
|
||||
|
||||
치환 내용:
|
||||
|
||||
| 현재 specifier | 바뀔 specifier |
|
||||
|---|---|
|
||||
| `../adapters/auth/external-session-adapter.ts` | `../adapters/auth/index.ts` |
|
||||
| `../adapters/diagnostics/bounded-diagnostics.ts` | `../adapters/diagnostics/index.ts` |
|
||||
| `../adapters/http/client.ts` | `../adapters/http/index.ts` |
|
||||
| `../adapters/http/http-execution-v3.ts` | `../adapters/http/index.ts` (위와 **한 블록으로 병합**) |
|
||||
| `../adapters/query-cache/tanstack-cache-coordinator.ts` | `../adapters/query-cache/index.ts` |
|
||||
| `../adapters/query-cache/tanstack-query-cache.ts` | `../adapters/query-cache/index.ts` |
|
||||
| `../adapters/query-cache/server-state-scope-runtime.ts` | `../adapters/query-cache/index.ts` |
|
||||
| `../adapters/query-cache/conditional-validator-store.ts` | `../adapters/query-cache/index.ts` (위 넷을 **한 블록으로 병합**) |
|
||||
| `../adapters/storage/browser-storage-adapter.ts` | `../adapters/storage/index.ts` |
|
||||
| `../adapters/platform/browser-mutation-intent-factory.ts` | `../adapters/platform/index.ts` |
|
||||
| `../adapters/telemetry/best-effort-telemetry.ts` | `../adapters/telemetry/index.ts` |
|
||||
|
||||
`../adapters/cross-context-invalidation/index.ts` 2줄은 이미 배럴이므로 **건드리지 않는다.**
|
||||
|
||||
- [ ] **Step 3: 나머지 4개 파일을 치환한다**
|
||||
|
||||
| 파일:줄 | 현재 | 바뀔 것 |
|
||||
|---|---|---|
|
||||
| `src/bootstrap/main.tsx:3` | `../adapters/diagnostics/bounded-diagnostics.ts` | `../adapters/diagnostics/index.ts` |
|
||||
| `src/bootstrap/optional-runtime-host.ts:4` | `../adapters/platform/browser-lifecycle.ts` | `../adapters/platform/index.ts` |
|
||||
| `src/bootstrap/register-service-worker.ts:5` | `../adapters/service-worker/service-worker-page-controller.ts` | `../adapters/service-worker/index.ts` |
|
||||
| `src/features/reference-feature/adapters/create-reference-feature-input.ts:9` | `../../../adapters/http/http-execution-v3.ts` | `../../../adapters/http/index.ts` |
|
||||
| `.storybook/preview.tsx:5` | `../src/adapters/auth/external-session-adapter.ts` | `../src/adapters/auth/index.ts` |
|
||||
| `.storybook/preview.tsx:6` | `../src/adapters/query-cache/tanstack-query-cache.ts` | `../src/adapters/query-cache/index.ts` |
|
||||
|
||||
- [ ] **Step 4: 위반이 0이 된 것을 확인한다**
|
||||
|
||||
Run:
|
||||
```bash
|
||||
grep -rn 'from "[^"]*adapters/[^"]*"' src/ | grep -v '^src/adapters/' | grep -v 'index\.ts"' | wc -l
|
||||
```
|
||||
Expected: `0`
|
||||
|
||||
- [ ] **Step 5: 타입·린트·아키텍처 게이트**
|
||||
|
||||
Run: `corepack pnpm check:types:app && corepack pnpm lint && corepack pnpm check:architecture`
|
||||
Expected: 셋 다 PASS.
|
||||
|
||||
- [ ] **Step 6: 번들 예산을 확인한다 — 이 태스크의 최대 위험**
|
||||
|
||||
배럴 재수출이 트리셰이킹을 무력화하면 초기 청크가 커진다. `package.json`에 `sideEffects` 선언이 없어 번들러가 모든 모듈을 부작용 있는 것으로 본다.
|
||||
|
||||
Run: `corepack pnpm check:bundle`
|
||||
Expected: PASS (`bundle.initialJsGzipBytes` 상한 204800).
|
||||
|
||||
**FAIL한 경우 순서대로 시도한다:**
|
||||
1. `package.json`에 `"sideEffects": false`를 추가한다. 추가 전에 `src/adapters` 아래 최상위 부작용이 있는지 확인하고, CSS import가 있으면 `"sideEffects": ["*.css"]` 형태로 보존한다.
|
||||
2. 그래도 넘치면 `src/adapters/service-worker/index.ts`에서 `service-worker-lifecycle.ts` 블록을 빼고, 워커 realm은 파일 직접 import를 유지한다. 그리고 Task 3의 게이트 규칙 `from.pathNot`에 워커 진입점을 추가한다. (근거: realm이 다르면 문도 다르다 — `platform`과 같은 논리.)
|
||||
3. 1·2로 안 되면 이 태스크를 중단하고 보고한다. 예산 초과를 안고 진행하지 않는다.
|
||||
|
||||
- [ ] **Step 7: 단위·통합 테스트**
|
||||
|
||||
Run: `corepack pnpm test:unit && corepack pnpm test:integration`
|
||||
Expected: PASS. 이 태스크는 import 경로만 바꿨으므로 테스트 결과가 달라질 이유가 없다. 깨지면 배럴이 내보내는 심볼이 원본과 다른 것이다.
|
||||
|
||||
- [ ] **Step 8: 커밋**
|
||||
|
||||
```bash
|
||||
git add src/bootstrap src/features .storybook
|
||||
git commit -m "$(cat <<'EOF'
|
||||
refactor: reach adapter groups through their barrel
|
||||
|
||||
bootstrap과 feature 어댑터가 어댑터 내부 파일을 직접 겨누던 15곳을 그룹
|
||||
배럴로 바꾼다. 커널(platform/**) 간선과 그룹 내부 import는 파일 경로를
|
||||
유지한다 — check:adapter-inventory가 그 경로를 직접 검증한다.
|
||||
|
||||
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||||
EOF
|
||||
)"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 3: 게이트 규칙 + 회귀 fixture
|
||||
|
||||
규칙만 추가하면 다음 사람이 규칙을 지워도 아무도 모른다. 이 레포는 거부 규칙마다 회귀 fixture를 두는 방식(`docs/architecture/layers.md`)이므로 fixture까지 같이 넣는다.
|
||||
|
||||
**Files:**
|
||||
- Modify: `.dependency-cruiser.json` (`:193`과 `:194` 사이에 규칙 1개)
|
||||
- Create: `tests/fixtures/architecture/dependency-graph/barrel/bootstrap/compose-deep.ts`
|
||||
- Create: `tests/fixtures/architecture/dependency-graph/barrel/bootstrap/compose-barrel.ts`
|
||||
- Create: `tests/fixtures/architecture/dependency-graph/barrel/adapters/http/client.ts`
|
||||
- Create: `tests/fixtures/architecture/dependency-graph/barrel/adapters/http/index.ts`
|
||||
- Modify: `scripts/check-architecture.ts` (`runGraphFixtureChecks`에 그래프 1개 + assertion 2개)
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: Task 2가 만든 "위반 0건" 상태. 위반이 남아 있으면 이 규칙 추가가 곧바로 게이트를 깬다.
|
||||
- Produces: `adapter-groups-are-reached-through-their-barrel` 규칙과 그 회귀 fixture 2종
|
||||
|
||||
- [ ] **Step 1: fixture 트리를 만든다 (규칙보다 먼저 — 실패를 먼저 본다)**
|
||||
|
||||
`tests/fixtures/architecture/dependency-graph/barrel/adapters/http/client.ts`:
|
||||
```ts
|
||||
export function createFixtureHttpClient(): string {
|
||||
return "fixture";
|
||||
}
|
||||
```
|
||||
|
||||
`tests/fixtures/architecture/dependency-graph/barrel/adapters/http/index.ts`:
|
||||
```ts
|
||||
export { createFixtureHttpClient } from "./client.ts";
|
||||
```
|
||||
|
||||
`tests/fixtures/architecture/dependency-graph/barrel/bootstrap/compose-deep.ts` — 규칙이 **잡아야 할** 형태:
|
||||
```ts
|
||||
import { createFixtureHttpClient } from "../adapters/http/client.ts";
|
||||
|
||||
export const deepComposition = createFixtureHttpClient;
|
||||
```
|
||||
|
||||
`tests/fixtures/architecture/dependency-graph/barrel/bootstrap/compose-barrel.ts` — 규칙이 **통과시켜야 할** 형태:
|
||||
```ts
|
||||
import { createFixtureHttpClient } from "../adapters/http/index.ts";
|
||||
|
||||
export const barrelComposition = createFixtureHttpClient;
|
||||
```
|
||||
|
||||
> fixture는 `analyzeSourceGraph(dir, "src")`로 분석되어 경로가 `src/...`로 보고된다(`scripts/check-architecture.ts:821-833`). 그래서 `^src/`로 시작하는 규칙이 fixture 트리에 그대로 적용된다.
|
||||
|
||||
- [ ] **Step 2: `.dependency-cruiser.json`에 규칙을 추가한다**
|
||||
|
||||
`forbidden` 배열의 `adapters-do-not-know-other-concrete-adapters` **바로 다음**, `no-circular-dependencies` **앞**에 넣는다.
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "adapter-groups-are-reached-through-their-barrel",
|
||||
"comment": "어댑터 그룹의 공개 표면은 그 그룹의 index.ts다. 그룹 바깥(bootstrap, features, presentation)은 배럴만 import한다. 배럴이 없던 시절 bootstrap은 어댑터 내부 파일 15곳을 직접 겨눴고, 그래서 어떤 파일이 공개이고 어떤 파일이 내부 헬퍼인지 아무 데도 적혀 있지 않았다. 출발점에서 src/adapters를 뺀 이유는 어댑터끼리의 간선은 바로 위 adapters-do-not-know-other-concrete-adapters가 이미 담당하고, 커널(platform/**)은 파일 단위로 공유되기 때문이다 — scripts/check-adapter-inventory.ts가 네 소비자에게 platform/abortable-operation.ts로 해석되는 specifier를 직접 요구한다. 도착점에서 1단계 중첩 index.ts를 허용한 이유는 storage/indexeddb와 storage/opfs가 각자 독립적으로 제거 가능한 런타임이고(scripts/test-browser-file-storage-runtime-removal.ts), 그래서 각자의 배럴이 곧 경계이기 때문이다.",
|
||||
"severity": "error",
|
||||
"from": {
|
||||
"path": "^src/",
|
||||
"pathNot": "^src/adapters/"
|
||||
},
|
||||
"to": {
|
||||
"path": "^src/adapters/[^/]+/",
|
||||
"pathNot": "^src/adapters/[^/]+/(?:[^/]+/)?index\\.ts$"
|
||||
}
|
||||
},
|
||||
```
|
||||
|
||||
> 규칙 형태 적합성: `scripts/check-architecture.ts:799-815`의 `validateArchitectureRules`는 `from`에 `path`/`pathNot`, `to`에 `path`/`pathNot`/`circular`만 허용한다. 정규식은 `new RegExp(pattern, "u")`로 평가되므로 비캡처 그룹 `(?:...)`이 허용된다. `$1` 역참조는 쓰지 않았다.
|
||||
|
||||
- [ ] **Step 3: 규칙이 실제 소스에서 위반 0인지 확인한다**
|
||||
|
||||
Run: `corepack pnpm check:architecture`
|
||||
Expected: PASS. FAIL하면 Task 2에서 놓친 import가 있다는 뜻이다 — 출력이 지목한 파일을 배럴 경로로 고쳐라.
|
||||
|
||||
- [ ] **Step 4: fixture 검사를 `check-architecture.ts`에 배선한다**
|
||||
|
||||
`runGraphFixtureChecks()`의 `Promise.all` 블록(`scripts/check-architecture.ts:826-833`)에 `barrelGraph`를 추가한다:
|
||||
|
||||
```ts
|
||||
const [allowedGraph, unresolvedGraph, layerGraph, cycleGraph, barrelGraph] =
|
||||
await Promise.all([
|
||||
analyzeSourceGraph(allowedRoot, "src"),
|
||||
analyzeSourceGraph(resolve(fixtureRoot, "unresolved"), "src"),
|
||||
analyzeSourceGraph(resolve(fixtureRoot, "layer"), "src"),
|
||||
analyzeSourceGraph(resolve(fixtureRoot, "cycle"), "src"),
|
||||
analyzeSourceGraph(resolve(fixtureRoot, "barrel"), "src"),
|
||||
]);
|
||||
```
|
||||
|
||||
그리고 `assertions` 배열에 두 항목을 추가한다:
|
||||
|
||||
```ts
|
||||
{
|
||||
name: "deep adapter import from outside the group is rejected",
|
||||
passed: blockingViolations(barrelGraph).some(
|
||||
({ rule, source, target }) =>
|
||||
rule === "adapter-groups-are-reached-through-their-barrel" &&
|
||||
source === "src/bootstrap/compose-deep.ts" &&
|
||||
target === "src/adapters/http/client.ts",
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "barrel import from outside the group is accepted",
|
||||
passed: !blockingViolations(barrelGraph).some(
|
||||
({ source }) => source === "src/bootstrap/compose-barrel.ts",
|
||||
),
|
||||
},
|
||||
```
|
||||
|
||||
> 필드명 근거: `ArchitectureViolation`은 `rule` / `severity` / `source` / `target`을 갖는다 (`scripts/check-architecture.ts:35`, 생성부 `:734-739`). `from`/`to`가 아니다.
|
||||
|
||||
- [ ] **Step 5: fixture 회귀 검사가 통과하는지 확인한다**
|
||||
|
||||
Run: `corepack pnpm check:architecture`
|
||||
Expected: `Architecture graph fixtures: 14 regression checks PASS` (기존 12 + 신규 2). 그리고 전체 PASS.
|
||||
|
||||
- [ ] **Step 6: fixture가 실제로 무언가를 잡는지 역검증한다**
|
||||
|
||||
규칙을 잠시 무력화해서 fixture가 FAIL하는지 본다. fixture가 항상 통과하면 회귀 검사가 아니다.
|
||||
|
||||
```bash
|
||||
# 규칙 이름을 일시적으로 바꿔 매칭되지 않게 한다
|
||||
sed -i 's/"adapter-groups-are-reached-through-their-barrel"/"temporarily-disabled-barrel-rule"/' .dependency-cruiser.json
|
||||
corepack pnpm check:architecture; echo "EXIT=$?"
|
||||
# 되돌린다
|
||||
sed -i 's/"temporarily-disabled-barrel-rule"/"adapter-groups-are-reached-through-their-barrel"/' .dependency-cruiser.json
|
||||
corepack pnpm check:architecture; echo "EXIT=$?"
|
||||
```
|
||||
Expected: 첫 번째 EXIT는 0이 아니어야 하고(assertion 실패), 되돌린 뒤 EXIT는 0이어야 한다.
|
||||
|
||||
- [ ] **Step 7: 커밋**
|
||||
|
||||
```bash
|
||||
git add .dependency-cruiser.json scripts/check-architecture.ts tests/fixtures/architecture/dependency-graph/barrel
|
||||
git commit -m "$(cat <<'EOF'
|
||||
feat: enforce the adapter barrel boundary in the architecture gate
|
||||
|
||||
그룹 바깥에서 어댑터 내부 파일을 직접 import하면 check:architecture가
|
||||
거부한다. 회귀 fixture 2개가 거부와 허용을 각각 고정한다 — 규칙을 지우면
|
||||
fixture 검사가 먼저 깨진다.
|
||||
|
||||
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||||
EOF
|
||||
)"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 4: 규칙을 문서에 명문화
|
||||
|
||||
코드로 강제되는 규칙이 문서에 없으면 다음 사람은 게이트 에러를 보고서야 규칙을 알게 된다.
|
||||
|
||||
**Files:**
|
||||
- Modify: `docs/architecture/layers.md` (배럴 경계 절 추가)
|
||||
- Modify: `docs/reviews/adapters/README.md` (테스트 이관 규칙)
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: Task 3의 규칙 이름 `adapter-groups-are-reached-through-their-barrel`
|
||||
- Produces: 없음 (문서만)
|
||||
|
||||
- [ ] **Step 1: `layers.md`에 배럴 경계 절을 추가한다**
|
||||
|
||||
"The adapter kernel" 절 **다음에** 아래를 넣는다:
|
||||
|
||||
```markdown
|
||||
## 어댑터 그룹의 공개 경계
|
||||
|
||||
각 어댑터 그룹의 공개 표면은 그 그룹의 `index.ts`다. 그룹 바깥
|
||||
(`bootstrap`, `features`, `presentation`)은 배럴만 import한다.
|
||||
`adapter-groups-are-reached-through-their-barrel` 규칙이 이를 강제하고,
|
||||
`tests/fixtures/architecture/dependency-graph/barrel`이 거부와 허용을
|
||||
각각 고정한다.
|
||||
|
||||
두 가지 예외가 있고 둘 다 의도된 것이다.
|
||||
|
||||
- **그룹 내부 파일끼리**는 파일 경로로 직접 import한다. 배럴은 바깥을 위한
|
||||
문이지 내부 규율이 아니다.
|
||||
- **어댑터 → 커널(`platform/**`)** 간선도 파일 경로를 유지한다. 커널은
|
||||
런타임 합성물이 아니라 프리미티브이고, `check:adapter-inventory`가 네
|
||||
소비자에게 `platform/abortable-operation.ts`로 해석되는 specifier를
|
||||
직접 요구한다. 커널 배럴(`platform/index.ts`)은 bootstrap과 테스트를
|
||||
위한 것이다.
|
||||
|
||||
`storage`는 최상위 배럴이 `indexeddb/`·`opfs/` 서브배럴을 재수출하지
|
||||
않는다. 두 런타임이 각자 독립적으로 제거 가능하고
|
||||
(`test:browser-file-storage-removal`), 그래서 각 서브배럴이 곧 경계다.
|
||||
규칙의 도착점 정규식이 1단계 중첩 `index.ts`를 배럴로 인정하는 이유가
|
||||
이것이다.
|
||||
```
|
||||
|
||||
- [ ] **Step 2: 테스트 이관 규칙을 적는다**
|
||||
|
||||
`docs/reviews/adapters/README.md` 끝에 추가:
|
||||
|
||||
```markdown
|
||||
## 테스트의 어댑터 import
|
||||
|
||||
`tests/` 아래 어댑터 import는 배럴로 일괄 이관하지 않는다. `check:architecture`는
|
||||
`src`만 스캔하므로 강제되지 않고, 단위 테스트의 상당수가 배럴에 없는 내부
|
||||
심볼을 의도적으로 겨눈다.
|
||||
|
||||
어떤 테스트 파일을 **다른 이유로** 수정하거나 분할할 때, 그 파일이 쓰는
|
||||
심볼이 해당 그룹 배럴에 있으면 그 파일 안에서만 배럴 경로로 바꾼다.
|
||||
배럴에 없는 심볼이면 깊은 경로를 유지한다. 배럴에 추가하고 싶으면 그 심볼이
|
||||
공개 표면임을 먼저 논증한다 — 테스트 편의로 배럴을 키우면 배럴이 경계가
|
||||
아니라 재수출 덤프가 된다.
|
||||
```
|
||||
|
||||
- [ ] **Step 3: 문서 게이트 확인**
|
||||
|
||||
Run: `corepack pnpm lint && corepack pnpm check:architecture`
|
||||
Expected: PASS. (문서 링크 검증이 있으면 `corepack pnpm verify:documentation`도 돌린다.)
|
||||
|
||||
- [ ] **Step 4: 커밋**
|
||||
|
||||
```bash
|
||||
git add docs/architecture/layers.md docs/reviews/adapters/README.md
|
||||
git commit -m "$(cat <<'EOF'
|
||||
docs: write down the adapter barrel boundary and its two exceptions
|
||||
|
||||
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||||
EOF
|
||||
)"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 완료 판정
|
||||
|
||||
네 태스크가 끝나면 아래가 전부 PASS여야 한다.
|
||||
|
||||
```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
|
||||
corepack pnpm test:integration
|
||||
corepack pnpm test:browser-file-storage-removal
|
||||
corepack pnpm test:realtime-removal
|
||||
```
|
||||
|
||||
그리고 아래가 `0`이어야 한다.
|
||||
```bash
|
||||
grep -rn 'from "[^"]*adapters/[^"]*"' src/ | grep -v '^src/adapters/' | grep -v 'index\.ts"' | wc -l
|
||||
```
|
||||
|
||||
## 이 계획이 하지 않는 것
|
||||
|
||||
- `tests/` 166줄의 배럴 이관 — Task 4 Step 2의 규칙대로 파일을 손댈 때만 한다.
|
||||
- 대형 파일 분할 — 별도 계획. 이 계획이 그 선행조건이다.
|
||||
- spec §6이 남긴 부수 발견 4건(`telemetry:49`의 잉여 재수출, `browser-files/index.ts`의 포트 재수출, `opfs/index.ts` 누락 심볼 3개, `.dependency-cruiser.json:141`의 존재하지 않는 `web-worker` 경로) — 각각 별도 티켓.
|
||||
@@ -0,0 +1,751 @@
|
||||
# 어댑터 배럴(`index.ts`)을 공개 경계로 승격 — 설계서
|
||||
|
||||
- 대상 레포: `/home/donghyeon/workspace/desktop-server-git/clean-architecture-frontend-template`
|
||||
- 브랜치: `develop` (기준 커밋 `5434760`)
|
||||
- 전제(이미 확정): 배럴 폐지안은 기각. `src/adapters/<group>/index.ts`를 **진짜 공개 경계**로 만든다.
|
||||
- 게이트 기준선: `check:architecture` PASS, `check:types:app` PASS 유지.
|
||||
- 이 문서는 설계만 한다. 소스 수정·빌드·테스트 실행 없음.
|
||||
|
||||
---
|
||||
|
||||
## 0. 요약 (먼저 읽을 것)
|
||||
|
||||
| 항목 | 판정 |
|
||||
|---|---|
|
||||
| 배럴 표기 표준 | **명명 재수출(named re-export)**. `export *`는 "이미 명시적인 서브배럴을 합칠 때"만 허용 |
|
||||
| 새로 만들 배럴 | 8개, 합계 114줄 (auth 9 / diagnostics 7 / telemetry 7 / storage 4 / service-worker 17 / query-cache 21 / platform 22 / http 27) |
|
||||
| `platform/` | 배럴은 만든다. 단 **어댑터→커널 간선은 파일 직접 import를 유지**한다 (게이트가 그걸 요구함) |
|
||||
| `storage/` | 최상위 배럴은 **서브폴더를 재수출하지 않는다**. `indexeddb/index.ts`·`opfs/index.ts`가 곧 경계다 |
|
||||
| `http/` | V3(`createContractHttpExecutor`)가 권장 경로, V2(`createHttpClient`)는 legacy 보존. 둘 다 배럴에 넣고 주석으로 표시 |
|
||||
| 치환할 import | `src/` 15줄 (배럴 경유 2줄은 이미 합격) + `.storybook/` 2줄(선택) |
|
||||
| 게이트 | `.dependency-cruiser.json`에 규칙 1개 추가. `tests/`는 대상 아님(스캔 범위가 `src`뿐) |
|
||||
| 최대 위험 | 번들 예산. `package.json`에 `sideEffects` 선언이 없어 배럴이 초기 청크를 키울 수 있다 |
|
||||
|
||||
---
|
||||
|
||||
## 1. 기존 배럴 8개의 지배적 관례
|
||||
|
||||
읽은 파일: `src/adapters/{browser-files,browser-file-storage,browser-rpc,browser-transfer,cache-storage,cross-context-invalidation,realtime,web-push}/index.ts`
|
||||
추가로 서브배럴 8개: `browser-transfer/{image-cdn,presigned,resumable-upload}/index.ts`, `realtime/{polling,sse,websocket}/index.ts`, `storage/{indexeddb,opfs}/index.ts`
|
||||
|
||||
### 1.1 관례 (문장으로)
|
||||
|
||||
1. **소스 파일 단위로 블록을 만들고, 블록마다 `export { ... } from "./파일.ts";` 로 이름을 전부 적는다.**
|
||||
블록이 전부 타입이면 `export type { ... } from "...";` 형태를 쓴다 (`browser-files/index.ts:1`, `:28`, `:52`, `:53`).
|
||||
2. **블록 안 순서는 「값 먼저, 타입 나중」이고 각각 대소문자 무시 알파벳순이다.**
|
||||
근거: `web-push/index.ts:42-51` — `createLinkedAbortController, failureCode, nativeFailure, observeWebPush, systemTimeoutScheduler, withAbortableDeadline, type LinkedAbortController, type TimeoutScheduler`.
|
||||
상수도 값이므로 같은 줄에 섞인다: `realtime/index.ts:13-22` — `parseRetryAfterDelay, REALTIME_RECONNECT_CEILINGS, reconnectBudgetRemaining` (대소문자 무시로 `parse < realtime_ < reconnect`).
|
||||
타입은 인라인 `type X` 접두어로 쓴다 (`browser-rpc/index.ts:9-18`).
|
||||
3. **블록(파일) 순서는 대체로 알파벳순이되 엄격하지 않다.** 하위 폴더 블록은 뒤로 몰아둔다 (`web-push/index.ts:59`, `:66`의 `./inbound/*`).
|
||||
엄격하지 않은 실례: `cross-context-invalidation/index.ts`는 `browser-cross-context-invalidation.ts`(:1) 다음에 `browser-cross-context-host.ts`(:20) — 역순.
|
||||
4. **배럴은 그룹의 전체 export 목록이 아니다.** 그룹 안에 배럴에 없는 파일이 실제로 존재한다.
|
||||
근거: `src/adapters/realtime/result.ts`는 export를 가지지만 `realtime/index.ts` 어디에도 없다. `browser-files/browser-file-vault.ts`도 `browser-files/index.ts`에 없다.
|
||||
→ 즉 이 레포는 이미 "배럴 = 선별된 공개 표면"을 실천하고 있다. 새 배럴도 같은 기준으로 고르면 된다.
|
||||
5. (참고, 따라하지 말 것) `browser-files/index.ts:1-16`은 어댑터가 아니라 **application 포트 타입**을 재수출한다. 배럴이 하위 레이어의 통로가 되는 형태라 새 배럴에서는 재현하지 않는다. 필요하면 소비자가 포트에서 직접 가져오면 된다.
|
||||
|
||||
### 1.2 `export *`는 표준인가 — 판정
|
||||
|
||||
**표준은 명명 재수출이다. `export *`는 예외가 아니라 "서브배럴 합성" 전용 관용구다.**
|
||||
|
||||
근거:
|
||||
|
||||
- `export *`가 쓰인 곳은 단 두 파일, 여섯 줄이다.
|
||||
- `src/adapters/browser-transfer/index.ts:1-3` — `./image-cdn/index.ts`, `./presigned/index.ts`, `./resumable-upload/index.ts`
|
||||
- `src/adapters/realtime/index.ts:56-58` — `./polling/index.ts`, `./sse/index.ts`, `./websocket/index.ts`
|
||||
- **여섯 줄 전부 대상이 `index.ts`(서브배럴)다.** 구현 파일(`.ts`)을 `export *`로 푼 사례는 0건이다.
|
||||
- 그리고 그 서브배럴들은 자기 자신이 전부 명명 재수출이다 (`browser-transfer/image-cdn/index.ts:1-6`, `realtime/sse/index.ts:1-9` 등).
|
||||
- `realtime/index.ts`는 한 파일 안에서 두 형태를 동시에 쓴다: 1-55줄은 구현 파일에 대한 명명 재수출, 56-58줄은 서브배럴에 대한 `export *`. 즉 `browser-transfer`만 특이한 게 아니라, **대상이 서브배럴이냐 구현 파일이냐**가 형태를 가른다.
|
||||
|
||||
왜 이 구분이 옳은가: 지시대로 `export *`는 "무엇이 공개되는지 파일을 열어야 안다"는 문제가 있다. 그런데 대상이 서브배럴이면 그 파일 자체가 이미 명시적 목록이므로, `export *` 한 줄을 따라가면 곧바로 명시적 목록에 도달한다. 목록이 사라지는 게 아니라 한 단계 아래에 있는 것뿐이다. 반대로 구현 파일을 `export *`하면 목록이 어디에도 없어진다.
|
||||
|
||||
**채택 규칙**
|
||||
|
||||
> 배럴은 구현 파일(`*.ts`)에 대해서는 반드시 이름을 하나씩 적는다.
|
||||
> `export *`는 대상이 같은 그룹의 서브배럴(`*/index.ts`)일 때만 쓴다.
|
||||
|
||||
새로 만드는 8개 중 `export *`를 쓸 자리는 **없다** (아래 §3.7에서 `storage`가 서브배럴을 재수출하지 않기로 판정하므로).
|
||||
|
||||
---
|
||||
|
||||
## 2. 공개/내부 판정 기준
|
||||
|
||||
판정 근거는 실제 사용처다. 측정 명령:
|
||||
|
||||
```
|
||||
grep -rnE 'from "[^"]*adapters/(auth|browser-files|browser-file-storage|browser-rpc|browser-transfer|cache-storage|cross-context-invalidation|diagnostics|http|platform|query-cache|realtime|service-worker|storage|telemetry|web-push)/' src/ --include='*.ts' --include='*.tsx' | grep -v '^src/adapters/'
|
||||
```
|
||||
|
||||
- **공개**: `src/bootstrap/**`, `src/features/**`, `src/presentation/**`이 import하는 심볼 + 그 심볼의 시그니처에 이름으로 등장하는 타입(의존성/옵션/반환 파사드).
|
||||
- **내부**: 그룹 안에서만 쓰이는 헬퍼.
|
||||
- **내부(테스트 전용)**: `tests/`만 import하는 심볼. 배럴에 넣지 않고, 테스트는 깊은 경로를 유지한다. 각 그룹에서 별도로 표시했다.
|
||||
|
||||
예외 처리 하나: 같은 파일의 동급 팩토리 형제는 오늘 테스트만 쓰더라도 공개로 올린다(예 `createAnonymousSessionAdapter`). 근거는 §3.1.
|
||||
|
||||
---
|
||||
|
||||
## 3. 새로 만들 배럴 8개 (전문)
|
||||
|
||||
아래 내용은 전부 **그대로 파일로 저장 가능**하다. 모든 심볼은 기계 대조로 존재를 확인했다(82개 전수, §7).
|
||||
|
||||
### 3.1 `src/adapters/auth/index.ts`
|
||||
|
||||
그룹 파일: `external-session-adapter.ts` 1개.
|
||||
|
||||
| 심볼 | 위치 | 판정 | 근거 |
|
||||
|---|---|---|---|
|
||||
| `createExternalAuthSessionAdapter` | `external-session-adapter.ts:51` | 공개 | `src/bootstrap/runtime-adapters.ts:3` |
|
||||
| `createDemoSessionAdapter` | `:104` | 공개 | `src/bootstrap/runtime-adapters.ts:2`, `:279` |
|
||||
| `createUnavailableSessionAdapter` | `:140` | 공개 | `src/bootstrap/runtime-adapters.ts:4` |
|
||||
| `createAnonymousSessionAdapter` | `:77` | 공개(승격) | 위 셋과 같은 파일·같은 반환형(`AuthSessionPort`)의 형제 팩토리. 오늘 `src` 소비자는 `.storybook/preview.tsx:5`뿐이라 배럴이 없으면 깊은 경로가 남는다 |
|
||||
| `DEMO_AUTHORIZATION_MARKER` | `:98` | 공개 | 계약 문서가 이름으로 참조: `docs/architecture/decisions/VD-23-api-transport-selection-and-rest-execution.md:305` |
|
||||
| `ExternalSessionOwner` | `:10` | 공개 | `src/bootstrap/runtime-adapters.ts:5` |
|
||||
| `DemoSessionAdapter` | `:89` | 공개 | `createDemoSessionAdapter`의 반환 타입 |
|
||||
| `validateCredentialPatch` | `:26` | **내부** | `src/`·`tests/` 어디서도 import하지 않음 |
|
||||
|
||||
```ts
|
||||
export {
|
||||
createAnonymousSessionAdapter,
|
||||
createDemoSessionAdapter,
|
||||
createExternalAuthSessionAdapter,
|
||||
createUnavailableSessionAdapter,
|
||||
DEMO_AUTHORIZATION_MARKER,
|
||||
type DemoSessionAdapter,
|
||||
type ExternalSessionOwner,
|
||||
} from "./external-session-adapter.ts";
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3.2 `src/adapters/diagnostics/index.ts`
|
||||
|
||||
그룹 파일: `bounded-diagnostics.ts` 1개. export 5개 전부 공개.
|
||||
|
||||
| 심볼 | 위치 | 판정 | 근거 |
|
||||
|---|---|---|---|
|
||||
| `createDiagnosticsAdapter` | `bounded-diagnostics.ts:18` | 공개 | `src/bootstrap/runtime-adapters.ts:7` |
|
||||
| `recordBootFailure` | `:80` | 공개 | `src/bootstrap/main.tsx:3` |
|
||||
| `getLastBootEvidence` | `:108` | 공개 | `recordBootFailure`의 짝(부팅 증거 읽기). 오늘 사용처는 `tests/`만 |
|
||||
| `noOpDiagnostics` | `:11` | 공개 | `DiagnosticsPort`의 null object. 다른 그룹이 기본값으로 기대하는 형태 |
|
||||
| `MAX_DIAGNOSTIC_ENTRIES` | `:16` | 공개 | 선언된 상한값(계약 수치) |
|
||||
|
||||
```ts
|
||||
export {
|
||||
createDiagnosticsAdapter,
|
||||
getLastBootEvidence,
|
||||
MAX_DIAGNOSTIC_ENTRIES,
|
||||
noOpDiagnostics,
|
||||
recordBootFailure,
|
||||
} from "./bounded-diagnostics.ts";
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3.3 `src/adapters/telemetry/index.ts`
|
||||
|
||||
그룹 파일: `best-effort-telemetry.ts` 1개.
|
||||
|
||||
| 심볼 | 위치 | 판정 | 근거 |
|
||||
|---|---|---|---|
|
||||
| `createTelemetryAdapter` | `best-effort-telemetry.ts:52` | 공개 | `src/bootstrap/runtime-adapters.ts:29` |
|
||||
| `noOpTelemetry` | `:31` | 공개 | null object |
|
||||
| `MAX_TELEMETRY_QUEUE` | `:47` | 공개 | 선언된 상한값 |
|
||||
| `TelemetryAdapter` | `:10` | 공개 | 팩토리 반환 타입 |
|
||||
| `TelemetryAdapterOptions` | `:20` | 공개 | 팩토리 인자 타입 |
|
||||
| `safeTraceparent` | `:229` | **내부(테스트 전용)** | `tests/`만 import. traceparent 정규화는 어댑터 내부 동작 |
|
||||
| `assertBoundedCapacity` 재수출 | `:49` | **배럴에 넣지 않음** | 이건 telemetry의 export가 아니라 **커널(`../platform/bounded-capacity.ts`) 심볼의 재수출**이다. 배럴에 올리면 커널로 가는 두 번째 문이 생긴다 |
|
||||
|
||||
> **부수 발견 / 별도 처리 권고**
|
||||
> `src/adapters/telemetry/best-effort-telemetry.ts:49`
|
||||
> ```ts
|
||||
> export { assertBoundedCapacity } from "../platform/bounded-capacity.ts";
|
||||
> ```
|
||||
> 이 줄은 이 배럴 작업과 무관하게 지우는 게 맞다. 같은 파일 `:8`에서 이미 같은 심볼을 import해서 `:61`에서 쓰고 있으므로 재수출은 순수 잉여이고, 실제 소비자도 없다(`grep -rn 'assertBoundedCapacity' src/ tests/` → `platform/bounded-capacity.ts` 정의부, `telemetry:8,49,61`, `diagnostics:9,25`가 전부). 이 문서 범위에서는 **건드리지 않고** 배럴에서 제외만 한다.
|
||||
|
||||
```ts
|
||||
export {
|
||||
createTelemetryAdapter,
|
||||
MAX_TELEMETRY_QUEUE,
|
||||
noOpTelemetry,
|
||||
type TelemetryAdapter,
|
||||
type TelemetryAdapterOptions,
|
||||
} from "./best-effort-telemetry.ts";
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3.4 `src/adapters/platform/index.ts` — 커널
|
||||
|
||||
#### 판정: 배럴은 만들되, **어댑터→커널 간선은 파일 직접 import를 유지한다.**
|
||||
|
||||
질문은 "`browser-transfer/presigned/presigned-transfer-executor.ts:15`의 `../../platform/abortable-operation.ts`가 `../../platform/index.ts`로 바뀌어야 하는가"였다. **아니다. 바꾸면 게이트가 깨진다.**
|
||||
|
||||
**근거 1 (결정적) — `check:adapter-inventory`가 파일 경로를 직접 검증한다.**
|
||||
`scripts/check-adapter-inventory.ts:63-83`:
|
||||
|
||||
```ts
|
||||
const PRIMITIVE_PATH = path.resolve("src/adapters/platform/abortable-operation.ts");
|
||||
for (const consumer of REQUIRED_ABORT_CONSUMERS) {
|
||||
...
|
||||
const specifiers = [...source.matchAll(/from\s+"([^"]*platform\/abortable-operation\.ts)"/gu)]...;
|
||||
const resolved = specifiers.some(
|
||||
(specifier) => path.resolve(path.dirname(consumer), specifier) === PRIMITIVE_PATH,
|
||||
);
|
||||
if (!resolved) { problems.push(`abortable-operation: ${consumer} does not resolve...`); }
|
||||
}
|
||||
```
|
||||
|
||||
`REQUIRED_ABORT_CONSUMERS`(`scripts/check-adapter-inventory.ts:134-137`)는 정확히 네 파일이다:
|
||||
`browser-transfer/presigned/presigned-capability-http-provider.ts`, `.../presigned-transfer-executor.ts`, `browser-transfer/image-cdn/browser-image-probe.ts`, `browser-transfer/resumable-upload/fetch-json-transport.ts`.
|
||||
이들이 `../../platform/index.ts`로 바뀌면 정규식이 매칭되지 않아 `check:adapter-inventory`가 실패한다.
|
||||
|
||||
**근거 2 — 규칙상으로는 둘 다 가능하지만, 커널의 정체성은 "파일"이다.**
|
||||
`.dependency-cruiser.json:191`의 carve-out은 `^src/adapters/($1/|platform/|browser-file-storage/result\.ts$|cross-context-invalidation/index\.ts$)` 로 `platform/` 폴더 전체를 허용한다. 즉 규칙은 중립이다.
|
||||
그런데 `docs/architecture/layers.md:34`는 커널을 "the system clock, the shared abort primitive and the bounded-capacity guard" — **세 개의 프리미티브**로 정의한다. 런타임 합성물이 아니라 원시 도구다. 원시 도구는 "어느 파일에서 왔는지"가 곧 정체성이고, 실제로 위 게이트가 그 정체성을 파일 경로로 확인한다.
|
||||
|
||||
**근거 3 — 그래서 새 게이트 규칙은 `src/adapters/**`를 출발점에서 제외한다.**
|
||||
어댑터끼리의 간선은 이미 `adapters-do-not-know-other-concrete-adapters`(`.dependency-cruiser.json:183-193`)가 담당한다. 새 규칙이 그 위에 겹칠 이유가 없다. §5의 `from.pathNot: "^src/adapters/"`가 이 판정의 실행형이다.
|
||||
|
||||
#### 그러면 배럴에는 뭘 넣나
|
||||
|
||||
커널 프리미티브도 **전부** 넣는다. 이유: 배럴을 쓰는 쪽은 `src/bootstrap`과 `tests`인데, 이들은 `systemClock`(tests), `createAbortableOperation`(tests), `createBrowserLifecycleRuntime`(`src/bootstrap/optional-runtime-host.ts:2`), `createBrowserMutationIntentFactory`(`src/bootstrap/runtime-adapters.ts:28`)를 쓴다. 일부만 넣으면 "바깥은 배럴만"이라는 규칙에 예외가 생긴다. 두 개의 문이 생기는 게 아니라 **문이 소비자별로 하나씩**이다: 어댑터는 파일, 그 외는 배럴.
|
||||
|
||||
| 파일 | 공개 심볼 | 소비자 근거 |
|
||||
|---|---|---|
|
||||
| `abortable-operation.ts` | `createAbortableOperation:90`, `compensateLateHandle:261`, `snapshotAbortTimers:70`, `AbortableOperation:24`, `AbortableOperationInput:51`, `AbortRace:19`, `AbortTerminalReason:11`, `AbortTimerSnapshot:59` | 어댑터 4곳(직접 경로 유지) + `tests/` |
|
||||
| `bounded-capacity.ts` | `assertBoundedCapacity:10` | `diagnostics:9`, `telemetry:8` (직접 경로 유지) |
|
||||
| `browser-lifecycle.ts` | `createBrowserLifecycleRuntime:57`, `BrowserLifecycleEvent:20`, `BrowserLifecycleRuntime:36`, `BrowserLifecycleSnapshot:13` | `src/bootstrap/optional-runtime-host.ts:1-4` |
|
||||
| `browser-mutation-intent-factory.ts` | `createBrowserMutationIntentFactory:9`, `BrowserMutationIntentFactoryDependencies:4` | `src/bootstrap/runtime-adapters.ts:28` |
|
||||
| `system-clock.ts` | `systemClock:3` | 어댑터 6곳(직접 경로 유지) + `tests/` |
|
||||
|
||||
내부: 없음. 이 그룹은 모든 export가 커널 표면이다.
|
||||
|
||||
```ts
|
||||
/**
|
||||
* 어댑터 커널의 공개 경계.
|
||||
*
|
||||
* `src/adapters/**` 안에서는 이 배럴을 쓰지 않는다. 커널 프리미티브는 파일
|
||||
* 경로로 직접 import한다 — `scripts/check-adapter-inventory.ts`가
|
||||
* `platform/abortable-operation.ts`로 해석되는 specifier를 네 소비자에게
|
||||
* 요구하고, `.dependency-cruiser.json`의 kernel carve-out도 폴더 단위다.
|
||||
* 이 배럴은 bootstrap·features·tests 같은 그룹 바깥 소비자를 위한 문이다.
|
||||
*/
|
||||
export {
|
||||
compensateLateHandle,
|
||||
createAbortableOperation,
|
||||
snapshotAbortTimers,
|
||||
type AbortableOperation,
|
||||
type AbortableOperationInput,
|
||||
type AbortRace,
|
||||
type AbortTerminalReason,
|
||||
type AbortTimerSnapshot,
|
||||
} from "./abortable-operation.ts";
|
||||
export { assertBoundedCapacity } from "./bounded-capacity.ts";
|
||||
export {
|
||||
createBrowserLifecycleRuntime,
|
||||
type BrowserLifecycleEvent,
|
||||
type BrowserLifecycleRuntime,
|
||||
type BrowserLifecycleSnapshot,
|
||||
} from "./browser-lifecycle.ts";
|
||||
export {
|
||||
createBrowserMutationIntentFactory,
|
||||
type BrowserMutationIntentFactoryDependencies,
|
||||
} from "./browser-mutation-intent-factory.ts";
|
||||
export { systemClock } from "./system-clock.ts";
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3.5 `src/adapters/query-cache/index.ts`
|
||||
|
||||
파일 5개, 파일마다 팩토리 1~3개. 전부 공개(4개는 bootstrap이 직접, `createCursorPaginationRuntime`·`createQueryCacheAdapter`는 동급 팩토리).
|
||||
|
||||
| 심볼 | 위치 | 근거 |
|
||||
|---|---|---|
|
||||
| `createConditionalValidatorStore` | `conditional-validator-store.ts:55` | `src/bootstrap/runtime-adapters.ts:26` |
|
||||
| `ConditionalValidatorBinding` / `ConditionalValidatorStore` | `:3` / `:10` | 위 팩토리의 반환·요소 타입 |
|
||||
| `createCursorPaginationRuntime` | `cursor-pagination-runtime.ts:49` | 동급 런타임 팩토리(오늘 소비자는 `tests/`만) |
|
||||
| `createServerStateScopeRuntime` | `server-state-scope-runtime.ts:34` | `src/bootstrap/runtime-adapters.ts:25` |
|
||||
| `ScopeResetParticipant` / `ServerStateScopeDependencies` | `:19` / `:26` | 위 팩토리의 인자 타입 |
|
||||
| `createTanStackCacheCoordinator` | `tanstack-cache-coordinator.ts:33` | `src/bootstrap/runtime-adapters.ts:22` |
|
||||
| `TanStackCacheCoordinatorDependencies` | `:21` | 인자 타입 |
|
||||
| `createQueryClient` | `tanstack-query-cache.ts:21` | `src/bootstrap/runtime-adapters.ts:24`, `.storybook/preview.tsx:6` |
|
||||
| `createQueryCacheAdapter` | `:59` | 동급 팩토리(`QueryCachePort` 구현) |
|
||||
| `QUERY_CACHE_DEFAULTS` | `:12` | 선언된 기본값 |
|
||||
| `QueryCacheDependencies` | `:8` | 인자 타입 |
|
||||
|
||||
내부: 없음.
|
||||
|
||||
> 주의: 이 그룹은 `.dependency-cruiser.json:184`가 명시적으로 이름 붙인 미해결 간선의 출발점이다 — `tanstack-cache-coordinator.ts:19`가 `../cross-context-invalidation/index.ts`에서 협력자 타입 2개를 읽는다. 배럴 작업은 이 간선을 건드리지 않는다(어댑터→어댑터 간선이므로 새 규칙 범위 밖).
|
||||
|
||||
```ts
|
||||
export {
|
||||
createConditionalValidatorStore,
|
||||
type ConditionalValidatorBinding,
|
||||
type ConditionalValidatorStore,
|
||||
} from "./conditional-validator-store.ts";
|
||||
export { createCursorPaginationRuntime } from "./cursor-pagination-runtime.ts";
|
||||
export {
|
||||
createServerStateScopeRuntime,
|
||||
type ScopeResetParticipant,
|
||||
type ServerStateScopeDependencies,
|
||||
} from "./server-state-scope-runtime.ts";
|
||||
export {
|
||||
createTanStackCacheCoordinator,
|
||||
type TanStackCacheCoordinatorDependencies,
|
||||
} from "./tanstack-cache-coordinator.ts";
|
||||
export {
|
||||
createQueryCacheAdapter,
|
||||
createQueryClient,
|
||||
QUERY_CACHE_DEFAULTS,
|
||||
type QueryCacheDependencies,
|
||||
} from "./tanstack-query-cache.ts";
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3.6 `src/adapters/service-worker/index.ts`
|
||||
|
||||
#### 제약 하나 먼저
|
||||
|
||||
`tsconfig.app.json:14-19`의 `exclude`에 `src/adapters/service-worker/service-worker-entry.ts`가 들어 있다. **배럴은 이 파일을 절대 참조하면 안 된다** — 참조하면 app 타입체크가 제외된 파일을 끌어들인다. 다행히 `service-worker-entry.ts`는 export가 0개라(`grep -n '^export ' → 없음`) 넣을 것도 없다.
|
||||
|
||||
| 파일 | 판정 | 근거 |
|
||||
|---|---|---|
|
||||
| `service-worker-page-controller.ts` | 공개 | `src/bootstrap/register-service-worker.ts:5`가 `createServiceWorkerPageController`를 씀 |
|
||||
| `service-worker-lifecycle.ts` | 공개 | 워커 realm 진입점(`service-worker-entry.ts:11`)이 합성하는 런타임. 그룹 바깥(워커 번들·tests)이 실제 소비자 |
|
||||
| `service-worker-protocol.ts` | 공개 | 페이지↔워커 메시지 코덱. 양쪽 realm이 공유하는 어휘 |
|
||||
| `service-worker-removal.ts` | **내부** | 소비자는 `service-worker-page-controller.ts:20`(그룹 내) + `tests/`뿐 |
|
||||
| `service-worker-static-assets.ts` | **내부** | 소비자는 `service-worker-lifecycle.ts:17`(그룹 내) + `tests/`뿐 |
|
||||
| `service-worker-entry.ts` | 대상 아님 | export 0개 + `tsconfig.app.json` 제외 |
|
||||
|
||||
내부(테스트 전용)로 남아 깊은 경로를 유지할 심볼: `removeOwnedRegistration`, `purgeOwnedResources`, `isOwnedRegistration`, `expectedServiceWorkerUrls`(`service-worker-removal.ts:33,65,89,138`), `installStaticAssets`, `classifyFetch`, `validateStaticAssetManifest`, `selectCachesToDelete`(`service-worker-static-assets.ts:35,85,119,377`).
|
||||
|
||||
```ts
|
||||
export {
|
||||
createServiceWorkerRuntime,
|
||||
type WorkerClientLike,
|
||||
type WorkerRuntimeConfig,
|
||||
type WorkerScopeLike,
|
||||
} from "./service-worker-lifecycle.ts";
|
||||
export {
|
||||
createServiceWorkerPageController,
|
||||
type ActivationBlocker,
|
||||
type PageControllerDependencies,
|
||||
} from "./service-worker-page-controller.ts";
|
||||
export {
|
||||
createNonceRegistry,
|
||||
createServiceWorkerMessage,
|
||||
parseServiceWorkerMessage,
|
||||
type ParsedMessage,
|
||||
} from "./service-worker-protocol.ts";
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3.7 `src/adapters/storage/index.ts`
|
||||
|
||||
#### 판정: 최상위 배럴은 서브폴더를 재수출하지 **않는다**. 서브폴더 배럴이 곧 경계다.
|
||||
|
||||
`browser-transfer/index.ts:1-3`과 `realtime/index.ts:56-58`의 선례를 따라 `export * from "./indexeddb/index.ts"`를 넣고 싶어지지만, **storage에서는 그게 게이트를 깬다.**
|
||||
|
||||
**근거 (결정적) — 제거 드릴이 서브폴더만 삭제한다.**
|
||||
`scripts/test-browser-file-storage-runtime-removal.ts:23-34`:
|
||||
|
||||
```ts
|
||||
const runtimePaths = [
|
||||
...
|
||||
"src/adapters/storage/indexeddb",
|
||||
"src/adapters/storage/opfs",
|
||||
...
|
||||
] as const;
|
||||
```
|
||||
|
||||
이 스크립트는 fixture 트리에서 위 경로를 `rm -rf`한 뒤(`:56-61`) `assertNoRuntimeImports(fixtureRoot, runtimeSourceRoots, ...)`를 호출한다(`:129-133`). 그 함수는 `scripts/lib/removal-fixture.ts:251-260`:
|
||||
|
||||
```ts
|
||||
const graph = await runtimeImportGraph(root, runtimeSourceRoots);
|
||||
if (graph.importingFiles.length > 0) {
|
||||
throw new Error(`Removed ${capability} runtime is still imported by: ...`);
|
||||
}
|
||||
```
|
||||
|
||||
즉 `src/adapters/storage/index.ts`가 `./indexeddb/index.ts`를 재수출하면, 삭제 후에도 그 파일이 살아남아 삭제된 런타임을 import하는 상태가 되어 드릴이 **즉시 예외로 실패**한다. 그 뒤에 이어지는 `check:types` / `lint` / `check:architecture` / `build`(`:135-143`)까지 전부 못 간다.
|
||||
|
||||
**대조 — realtime·browser-transfer는 왜 괜찮은가.**
|
||||
`scripts/test-realtime-runtime-removal.ts:21-31`은 `src/adapters/realtime` **폴더 전체**를 지운다. `browser-file-storage` 드릴도 `src/adapters/browser-transfer` 전체를 지운다(`test-browser-file-storage-runtime-removal.ts:28`). 배럴이 폴더와 함께 사라지므로 문제가 없다. **storage만 부분 삭제 대상**이다 — 최상위 `browser-storage-adapter.ts`(localStorage/sessionStorage KV)는 남고 IndexedDB·OPFS 런타임만 빠진다.
|
||||
|
||||
**따라서:**
|
||||
- `src/adapters/storage/index.ts` = 최상위 파일들만.
|
||||
- `src/adapters/storage/indexeddb/index.ts`, `src/adapters/storage/opfs/index.ts` = 각 제거 가능 런타임의 공개 경계. **이미 존재하고 이미 명명 재수출이다.** 새로 만들 필요 없다.
|
||||
- §5의 게이트 정규식은 그래서 1단계 중첩 `index.ts`까지 배럴로 인정해야 한다.
|
||||
|
||||
| 심볼 | 위치 | 판정 | 근거 |
|
||||
|---|---|---|---|
|
||||
| `createBrowserStorageAdapter` | `browser-storage-adapter.ts:35` | 공개 | `src/bootstrap/runtime-adapters.ts:27` |
|
||||
| `BrowserStorageDependencies` | `:20` | 공개 | 팩토리 인자 타입. 필드가 전부 원시형/포트라 codec 타입을 노출하지 않는다(`:20-27` 확인) |
|
||||
| `browser-storage-codec.ts` 전체 (`DEFAULT_BROWSER_STORAGE_MAX_SERIALIZED_BYTES:1`, `encodeBrowserStorageEnvelope:31`, `decodeBrowserStorageEnvelope:57`, `assertValidBrowserStorageByteLimit:88`, `BrowserStorageEnvelope:11`, `BrowserStorageCodecFailure:17`, `BrowserStorageCodecResult:22`) | — | **내부** | 유일한 소비자가 `browser-storage-adapter.ts:14-18`(그룹 내). `src/`·`tests/` 어디서도 직접 import 없음 |
|
||||
|
||||
```ts
|
||||
export {
|
||||
createBrowserStorageAdapter,
|
||||
type BrowserStorageDependencies,
|
||||
} from "./browser-storage-adapter.ts";
|
||||
```
|
||||
|
||||
> **서브배럴 보완 권고 (별건, 이 문서 범위 밖)**
|
||||
> `tests/unit/opfs-byte-store.test.ts:25,28`가 `OPFS_WORKER_PROTOCOL_VERSION`(`opfs/opfs-worker-protocol.ts:23`), `PreparePhysicalObjectRequest`(`:179`), `writeWithSyncAccessHandle`(`opfs/opfs-worker-runtime.ts:1363`)를 쓰는데 이 셋은 `opfs/index.ts`에 없다. 테스트를 배럴로 옮길 때(§4.2) 함께 결정해야 한다 — 올리거나, 내부로 확정하고 테스트가 깊은 경로를 유지하거나.
|
||||
|
||||
---
|
||||
|
||||
### 3.8 `src/adapters/http/index.ts`
|
||||
|
||||
#### V2 / V3 판정
|
||||
|
||||
**V3(`createContractHttpExecutor`, `http-execution-v3.ts:377`)가 권장 경로.** V2(`createHttpClient`, `client.ts:138`)는 legacy 보존.
|
||||
|
||||
근거:
|
||||
1. `src/adapters/http/client.ts:121`에 `LegacyHttpInput`이라는 타입이 있고, 공개 시그니처 `HttpClient.execute`(`:131-137`)가 그걸 두 번째 인자로 받는다. 파일이 스스로 legacy라고 말한다.
|
||||
2. `src/adapters/http/http-execution-v3.ts:45-51` 헤더 주석: "§7–§8. Descriptor-driven HTTP execution. ... the runtime owns bounds, the total deadline, the single retry authority and the effect-certainty verdict." — 계약 기반 실행이 V3에 있다.
|
||||
3. 라이브 경로가 V3다. `src/bootstrap/runtime-adapters.ts:423`이 `createContractHttpExecutor`를 조립해 실제 런타임에 넣는다. 반면 V2 래퍼 `createRuntimeHttpClient`(`src/bootstrap/runtime-adapters.ts:163`)의 유일한 호출자는 `tests/unit/runtime-adapters.test.ts:371`이다 (`grep -rn 'createRuntimeHttpClient' src/ tests/` 결과 3줄: 정의 1 + 테스트 2).
|
||||
4. V3 타입은 이미 feature 경계를 넘는다: `src/features/reference-feature/adapters/create-reference-feature-input.ts:9`가 `HttpExecutionOutcome`를 import한다.
|
||||
|
||||
둘 다 배럴에 넣되 **블록 순서로 V3를 먼저 두고 주석으로 표시**한다.
|
||||
|
||||
#### 공개 표면
|
||||
|
||||
| 파일 | 판정 | 근거 |
|
||||
|---|---|---|
|
||||
| `http-execution-v3.ts` | 공개 | `src/bootstrap/runtime-adapters.ts:10,11`, `src/features/reference-feature/adapters/create-reference-feature-input.ts:9` |
|
||||
| `client.ts` | 공개(legacy) | `src/bootstrap/runtime-adapters.ts:8` |
|
||||
| `http-contract-bridge.ts` | `CredentialPatchOutcome`만 공개 | `http-execution-v3.ts:292`의 공개 시그니처 `attachCredentials(...): Promise<CredentialPatchOutcome> \| CredentialPatchOutcome`에 이름으로 등장 → bootstrap이 그 콜백을 구현한다(`src/bootstrap/runtime-adapters.ts:434` 부근) |
|
||||
| `request-builder.ts` | `OperationRequestInput`만 공개 | `client.ts:133`의 공개 시그니처 `execute(request: string \| OperationRequestInput, ...)`에 이름으로 등장 |
|
||||
| `bounded-body-reader.ts` | **내부** | 소비자는 `bounded-json.ts:1`, `http-execution-v3.ts:18-20`(그룹 내) + `tests/` |
|
||||
| `bounded-json.ts` | **내부** | 소비자는 `client.ts:45`(그룹 내) + `tests/` |
|
||||
| `http-effect-certainty.ts` | **내부** | 소비자는 `http-execution-v3.ts:38-42`(그룹 내) + `tests/` |
|
||||
| `retry-policy.ts` | **내부** | 소비자는 `client.ts:9`, `http-execution-v3.ts:43`(그룹 내) + `tests/` |
|
||||
| `schema-registry.ts` | **내부** | 소비자는 `client.ts:14`(그룹 내) + `tests/` |
|
||||
| `resource-mapper.ts` | **내부** | 소비자는 `client.ts:8`(그룹 내) + `tests/` |
|
||||
| `http-contract-bridge.ts` 나머지 11개 | **내부** | 그룹 내 + `tests/` |
|
||||
| `request-builder.ts` 나머지 2개 (`buildRequestTarget:28`, `RequestTargetResult:14`) | **내부(테스트 전용)** | `tests/`만 |
|
||||
|
||||
```ts
|
||||
/**
|
||||
* §7–§8. 권장 경로는 V3 계약 실행기(`createContractHttpExecutor`)다. 설치된
|
||||
* 계약과 타입 입력을 받아 상한·전체 데드라인·재시도 권한·효과 확실성 판정을
|
||||
* 런타임이 소유한다.
|
||||
*/
|
||||
export {
|
||||
createContractHttpExecutor,
|
||||
type AuthIntegrationFailureReason,
|
||||
type AuthOperationContext,
|
||||
type CancellationOwner,
|
||||
type ContractHttpExecutor,
|
||||
type ContractHttpExecutorDependencies,
|
||||
type HttpContractViolation,
|
||||
type HttpContractViolationKind,
|
||||
type HttpEffectCertainty,
|
||||
type HttpExecutionContext,
|
||||
type HttpExecutionObservation,
|
||||
type HttpExecutionOutcome,
|
||||
type HttpTransportFailure,
|
||||
type SafeResponseMetadata,
|
||||
} from "./http-execution-v3.ts";
|
||||
/** V3 `attachCredentials` 콜백이 반환해야 하는 결과 타입. */
|
||||
export type { CredentialPatchOutcome } from "./http-contract-bridge.ts";
|
||||
/**
|
||||
* V2 legacy. operationId + `LegacyHttpInput`으로 호출하는 범용 클라이언트다.
|
||||
* 새 코드는 위의 V3 실행기를 쓴다. 남아 있는 이유는 계약이 아직 없는
|
||||
* 오퍼레이션을 위한 이행 경로이기 때문이다.
|
||||
*/
|
||||
export {
|
||||
createHttpClient,
|
||||
type HttpClient,
|
||||
type HttpClientDependencies,
|
||||
type HttpFailure,
|
||||
type HttpResult,
|
||||
type LegacyHttpInput,
|
||||
type Scheduler,
|
||||
} from "./client.ts";
|
||||
/** V2 `HttpClient.execute`의 첫 인자 타입. */
|
||||
export type { OperationRequestInput } from "./request-builder.ts";
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. 호출처 치환 목록
|
||||
|
||||
### 4.1 `src/` — 전수 (17줄)
|
||||
|
||||
측정: §2의 grep. 17줄 중 2줄은 이미 배럴 경유라 변경 없음. 실제 치환 대상 **15줄**.
|
||||
|
||||
| # | 파일:줄 | 현재 specifier | 바뀔 specifier | 비고 |
|
||||
|---:|---|---|---|---|
|
||||
| 1 | `src/bootstrap/main.tsx:3` | `../adapters/diagnostics/bounded-diagnostics.ts` | `../adapters/diagnostics/index.ts` | `recordBootFailure` |
|
||||
| 2 | `src/bootstrap/optional-runtime-host.ts:4` | `../adapters/platform/browser-lifecycle.ts` | `../adapters/platform/index.ts` | 블록 시작은 `:1` |
|
||||
| 3 | `src/bootstrap/register-service-worker.ts:5` | `../adapters/service-worker/service-worker-page-controller.ts` | `../adapters/service-worker/index.ts` | `createServiceWorkerPageController` |
|
||||
| 4 | `src/bootstrap/runtime-adapters.ts:6` | `../adapters/auth/external-session-adapter.ts` | `../adapters/auth/index.ts` | 블록 시작 `:1` |
|
||||
| 5 | `src/bootstrap/runtime-adapters.ts:7` | `../adapters/diagnostics/bounded-diagnostics.ts` | `../adapters/diagnostics/index.ts` | `createDiagnosticsAdapter` |
|
||||
| 6 | `src/bootstrap/runtime-adapters.ts:8` | `../adapters/http/client.ts` | `../adapters/http/index.ts` | **7번과 한 블록으로 합칠 것** |
|
||||
| 7 | `src/bootstrap/runtime-adapters.ts:12` | `../adapters/http/http-execution-v3.ts` | `../adapters/http/index.ts` | 블록 시작 `:9` |
|
||||
| 8 | `src/bootstrap/runtime-adapters.ts:20` | `../adapters/cross-context-invalidation/index.ts` | (변경 없음) | 이미 배럴 |
|
||||
| 9 | `src/bootstrap/runtime-adapters.ts:23` | `../adapters/query-cache/tanstack-cache-coordinator.ts` | `../adapters/query-cache/index.ts` | 블록 시작 `:21`. **9~12를 한 블록으로 합칠 것** |
|
||||
| 10 | `src/bootstrap/runtime-adapters.ts:24` | `../adapters/query-cache/tanstack-query-cache.ts` | `../adapters/query-cache/index.ts` | `createQueryClient` |
|
||||
| 11 | `src/bootstrap/runtime-adapters.ts:25` | `../adapters/query-cache/server-state-scope-runtime.ts` | `../adapters/query-cache/index.ts` | `createServerStateScopeRuntime` |
|
||||
| 12 | `src/bootstrap/runtime-adapters.ts:26` | `../adapters/query-cache/conditional-validator-store.ts` | `../adapters/query-cache/index.ts` | `createConditionalValidatorStore` |
|
||||
| 13 | `src/bootstrap/runtime-adapters.ts:27` | `../adapters/storage/browser-storage-adapter.ts` | `../adapters/storage/index.ts` | `createBrowserStorageAdapter` |
|
||||
| 14 | `src/bootstrap/runtime-adapters.ts:28` | `../adapters/platform/browser-mutation-intent-factory.ts` | `../adapters/platform/index.ts` | `createBrowserMutationIntentFactory` |
|
||||
| 15 | `src/bootstrap/runtime-adapters.ts:29` | `../adapters/telemetry/best-effort-telemetry.ts` | `../adapters/telemetry/index.ts` | `createTelemetryAdapter` |
|
||||
| 16 | `src/bootstrap/server-state-generation-store.ts:3` | `../adapters/cross-context-invalidation/index.ts` | (변경 없음) | 이미 배럴 |
|
||||
| 17 | `src/features/reference-feature/adapters/create-reference-feature-input.ts:9` | `../../../adapters/http/http-execution-v3.ts` | `../../../adapters/http/index.ts` | `type HttpExecutionOutcome` |
|
||||
|
||||
**작업 순서 주의.** `runtime-adapters.ts`는 12줄이 한 덩어리(`:6`~`:29`)다. 6·7을 합치고 9~12를 합치면 그 아래 줄 번호가 전부 밀린다. **아래에서 위로 편집**하거나 `:1`~`:29` 블록을 한 번에 다시 쓴다.
|
||||
|
||||
**선택 사항 (게이트 범위 밖, 일관성 목적).** `check:architecture`는 `src`만 스캔하므로(`scripts/check-architecture.ts:69`, `:116-123`) 아래 두 줄은 강제되지 않는다. 같은 커밋에서 정리하는 걸 권한다.
|
||||
|
||||
| 파일:줄 | 현재 | 바뀔 것 |
|
||||
|---|---|---|
|
||||
| `.storybook/preview.tsx:5` | `../src/adapters/auth/external-session-adapter.ts` | `../src/adapters/auth/index.ts` |
|
||||
| `.storybook/preview.tsx:6` | `../src/adapters/query-cache/tanstack-query-cache.ts` | `../src/adapters/query-cache/index.ts` |
|
||||
|
||||
**곁다리 정리 기회 하나.** `src/bootstrap/runtime-adapters.ts:65`가 `type HttpClientDependencies = Parameters<typeof createHttpClient>[0];`로 타입을 역추출한다. 배럴이 `HttpClientDependencies`를 직접 내보내므로 이 줄은 지우고 import로 대체할 수 있다. 필수는 아니다.
|
||||
|
||||
### 4.2 `tests/` — 그룹별 집계와 권고
|
||||
|
||||
측정(2026-09-16, `develop` `5434760`):
|
||||
|
||||
```
|
||||
grep -rnE 'from "[^"]*adapters/<16개 그룹>/' tests/ --include='*.ts' --include='*.tsx' | grep -v '^tests/fixtures/'
|
||||
```
|
||||
|
||||
**166줄 / 83개 파일.** (`tests/fixtures` 포함 시 169줄. 지시문의 184와 다른데, `tests/fixtures`는 `.dependency-cruiser.json:208`에서 제외 대상이고 그 안의 `indexeddb-repository.ts` 같은 경로는 존재하지 않는 금지 recipe fixture다.)
|
||||
|
||||
| 그룹 | 줄 수 | 배럴 존재 | 배럴 경유 |
|
||||
|---|---:|---|---:|
|
||||
| browser-transfer | 26 | 기존 | 0 |
|
||||
| realtime | 24 | 기존 | 1 |
|
||||
| http | 22 | **신규** | 0 |
|
||||
| storage | 20 | **신규**(+서브배럴 2) | 0 |
|
||||
| web-push | 17 | 기존 | 0 |
|
||||
| browser-files | 17 | 기존 | 1 |
|
||||
| query-cache | 9 | **신규** | 0 |
|
||||
| auth | 9 | **신규** | 0 |
|
||||
| browser-file-storage | 5 | 기존 | 0 |
|
||||
| service-worker | 4 | **신규** | 0 |
|
||||
| platform | 3 | **신규** | 0 |
|
||||
| browser-rpc | 3 | 기존 | 2 |
|
||||
| diagnostics | 2 | **신규** | 0 |
|
||||
| cross-context-invalidation | 2 | 기존 | 2 |
|
||||
| cache-storage | 2 | 기존 | 0 |
|
||||
| telemetry | 1 | **신규** | 0 |
|
||||
| **합계** | **166** | | **6** |
|
||||
|
||||
배럴 경유 6줄의 정확한 위치:
|
||||
`tests/unit/browser-file-runtime.test.ts:13`, `tests/unit/browser-rpc/browser-rpc-remediation.test.ts:9`, `tests/unit/browser-rpc/browser-rpc-runtime.test.ts:11`, `tests/unit/cross-tab-invalidation.test.ts:21`, `tests/unit/realtime/realtime-reconnect-coordinator.test.ts:20`, `tests/unit/tanstack-cache-coordinator.test.ts:7`.
|
||||
|
||||
#### 권고: 한꺼번에 옮기지 않는다. 파일을 건드릴 때 그 파일 것만 옮긴다.
|
||||
|
||||
이유 셋:
|
||||
|
||||
1. **게이트가 강제하지 않는다.** `check:architecture`는 `src`만 본다(`scripts/check-architecture.ts:69`, `:116-123`). 테스트를 지금 옮겨도 검증되는 게 없고, 안 옮겨도 깨지는 게 없다. 강제되지 않는 대량 변경은 리뷰 비용만 남는다.
|
||||
2. **테스트의 절반 이상이 내부 심볼을 쓴다.** 예: `tests/unit/opfs-byte-store.test.ts:25,28`의 `PreparePhysicalObjectRequest`·`writeWithSyncAccessHandle`은 `opfs/index.ts`에 없다. http의 `retry-policy`·`schema-registry`·`bounded-body-reader` 테스트도 마찬가지로 §3.8에서 내부로 판정한 심볼을 직접 겨눈다. 일괄 치환은 곧 "배럴에 내부 심볼을 밀어넣자"는 압력이 되고, 그러면 배럴이 경계가 아니라 재수출 덤프가 된다.
|
||||
3. **내부 심볼을 직접 겨누는 단위 테스트는 그래도 된다.** 배럴 규칙은 "그룹 바깥 **프로덕션 코드**는 배럴만"이지 "아무도 내부를 못 본다"가 아니다. 테스트는 구현 계약을 검증하는 게 일이다.
|
||||
|
||||
**실행 규칙 (문서에 남길 문장)**
|
||||
|
||||
> `tests/` 아래 어댑터 import는 배럴로 일괄 이관하지 않는다.
|
||||
> 어떤 테스트 파일을 **다른 이유로** 수정하거나 분할할 때, 그 파일이 쓰는 심볼이 해당 그룹 배럴에 있으면 그 파일 안에서만 배럴 경로로 바꾼다.
|
||||
> 배럴에 없는 심볼이면 깊은 경로를 유지한다. 배럴에 추가하고 싶으면 §2 기준으로 "공개"임을 논증하는 게 먼저다.
|
||||
|
||||
우선순위를 굳이 매긴다면: `auth`(9줄, 전부 공개 심볼), `diagnostics`(2줄), `telemetry`(1줄), `query-cache`(9줄) — 이 넷은 배럴이 그룹 표면을 100% 덮으므로 기계적 치환이 가능하다. 21줄. `http`·`storage`·`service-worker`는 내부 심볼 비중이 커서 파일 단위로만 접근한다.
|
||||
|
||||
---
|
||||
|
||||
## 5. 게이트로 강제하기
|
||||
|
||||
### 5.1 추가할 규칙 (그대로 붙여넣기)
|
||||
|
||||
`.dependency-cruiser.json`의 `forbidden` 배열에서 **`adapters-do-not-know-other-concrete-adapters` 바로 다음, `no-circular-dependencies` 앞**에 넣는다 (현재 `:193`과 `:194` 사이).
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "adapter-groups-are-reached-through-their-barrel",
|
||||
"comment": "docs/architecture/layers.md §4: 어댑터 그룹의 공개 표면은 그 그룹의 `index.ts`다. 그룹 바깥(bootstrap, features, presentation)은 배럴만 import한다. 배럴이 없던 시절 bootstrap은 어댑터 내부 파일 15곳을 직접 겨눴고, 그래서 어떤 파일이 공개이고 어떤 파일이 내부 헬퍼인지 아무 데도 적혀 있지 않았다. 출발점에서 `src/adapters`를 뺀 이유는 어댑터끼리의 간선은 바로 위 `adapters-do-not-know-other-concrete-adapters`가 이미 담당하고, 커널(`platform/**`)은 파일 단위로 공유되기 때문이다 — `scripts/check-adapter-inventory.ts`가 네 소비자에게 `platform/abortable-operation.ts`로 해석되는 specifier를 직접 요구한다. 도착점에서 1단계 중첩 `index.ts`를 허용한 이유는 `storage/indexeddb`와 `storage/opfs`가 각자 독립적으로 제거 가능한 런타임이고(scripts/test-browser-file-storage-runtime-removal.ts), 그래서 각자의 배럴이 곧 경계이기 때문이다.",
|
||||
"severity": "error",
|
||||
"from": {
|
||||
"path": "^src/",
|
||||
"pathNot": "^src/adapters/"
|
||||
},
|
||||
"to": {
|
||||
"path": "^src/adapters/[^/]+/",
|
||||
"pathNot": "^src/adapters/[^/]+/(?:[^/]+/)?index\\.ts$"
|
||||
}
|
||||
},
|
||||
```
|
||||
|
||||
규칙 형태 적합성: `scripts/check-architecture.ts:799-815`의 `validateArchitectureRules`는 `from`에 `path`/`pathNot`, `to`에 `path`/`pathNot`/`circular`만 허용한다. 이 규칙은 그 안에 있다. 정규식은 `new RegExp(pattern, "u")`로 평가되므로(`:739`, `:747`) 비캡처 그룹 `(?:...)`도 문제없다. `$1` 역참조는 쓰지 않았다.
|
||||
|
||||
### 5.2 이 규칙이 무엇을 잡고 무엇을 안 잡는가 — 실측
|
||||
|
||||
레포의 실제 import 그래프(`src` 전체, 상대 specifier 해석)에 규칙을 그대로 돌려본 결과:
|
||||
|
||||
**잡는 것 — 오늘 기준 위반 15건** (= §4.1의 치환 대상 15줄과 정확히 일치)
|
||||
|
||||
```
|
||||
src/bootstrap/main.tsx:3 -> src/adapters/diagnostics/bounded-diagnostics.ts
|
||||
src/bootstrap/optional-runtime-host.ts:4 -> src/adapters/platform/browser-lifecycle.ts
|
||||
src/bootstrap/register-service-worker.ts:5 -> src/adapters/service-worker/service-worker-page-controller.ts
|
||||
src/bootstrap/runtime-adapters.ts:6 -> src/adapters/auth/external-session-adapter.ts
|
||||
src/bootstrap/runtime-adapters.ts:7 -> src/adapters/diagnostics/bounded-diagnostics.ts
|
||||
src/bootstrap/runtime-adapters.ts:8 -> src/adapters/http/client.ts
|
||||
src/bootstrap/runtime-adapters.ts:12 -> src/adapters/http/http-execution-v3.ts
|
||||
src/bootstrap/runtime-adapters.ts:23 -> src/adapters/query-cache/tanstack-cache-coordinator.ts
|
||||
src/bootstrap/runtime-adapters.ts:24 -> src/adapters/query-cache/tanstack-query-cache.ts
|
||||
src/bootstrap/runtime-adapters.ts:25 -> src/adapters/query-cache/server-state-scope-runtime.ts
|
||||
src/bootstrap/runtime-adapters.ts:26 -> src/adapters/query-cache/conditional-validator-store.ts
|
||||
src/bootstrap/runtime-adapters.ts:27 -> src/adapters/storage/browser-storage-adapter.ts
|
||||
src/bootstrap/runtime-adapters.ts:28 -> src/adapters/platform/browser-mutation-intent-factory.ts
|
||||
src/bootstrap/runtime-adapters.ts:29 -> src/adapters/telemetry/best-effort-telemetry.ts
|
||||
src/features/reference-feature/adapters/create-reference-feature-input.ts:9 -> src/adapters/http/http-execution-v3.ts
|
||||
```
|
||||
|
||||
§4.1을 적용하면 이 15건이 0이 된다. **즉 배럴 8개 생성 + import 15줄 치환 + 규칙 추가를 한 커밋에 넣어야 `check:architecture`가 계속 PASS한다.** 순서를 나누면 중간 커밋이 빨간불이 된다.
|
||||
|
||||
**안 잡는 것 (의도대로)**
|
||||
|
||||
| 경우 | 건수 | 왜 안 잡히나 |
|
||||
|---|---:|---|
|
||||
| 같은 그룹 내부 파일끼리 | 229건 중 189건 | `from.pathNot: "^src/adapters/"`가 출발점을 제외 |
|
||||
| 어댑터 → 커널 (`platform/**`, `browser-file-storage/result.ts`, `cross-context-invalidation/index.ts`) | 40건 | 같은 이유. 이 간선들은 `adapters-do-not-know-other-concrete-adapters`(`:183-193`)의 `pathNot` carve-out이 계속 담당 |
|
||||
| 바깥 → 배럴 (이미 합격) | 2건 | `to.pathNot`이 `index.ts`를 면제. `src/bootstrap/runtime-adapters.ts:20`, `src/bootstrap/server-state-generation-store.ts:3` |
|
||||
| `tests/**` | 166줄 전부 | 스캔 범위 밖 (아래 §5.3) |
|
||||
| `src/presentation/adapters/query/**` | — | 경로가 `src/presentation/...`이라 `to.path` `^src/adapters/`에 매칭 자체가 안 됨 |
|
||||
|
||||
세 가지 질문에 대한 명시적 답:
|
||||
|
||||
1. **같은 그룹 내부 import — 허용된다.** 출발점이 `^src/adapters/`이면 규칙이 아예 평가되지 않는다.
|
||||
2. **커널 접근 — 허용된다.** `platform/`은 위와 같은 이유로 통과. `browser-file-storage/result.ts`와 `cross-context-invalidation/index.ts`도 출발점이 어댑터이므로 통과. (참고: `cross-context-invalidation/index.ts`는 마침 `index.ts`라 도착점 면제에도 걸린다 — 이중으로 안전.)
|
||||
3. **`tests/`는 대상이 아니다.** `scripts/check-architecture.ts:69`가 `sourceRoot = resolve(projectRoot, "src")`이고 `:116-123`이 `depcruise src --config ...`를 돌린다. 두 그래프 모두 `src`만 본다.
|
||||
|
||||
### 5.3 기존 규칙과의 충돌 검토
|
||||
|
||||
`.dependency-cruiser.json` 전체(18개 규칙)를 읽고 대조했다.
|
||||
|
||||
| 기존 규칙 | 줄 | 충돌 |
|
||||
|---|---:|---|
|
||||
| `domain-is-framework-neutral` | 4 | 없음. domain→adapters는 어차피 전면 금지 |
|
||||
| `application-does-not-know-concrete-runtime` | 13 | 없음. 동일 |
|
||||
| `presentation-does-not-know-adapters` | 24 | 없음. presentation→adapters 전면 금지가 상위 |
|
||||
| `page-templates-own-layout-only` | 34 | 없음 |
|
||||
| `icon-vendor-is-facade-only` | 44 | 없음 (외부 패키지 대상) |
|
||||
| `adapters-do-not-know-presentation` | 54 | 없음 (방향 반대) |
|
||||
| `feature-*` 4개 | 64–103 | 없음. feature adapters→`src/adapters`는 이 규칙들이 막지 않으므로 새 규칙이 유효하게 작동 (실측 15번째 위반이 그 경우) |
|
||||
| `concrete-adapters-compose-only-in-bootstrap` | 104 | 없음. `src/(domain\|application\|presentation\|contracts)` → `^src/adapters` 전면 금지. 새 규칙은 그 나머지(`bootstrap`, `features`)에서만 실효 |
|
||||
| `external-contract-package-single-import-path` | 114 | 없음 |
|
||||
| `presentation-does-not-fetch-directly` | 126 | 없음. `^src/adapters/(http\|realtime\|service-worker\|web-worker\|storage)` 를 겨누는데 presentation은 이미 전면 금지 |
|
||||
| `generic-worker-has-no-network-or-credentials` | 137 | 없음. `^src/adapters/web-worker` 출발이라 새 규칙 출발점 제외와 겹칠 뿐. (`src/adapters/web-worker`는 현재 존재하지 않는 예방 규칙) |
|
||||
| `service-worker-entry-is-not-page-code` | 148 | 없음 |
|
||||
| `contracts-do-not-know-application` | 159 | 없음 |
|
||||
| `generic-presentation-does-not-compose-the-product` | 170 | 없음 |
|
||||
| **`adapters-do-not-know-other-concrete-adapters`** | **183** | **없음 — 상보적.** 저쪽은 `from: ^src/adapters/([^/]+)/`, 이쪽은 `from.pathNot: ^src/adapters/`. 정확히 반대 집합이라 이중 판정이 생기지 않는다 |
|
||||
| `no-circular-dependencies` | 194 | 주의 필요 → 아래 |
|
||||
|
||||
**순환 검토.** `no-circular-dependencies`(`:194-201`)가 error다. 배럴 도입이 순환을 만들려면 그룹 안 파일이 자기 그룹 `index.ts`를 import해야 한다.
|
||||
|
||||
```
|
||||
grep -rn 'from "\./index\.ts"\|from "\.\./index\.ts"' src/adapters/ --include='*.ts' → 0건
|
||||
```
|
||||
|
||||
또 `src/bootstrap/**`는 어댑터에서 import되지 않는다(`adapters-do-not-know-presentation`이 `^src/(presentation|bootstrap)`을 막음, `:54-62`). 따라서 **새 순환 없음.** 단 §3.7의 판정을 뒤집어 `storage/index.ts`가 서브배럴을 재수출하면, 순환은 아니지만 제거 드릴이 깨진다(§6 위험 2).
|
||||
|
||||
### 5.4 ESLint 쪽은 손대지 않는다
|
||||
|
||||
`eslint.config.ts:9-34`의 `layerPatterns`는 레이어 단위(`**/adapters/**`)만 다루고 그룹 내부 경로를 구분하지 않는다. 배럴 규칙을 여기에도 복제하면 두 곳에서 같은 사실을 관리하게 된다. dependency-cruiser 쪽 한 곳만 유지한다.
|
||||
|
||||
---
|
||||
|
||||
## 6. 위험과 부수 작업
|
||||
|
||||
### 위험 1 (최대) — 번들 예산
|
||||
|
||||
`package.json`에 `"sideEffects"` 필드가 없다. 번들러는 모든 모듈을 부작용 있을 수 있는 것으로 보고, 배럴 재수출을 통해 들어온 모듈을 트리셰이킹에서 살려둘 수 있다.
|
||||
|
||||
구체적으로 위험한 세 곳:
|
||||
- `src/bootstrap/register-service-worker.ts`가 `service-worker/index.ts`를 import하면 `service-worker-lifecycle.ts`(666줄) → `service-worker-static-assets.ts`(394줄)가 **페이지 번들 그래프**에 들어온다. 지금은 `service-worker-page-controller.ts`(494줄) → `service-worker-protocol.ts` + `service-worker-removal.ts`만 들어온다.
|
||||
- `src/bootstrap/runtime-adapters.ts`가 `http/index.ts`를 import하면 V2·V3가 항상 함께 들어온다 (`client.ts` 1107줄 + `http-execution-v3.ts` 1602줄). 지금도 둘 다 import하긴 한다.
|
||||
- `query-cache/index.ts`는 `cursor-pagination-runtime.ts`(234줄)를 추가로 끌어온다.
|
||||
|
||||
예산: `config/performance/budgets.json`의 `bundle.initialJsGzipBytes = 204800`. `check:bundle`(`scripts/check-bundle.ts:47`)이 초과 시 실패한다.
|
||||
|
||||
**대응 (권장 순서)**
|
||||
1. 배럴 커밋에서 `corepack pnpm check:bundle`을 반드시 돌린다. 이 문서에서는 실행하지 않았다.
|
||||
2. 넘치면 `package.json`에 `"sideEffects": false`를 추가한다. `src/adapters` 아래에 최상위 부작용이 있는지 먼저 확인해야 한다(`presentation/styles/theme.css` 같은 CSS import는 `"sideEffects": ["*.css"]` 형태로 보존).
|
||||
3. 그래도 넘치면 `service-worker/index.ts`에서 `service-worker-lifecycle.ts` 블록을 빼고, 워커 realm은 파일 직접 import를 유지한다(`platform`과 같은 논리 — realm이 다르면 문도 다르다).
|
||||
|
||||
### 위험 2 — `storage/index.ts`에 서브배럴을 넣고 싶은 충동
|
||||
|
||||
`browser-transfer`/`realtime` 선례만 보고 `export * from "./indexeddb/index.ts"`를 넣으면 `test:browser-file-storage-removal`이 `assertNoRuntimeImports` 단계에서 예외로 죽는다(§3.7). 배럴 파일에 그 이유를 주석으로 못 박아 두는 걸 권한다.
|
||||
|
||||
### 위험 3 — `check:adapter-inventory`가 새 파일 8개를 거부한다
|
||||
|
||||
`scripts/check-adapter-inventory.ts:58-74`가 `git ls-files src/adapters` 결과와 `docs/reviews/adapters/INVENTORY.md`의 행 목록을 **정확히 일치**시키고, `합계: **N/N**` 숫자도 파일 수와 같아야 한다.
|
||||
|
||||
**같은 커밋에서 해야 할 일:**
|
||||
1. 새 `index.ts` 8개를 `git add` 한다 (추적되지 않으면 `git ls-files`에 안 잡혀서 오히려 통과하지만, 커밋하는 순간 깨진다).
|
||||
2. `docs/reviews/adapters/INVENTORY.md`에 행 8개 추가:
|
||||
|
||||
| 추가할 경로 | 상세 리뷰 링크 (같은 그룹 기존 행과 동일하게) |
|
||||
|---|---|
|
||||
| `src/adapters/auth/index.ts` | `[Network/state](./01-network-and-state.md)` — 기존 행 `:11` |
|
||||
| `src/adapters/diagnostics/index.ts` | `[Network/state](./01-network-and-state.md)` — `:58` |
|
||||
| `src/adapters/http/index.ts` | `[Network/state](./01-network-and-state.md)` — `:59-68` |
|
||||
| `src/adapters/platform/index.ts` | `[Network/state](./01-network-and-state.md)` — `:69-73` |
|
||||
| `src/adapters/query-cache/index.ts` | `[Network/state](./01-network-and-state.md)` — `:74-78` |
|
||||
| `src/adapters/telemetry/index.ts` | `[Network/state](./01-network-and-state.md)` — `:119` |
|
||||
| `src/adapters/service-worker/index.ts` | `[Worker/push](./05-service-worker-and-web-push.md)` — `:96-101` |
|
||||
| `src/adapters/storage/index.ts` | `[Storage/files](./03-storage-and-browser-files.md)` — `:102-103` |
|
||||
|
||||
3. `docs/reviews/adapters/INVENTORY.md:132`의 `합계: **120/120**` → `합계: **128/128**`.
|
||||
|
||||
행 번호(`| N |`)는 `inventoryRows`(`scripts/check-adapter-inventory.ts:33-40`)가 아래 정규식으로 **경로만** 뽑고 순서·연속성은 검사하지 않는다.
|
||||
|
||||
```
|
||||
^\|\s*\d+\s*\|\s*`([^`]+)`\s*\|
|
||||
```
|
||||
|
||||
그래도 읽는 사람을 위해 정렬 위치에 끼워 넣고 번호를 다시 매기는 걸 권한다.
|
||||
|
||||
### 위험 4 — `tsconfig.app.json` 제외 파일
|
||||
|
||||
`tsconfig.app.json:18`이 `src/adapters/service-worker/service-worker-entry.ts`를 제외한다. `service-worker/index.ts`가 이 파일을 참조하면 app 타입체크가 제외 대상을 끌어들인다. §3.6의 배럴은 참조하지 않는다(그 파일은 export가 0개다). 나중에 누가 "완전성"을 이유로 추가하지 않도록 배럴에 주석을 남기는 것도 방법이다.
|
||||
|
||||
### 부수 발견 (이 문서 범위 밖, 별도 티켓)
|
||||
|
||||
1. `src/adapters/telemetry/best-effort-telemetry.ts:49` — 커널 심볼 재수출. 소비자 0, 같은 파일 `:8`에 import가 이미 있음. 삭제 후보.
|
||||
2. `src/adapters/browser-files/index.ts:1-16` — 배럴이 application 포트 타입을 재수출. 어댑터 배럴이 하위 레이어의 통로가 되는 형태.
|
||||
3. `src/adapters/storage/opfs/index.ts` — `tests/unit/opfs-byte-store.test.ts:25,28`이 쓰는 `OPFS_WORKER_PROTOCOL_VERSION`·`PreparePhysicalObjectRequest`·`writeWithSyncAccessHandle` 3개가 빠져 있다.
|
||||
4. `.dependency-cruiser.json:141` — `^src/adapters/web-worker` 를 겨누는 규칙이 있으나 해당 디렉터리는 존재하지 않는다(어댑터 그룹은 16개). 예방 규칙인지 잔재인지 확인 필요.
|
||||
|
||||
---
|
||||
|
||||
## 7. 심볼 존재 검증 (기계 대조)
|
||||
|
||||
제시한 8개 배럴의 **모든 재수출 심볼 82개**를, 각 대상 파일의 실제 `export` 선언과 이름 단위로 대조했다.
|
||||
|
||||
- 대조 방법: 각 `export { ... } from "./X.ts"` 블록의 이름을 뽑아, `src/adapters/<group>/X.ts`에서 `^export (declare )?(async function|function|const|let|var|class|type|interface|enum) <name>` 으로 선언된 이름 집합에 들어 있는지 확인.
|
||||
- 결과: **82/82 존재. 누락 0, 오타 0.**
|
||||
- 대상 파일 존재 여부도 함께 확인(8개 배럴이 참조하는 소스 파일 16개 전부 존재).
|
||||
|
||||
그룹별 심볼 수: auth 7, diagnostics 5, telemetry 5, platform 16, query-cache 13, storage 2, service-worker 11, http 23.
|
||||
|
||||
---
|
||||
|
||||
## 8. 실행 체크리스트 (한 커밋)
|
||||
|
||||
1. `src/adapters/{auth,diagnostics,http,platform,query-cache,service-worker,storage,telemetry}/index.ts` 8개 생성 (§3 내용 그대로).
|
||||
2. §4.1 표의 15줄 치환. `src/bootstrap/runtime-adapters.ts`는 아래에서 위로 편집하거나 `:1-29` 블록 전체를 다시 쓴다.
|
||||
3. (선택) `.storybook/preview.tsx:5,6` 2줄 치환.
|
||||
4. `.dependency-cruiser.json`에 §5.1 규칙 추가 (현재 `:193`과 `:194` 사이).
|
||||
5. `docs/reviews/adapters/INVENTORY.md`에 행 8개 추가 + `:132`의 합계를 `128/128`로.
|
||||
6. `docs/architecture/layers.md`에 "어댑터 그룹의 공개 표면은 그 그룹의 `index.ts`다. 커널은 예외로 파일 단위로 공유된다"를 한 문단 추가 (`:29-45`의 adapter kernel 절 뒤). 이 문서 규칙들은 실행 규칙과 짝을 이루게 되어 있다(`layers.md:39-45`).
|
||||
7. 게이트 실행: `check:architecture` → `check:types:app` → `check:adapter-inventory` → `lint` → `check:bundle` → `test:browser-file-storage-removal`.
|
||||
마지막 두 개가 이번 변경의 실제 리스크 지점이다(§6 위험 1, 2).
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,9 @@
|
||||
export {
|
||||
createAnonymousSessionAdapter,
|
||||
createDemoSessionAdapter,
|
||||
createExternalAuthSessionAdapter,
|
||||
createUnavailableSessionAdapter,
|
||||
DEMO_AUTHORIZATION_MARKER,
|
||||
type DemoSessionAdapter,
|
||||
type ExternalSessionOwner,
|
||||
} from "./external-session-adapter.ts";
|
||||
@@ -0,0 +1,7 @@
|
||||
export {
|
||||
createDiagnosticsAdapter,
|
||||
getLastBootEvidence,
|
||||
MAX_DIAGNOSTIC_ENTRIES,
|
||||
noOpDiagnostics,
|
||||
recordBootFailure,
|
||||
} from "./bounded-diagnostics.ts";
|
||||
@@ -0,0 +1,39 @@
|
||||
/**
|
||||
* §7–§8. 권장 경로는 V3 계약 실행기(`createContractHttpExecutor`)다. 설치된
|
||||
* 계약과 타입 입력을 받아 상한·전체 데드라인·재시도 권한·효과 확실성 판정을
|
||||
* 런타임이 소유한다.
|
||||
*/
|
||||
export {
|
||||
createContractHttpExecutor,
|
||||
type AuthIntegrationFailureReason,
|
||||
type AuthOperationContext,
|
||||
type CancellationOwner,
|
||||
type ContractHttpExecutor,
|
||||
type ContractHttpExecutorDependencies,
|
||||
type HttpContractViolation,
|
||||
type HttpContractViolationKind,
|
||||
type HttpEffectCertainty,
|
||||
type HttpExecutionContext,
|
||||
type HttpExecutionObservation,
|
||||
type HttpExecutionOutcome,
|
||||
type HttpTransportFailure,
|
||||
type SafeResponseMetadata,
|
||||
} from "./http-execution-v3.ts";
|
||||
/** V3 `attachCredentials` 콜백이 반환해야 하는 결과 타입. */
|
||||
export type { CredentialPatchOutcome } from "./http-contract-bridge.ts";
|
||||
/**
|
||||
* V2 legacy. operationId + `LegacyHttpInput`으로 호출하는 범용 클라이언트다.
|
||||
* 새 코드는 위의 V3 실행기를 쓴다. 남아 있는 이유는 계약이 아직 없는
|
||||
* 오퍼레이션을 위한 이행 경로이기 때문이다.
|
||||
*/
|
||||
export {
|
||||
createHttpClient,
|
||||
type HttpClient,
|
||||
type HttpClientDependencies,
|
||||
type HttpFailure,
|
||||
type HttpResult,
|
||||
type LegacyHttpInput,
|
||||
type Scheduler,
|
||||
} from "./client.ts";
|
||||
/** V2 `HttpClient.execute`의 첫 인자 타입. */
|
||||
export type { OperationRequestInput } from "./request-builder.ts";
|
||||
@@ -0,0 +1,31 @@
|
||||
/**
|
||||
* 어댑터 커널의 공개 경계.
|
||||
*
|
||||
* `src/adapters/**` 안에서는 이 배럴을 쓰지 않는다. 커널 프리미티브는 파일
|
||||
* 경로로 직접 import한다 — `scripts/check-adapter-inventory.ts:63-83`이
|
||||
* `platform/abortable-operation.ts`로 해석되는 specifier를 네 소비자에게
|
||||
* 요구하고, `.dependency-cruiser.json`의 kernel carve-out도 폴더 단위다.
|
||||
* 이 배럴은 bootstrap·features·tests 같은 그룹 바깥 소비자를 위한 문이다.
|
||||
*/
|
||||
export {
|
||||
compensateLateHandle,
|
||||
createAbortableOperation,
|
||||
snapshotAbortTimers,
|
||||
type AbortableOperation,
|
||||
type AbortableOperationInput,
|
||||
type AbortRace,
|
||||
type AbortTerminalReason,
|
||||
type AbortTimerSnapshot,
|
||||
} from "./abortable-operation.ts";
|
||||
export { assertBoundedCapacity } from "./bounded-capacity.ts";
|
||||
export {
|
||||
createBrowserLifecycleRuntime,
|
||||
type BrowserLifecycleEvent,
|
||||
type BrowserLifecycleRuntime,
|
||||
type BrowserLifecycleSnapshot,
|
||||
} from "./browser-lifecycle.ts";
|
||||
export {
|
||||
createBrowserMutationIntentFactory,
|
||||
type BrowserMutationIntentFactoryDependencies,
|
||||
} from "./browser-mutation-intent-factory.ts";
|
||||
export { systemClock } from "./system-clock.ts";
|
||||
@@ -0,0 +1,21 @@
|
||||
export {
|
||||
createConditionalValidatorStore,
|
||||
type ConditionalValidatorBinding,
|
||||
type ConditionalValidatorStore,
|
||||
} from "./conditional-validator-store.ts";
|
||||
export { createCursorPaginationRuntime } from "./cursor-pagination-runtime.ts";
|
||||
export {
|
||||
createServerStateScopeRuntime,
|
||||
type ScopeResetParticipant,
|
||||
type ServerStateScopeDependencies,
|
||||
} from "./server-state-scope-runtime.ts";
|
||||
export {
|
||||
createTanStackCacheCoordinator,
|
||||
type TanStackCacheCoordinatorDependencies,
|
||||
} from "./tanstack-cache-coordinator.ts";
|
||||
export {
|
||||
createQueryCacheAdapter,
|
||||
createQueryClient,
|
||||
QUERY_CACHE_DEFAULTS,
|
||||
type QueryCacheDependencies,
|
||||
} from "./tanstack-query-cache.ts";
|
||||
@@ -0,0 +1,31 @@
|
||||
/**
|
||||
* 이 배럴은 그룹 바깥(bootstrap, tests)을 위한 문이다. 워커 realm 진입점
|
||||
* `service-worker-entry.ts`는 이 배럴을 쓰지 않고 파일을 직접 import한다.
|
||||
*
|
||||
* 그래서 `tsconfig.service-worker.json`의 `exclude`에 이 파일이 들어 있다.
|
||||
* 그 설정은 `src/adapters/service-worker` 폴더를 통째로 WebWorker lib로
|
||||
* 컴파일하면서 페이지 realm 파일(`service-worker-page-controller.ts`,
|
||||
* `service-worker-removal.ts`)만 빼는 구조다. 배럴이 제외되지 않으면 그
|
||||
* 페이지 realm 파일을 다시 끌어들여 `document`를 찾지 못한다.
|
||||
* 타입 커버리지는 `tsconfig.app.json`이 이 배럴을 포함하므로 유지된다.
|
||||
*
|
||||
* `service-worker-entry.ts`도 여기서 참조하지 않는다. `tsconfig.app.json:18`이
|
||||
* 제외한 파일이고, export가 0개이므로 넣을 것도 없다.
|
||||
*/
|
||||
export {
|
||||
createServiceWorkerRuntime,
|
||||
type WorkerClientLike,
|
||||
type WorkerRuntimeConfig,
|
||||
type WorkerScopeLike,
|
||||
} from "./service-worker-lifecycle.ts";
|
||||
export {
|
||||
createServiceWorkerPageController,
|
||||
type ActivationBlocker,
|
||||
type PageControllerDependencies,
|
||||
} from "./service-worker-page-controller.ts";
|
||||
export {
|
||||
createNonceRegistry,
|
||||
createServiceWorkerMessage,
|
||||
parseServiceWorkerMessage,
|
||||
type ParsedMessage,
|
||||
} from "./service-worker-protocol.ts";
|
||||
@@ -0,0 +1,12 @@
|
||||
/**
|
||||
* 최상위 storage 배럴은 Web Storage 어댑터만 내보낸다.
|
||||
*
|
||||
* `./indexeddb/index.ts`와 `./opfs/index.ts`를 여기서 재수출하지 말 것.
|
||||
* `scripts/test-browser-file-storage-runtime-removal.ts:23-34`가 그 두 폴더만
|
||||
* 삭제한 뒤 잔존 import를 예외로 잡는다 — 재수출하면 제거 드릴이 죽는다.
|
||||
* 두 서브배럴이 각자 제거 가능한 런타임의 경계다.
|
||||
*/
|
||||
export {
|
||||
createBrowserStorageAdapter,
|
||||
type BrowserStorageDependencies,
|
||||
} from "./browser-storage-adapter.ts";
|
||||
@@ -0,0 +1,7 @@
|
||||
export {
|
||||
createTelemetryAdapter,
|
||||
MAX_TELEMETRY_QUEUE,
|
||||
noOpTelemetry,
|
||||
type TelemetryAdapter,
|
||||
type TelemetryAdapterOptions,
|
||||
} from "./best-effort-telemetry.ts";
|
||||
Reference in New Issue
Block a user