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` 경로) — 각각 별도 티켓.
|
||||
Reference in New Issue
Block a user