feat: enforce the adapter barrel boundary in the architecture gate
그룹 바깥에서 어댑터 내부 파일을 직접 import하면 check:architecture가 거부한다. 회귀 fixture 2개가 거부와 허용을 각각 고정한다 — 규칙 이름을 바꿔 보면 fixture 검사가 먼저 깨지는 것을 확인했다. 도착점 정규식은 중첩 수량자 대신 교대를 쓴다. dependency-cruiser의 안전 정규식 검사가 (?:[^/]+/)? 안의 + 를 unsafe로 거부하기 때문이다. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
1606d9b0c1
commit
27ab17d6d6
@@ -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",
|
||||
|
||||
@@ -824,12 +824,13 @@ async function runGraphFixtureChecks(): Promise<GraphFixtureResult> {
|
||||
"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<GraphFixtureResult> {
|
||||
),
|
||||
);
|
||||
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(
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
export function createFixtureHttpClient(): string {
|
||||
return "fixture";
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { createFixtureHttpClient } from "./client.ts";
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
import { createFixtureHttpClient } from "../adapters/http/index.ts";
|
||||
|
||||
export const barrelComposition = createFixtureHttpClient;
|
||||
@@ -0,0 +1,3 @@
|
||||
import { createFixtureHttpClient } from "../adapters/http/client.ts";
|
||||
|
||||
export const deepComposition = createFixtureHttpClient;
|
||||
Reference in New Issue
Block a user