모듈별 구조 리뷰에서 나온 두 설계를 spec으로 남기고, 그중 배럴 경계 작업의 실행 계획을 쓴다. 배럴 규칙은 실제 import 그래프에 돌려 위반 15건이 치환 대상 15줄과 일치함을 확인했고, 재수출할 심볼 82개는 실제 export와 대조했다. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
30 KiB
어댑터 배럴 공개 경계 확립 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
Global Constraints
- 기준선(2026-09-16,
develop5434760):check:architecturePASS(297 모듈 / 897 의존 / 미해결 0),check:types:appPASS,check:adapter-inventoryPASS(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:
export {
createAnonymousSessionAdapter,
createDemoSessionAdapter,
createExternalAuthSessionAdapter,
createUnavailableSessionAdapter,
DEMO_AUTHORIZATION_MARKER,
type DemoSessionAdapter,
type ExternalSessionOwner,
} from "./external-session-adapter.ts";
src/adapters/diagnostics/index.ts:
export {
createDiagnosticsAdapter,
getLastBootEvidence,
MAX_DIAGNOSTIC_ENTRIES,
noOpDiagnostics,
recordBootFailure,
} from "./bounded-diagnostics.ts";
src/adapters/telemetry/index.ts:
export {
createTelemetryAdapter,
MAX_TELEMETRY_QUEUE,
noOpTelemetry,
type TelemetryAdapter,
type TelemetryAdapterOptions,
} from "./best-effort-telemetry.ts";
src/adapters/storage/index.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:
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:
/**
* 어댑터 커널의 공개 경계.
*
* `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:
/**
* `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:
/**
* §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 배열 맨 앞에 추가한다:
"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로 실패한다. 샌드박스가 네트워크 네임스페이스를 못 만들기 때문이며develop5434760기준선에서도 동일하게 16건 실패한다(2026-09-16 확인). 이 드릴은 내부적으로test:unit을 돌리므로 그 16건 때문에 항상 exit 1이 된다. 판정 기준은 드릴의 exit code가 아니라error TS0건과still imported부재다. CI 환경에서는 전체 PASS를 확인할 것.
- Step 9: 커밋
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:
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:
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한 경우 순서대로 시도한다:
package.json에"sideEffects": false를 추가한다. 추가 전에src/adapters아래 최상위 부작용이 있는지 확인하고, CSS import가 있으면"sideEffects": ["*.css"]형태로 보존한다.- 그래도 넘치면
src/adapters/service-worker/index.ts에서service-worker-lifecycle.ts블록을 빼고, 워커 realm은 파일 직접 import를 유지한다. 그리고 Task 3의 게이트 규칙from.pathNot에 워커 진입점을 추가한다. (근거: realm이 다르면 문도 다르다 —platform과 같은 논리.) - 1·2로 안 되면 이 태스크를 중단하고 보고한다. 예산 초과를 안고 진행하지 않는다.
- Step 7: 단위·통합 테스트
Run: corepack pnpm test:unit && corepack pnpm test:integration
Expected: PASS. 이 태스크는 import 경로만 바꿨으므로 테스트 결과가 달라질 이유가 없다. 깨지면 배럴이 내보내는 심볼이 원본과 다른 것이다.
- Step 8: 커밋
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:
export function createFixtureHttpClient(): string {
return "fixture";
}
tests/fixtures/architecture/dependency-graph/barrel/adapters/http/index.ts:
export { createFixtureHttpClient } from "./client.ts";
tests/fixtures/architecture/dependency-graph/barrel/bootstrap/compose-deep.ts — 규칙이 잡아야 할 형태:
import { createFixtureHttpClient } from "../adapters/http/client.ts";
export const deepComposition = createFixtureHttpClient;
tests/fixtures/architecture/dependency-graph/barrel/bootstrap/compose-barrel.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 앞에 넣는다.
{
"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를 추가한다:
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 배열에 두 항목을 추가한다:
{
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가 항상 통과하면 회귀 검사가 아니다.
# 규칙 이름을 일시적으로 바꿔 매칭되지 않게 한다
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: 커밋
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" 절 다음에 아래를 넣는다:
## 어댑터 그룹의 공개 경계
각 어댑터 그룹의 공개 표면은 그 그룹의 `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 끝에 추가:
## 테스트의 어댑터 import
`tests/` 아래 어댑터 import는 배럴로 일괄 이관하지 않는다. `check:architecture`는
`src`만 스캔하므로 강제되지 않고, 단위 테스트의 상당수가 배럴에 없는 내부
심볼을 의도적으로 겨눈다.
어떤 테스트 파일을 **다른 이유로** 수정하거나 분할할 때, 그 파일이 쓰는
심볼이 해당 그룹 배럴에 있으면 그 파일 안에서만 배럴 경로로 바꾼다.
배럴에 없는 심볼이면 깊은 경로를 유지한다. 배럴에 추가하고 싶으면 그 심볼이
공개 표면임을 먼저 논증한다 — 테스트 편의로 배럴을 키우면 배럴이 경계가
아니라 재수출 덤프가 된다.
- Step 3: 문서 게이트 확인
Run: corepack pnpm lint && corepack pnpm check:architecture
Expected: PASS. (문서 링크 검증이 있으면 corepack pnpm verify:documentation도 돌린다.)
- Step 4: 커밋
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여야 한다.
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이어야 한다.
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경로) — 각각 별도 티켓.