From 27ab17d6d68b355a19acd4228efe58f4280556bf Mon Sep 17 00:00:00 2001 From: DongHyeonka Date: Wed, 16 Sep 2026 18:54:17 +0900 Subject: [PATCH] feat: enforce the adapter barrel boundary in the architecture gate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 그룹 바깥에서 어댑터 내부 파일을 직접 import하면 check:architecture가 거부한다. 회귀 fixture 2개가 거부와 허용을 각각 고정한다 — 규칙 이름을 바꿔 보면 fixture 검사가 먼저 깨지는 것을 확인했다. 도착점 정규식은 중첩 수량자 대신 교대를 쓴다. dependency-cruiser의 안전 정규식 검사가 (?:[^/]+/)? 안의 + 를 unsafe로 거부하기 때문이다. Co-Authored-By: Claude Opus 5 (1M context) --- .dependency-cruiser.json | 13 +++++++++++++ scripts/check-architecture.ts | 18 +++++++++++++++++- .../barrel/adapters/http/client.ts | 3 +++ .../barrel/adapters/http/index.ts | 1 + .../barrel/bootstrap/compose-barrel.ts | 3 +++ .../barrel/bootstrap/compose-deep.ts | 3 +++ 6 files changed, 40 insertions(+), 1 deletion(-) create mode 100644 tests/fixtures/architecture/dependency-graph/barrel/adapters/http/client.ts create mode 100644 tests/fixtures/architecture/dependency-graph/barrel/adapters/http/index.ts create mode 100644 tests/fixtures/architecture/dependency-graph/barrel/bootstrap/compose-barrel.ts create mode 100644 tests/fixtures/architecture/dependency-graph/barrel/bootstrap/compose-deep.ts diff --git a/.dependency-cruiser.json b/.dependency-cruiser.json index ea20ba8..f3ffd9d 100644 --- a/.dependency-cruiser.json +++ b/.dependency-cruiser.json @@ -191,6 +191,19 @@ "pathNot": "^src/adapters/($1/|platform/|browser-file-storage/result\\.ts$|cross-context-invalidation/index\\.ts$)" } }, + { + "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$|^src/adapters/[^/]+/[^/]+/index\\.ts$" + } + }, { "name": "no-circular-dependencies", "severity": "error", diff --git a/scripts/check-architecture.ts b/scripts/check-architecture.ts index 6b2957a..ee4c14f 100644 --- a/scripts/check-architecture.ts +++ b/scripts/check-architecture.ts @@ -824,12 +824,13 @@ async function runGraphFixtureChecks(): Promise { "tests/fixtures/architecture/dependency-graph", ); const allowedRoot = resolve(fixtureRoot, "allowed"); - const [allowedGraph, unresolvedGraph, layerGraph, cycleGraph] = + 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"), ]); const allowedFiles = new Set(await listFiles(allowedRoot)); const allowedSourceFile = resolve( @@ -850,6 +851,21 @@ async function runGraphFixtureChecks(): Promise { ), ); const 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", + ), + }, { name: "explicit TS specifier resolves to a TS module", passed: allowedGraph.dependencies.some( diff --git a/tests/fixtures/architecture/dependency-graph/barrel/adapters/http/client.ts b/tests/fixtures/architecture/dependency-graph/barrel/adapters/http/client.ts new file mode 100644 index 0000000..3a414bb --- /dev/null +++ b/tests/fixtures/architecture/dependency-graph/barrel/adapters/http/client.ts @@ -0,0 +1,3 @@ +export function createFixtureHttpClient(): string { + return "fixture"; +} diff --git a/tests/fixtures/architecture/dependency-graph/barrel/adapters/http/index.ts b/tests/fixtures/architecture/dependency-graph/barrel/adapters/http/index.ts new file mode 100644 index 0000000..1ca20a3 --- /dev/null +++ b/tests/fixtures/architecture/dependency-graph/barrel/adapters/http/index.ts @@ -0,0 +1 @@ +export { createFixtureHttpClient } from "./client.ts"; diff --git a/tests/fixtures/architecture/dependency-graph/barrel/bootstrap/compose-barrel.ts b/tests/fixtures/architecture/dependency-graph/barrel/bootstrap/compose-barrel.ts new file mode 100644 index 0000000..29f769d --- /dev/null +++ b/tests/fixtures/architecture/dependency-graph/barrel/bootstrap/compose-barrel.ts @@ -0,0 +1,3 @@ +import { createFixtureHttpClient } from "../adapters/http/index.ts"; + +export const barrelComposition = createFixtureHttpClient; diff --git a/tests/fixtures/architecture/dependency-graph/barrel/bootstrap/compose-deep.ts b/tests/fixtures/architecture/dependency-graph/barrel/bootstrap/compose-deep.ts new file mode 100644 index 0000000..590199c --- /dev/null +++ b/tests/fixtures/architecture/dependency-graph/barrel/bootstrap/compose-deep.ts @@ -0,0 +1,3 @@ +import { createFixtureHttpClient } from "../adapters/http/client.ts"; + +export const deepComposition = createFixtureHttpClient;