Files
clean-architecture-frontend…/.dependency-cruiser.json
T
DongHyeonkaandClaude Opus 5 27ab17d6d6 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>
2026-09-16 18:54:17 +09:00

239 lines
8.4 KiB
JSON

{
"forbidden": [
{
"name": "domain-is-framework-neutral",
"severity": "error",
"from": {
"path": "^src/domain"
},
"to": {
"path": "^(src/(application|presentation|adapters|bootstrap)|react|react-dom|@tanstack)"
}
},
{
"name": "application-does-not-know-concrete-runtime",
"severity": "error",
"from": {
"path": "^src/application"
},
"to": {
"path": "^(src/(presentation|adapters|bootstrap)|react|react-dom|@tanstack)"
}
},
{
"name": "presentation-does-not-know-adapters",
"severity": "error",
"from": {
"path": "^src/presentation/(?!adapters/query)"
},
"to": {
"path": "^(src/(adapters|bootstrap)|@tanstack)"
}
},
{
"name": "page-templates-own-layout-only",
"severity": "error",
"from": {
"path": "^src/presentation/templates"
},
"to": {
"path": "^(src/(application|adapters|bootstrap)|src/presentation/adapters|@tanstack)"
}
},
{
"name": "icon-vendor-is-facade-only",
"severity": "error",
"from": {
"path": "^src",
"pathNot": "^src/presentation/design-system/icons/vendors/lucide\\.tsx$"
},
"to": {
"path": "^lucide-react$"
}
},
{
"name": "adapters-do-not-know-presentation",
"severity": "error",
"from": {
"path": "^src/adapters"
},
"to": {
"path": "^src/(presentation|bootstrap)"
}
},
{
"name": "feature-domain-is-framework-neutral",
"severity": "error",
"from": {
"path": "^src/features/[^/]+/domain"
},
"to": {
"path": "^(src/(application|presentation|adapters|bootstrap)|src/features/[^/]+/(application|adapters|presentation)|react|react-dom|@tanstack)"
}
},
{
"name": "feature-application-does-not-know-runtime",
"severity": "error",
"from": {
"path": "^src/features/[^/]+/application"
},
"to": {
"path": "^(src/(presentation|adapters|bootstrap)|src/features/[^/]+/(adapters|presentation)|react|react-dom|@tanstack)"
}
},
{
"name": "feature-presentation-does-not-know-outbound-adapters",
"severity": "error",
"from": {
"path": "^src/features/[^/]+/presentation"
},
"to": {
"path": "^(src/(adapters|bootstrap)|src/features/[^/]+/adapters|@tanstack)"
}
},
{
"name": "feature-adapters-do-not-know-presentation",
"severity": "error",
"from": {
"path": "^src/features/[^/]+/adapters"
},
"to": {
"path": "^(src/(presentation|bootstrap)|src/features/[^/]+/presentation)"
}
},
{
"name": "concrete-adapters-compose-only-in-bootstrap",
"severity": "error",
"from": {
"path": "^src/(domain|application|presentation|contracts)"
},
"to": {
"path": "^src/adapters"
}
},
{
"name": "external-contract-package-single-import-path",
"comment": "§4.1: a generated service package may only be imported from src/features/<feature>/contracts/*-contract-contribution.ts",
"severity": "error",
"from": {
"path": "^src",
"pathNot": "^src/features/[^/]+/contracts/[^/]+-contract-contribution\\.ts$"
},
"to": {
"path": "^@org-contracts/"
}
},
{
"name": "presentation-does-not-fetch-directly",
"comment": "§9.2 / appendix B: a page or hook never opens a socket, worker or HTTP adapter itself",
"severity": "error",
"from": {
"path": "^src/(presentation|features/[^/]+/presentation)"
},
"to": {
"path": "^src/adapters/(http|realtime|service-worker|web-worker|storage)"
}
},
{
"name": "generic-worker-has-no-network-or-credentials",
"comment": "§16.13 / §21.10: a CPU worker never imports HTTP, realtime or auth",
"severity": "error",
"from": {
"path": "^src/adapters/web-worker"
},
"to": {
"path": "^src/adapters/(http|realtime|auth|web-push)"
}
},
{
"name": "service-worker-entry-is-not-page-code",
"comment": "§17.2.1: the worker realm never imports React, presentation or bootstrap page code",
"severity": "error",
"from": {
"path": "^src/adapters/service-worker"
},
"to": {
"path": "^(src/(presentation|bootstrap)|react|react-dom|@tanstack)"
}
},
{
"name": "contracts-do-not-know-application",
"comment": "§4. `src/contracts` is the lower of the two packages: application reads contracts, never the other way round. Before this rule the shared Result carrier and the compatibility predicate lived in application and were imported back down by contracts, so neither package owned the shared vocabulary and the coupling was invisible to every gate.",
"severity": "error",
"from": {
"path": "^src/contracts"
},
"to": {
"path": "^src/(application|features)"
}
},
{
"name": "generic-presentation-does-not-compose-the-product",
"comment": "§4 / §9. Which features are installed is a product decision that belongs to bootstrap. Generic presentation reads the installed registries directly today; the paths below are the exact set that does so, frozen so the coupling cannot spread while the assembly is lifted into bootstrap.",
"severity": "error",
"from": {
"path": "^src/presentation/",
"pathNot": "^src/presentation/(layouts/app-shell\\.tsx|pages/(not-found-page|home-page)\\.tsx|routes/(route-contract|route-codecs|app-router|navigation-policy)\\.(ts|tsx)|i18n/catalog\\.ts|examples/platform-overview-page\\.tsx)$"
},
"to": {
"path": "^src/features/installed-"
}
},
{
"name": "adapters-do-not-know-other-concrete-adapters",
"comment": "docs/architecture/layers.md §4: a concrete adapter never depends on another concrete adapter. Only the adapter kernel is shared — `src/adapters/platform` (clock, abort primitive, capacity guard) and the browser-data result helpers. `query-cache` still reads two collaborator types from `cross-context-invalidation`; that edge is named here rather than left silent, and closes when those types are lifted to a port.",
"severity": "error",
"from": {
"path": "^src/adapters/([^/]+)/"
},
"to": {
"path": "^src/adapters/([^/]+)/",
"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",
"from": {},
"to": {
"circular": true
}
}
],
"options": {
"doNotFollow": {
"path": "node_modules"
},
"exclude": {
"path": "^(dist|artifacts|tests/fixtures)"
},
"enhancedResolveOptions": {
"exportsFields": [
"exports"
],
"conditionNames": [
"import",
"require",
"node",
"default"
]
},
"tsConfig": {
"fileName": "tsconfig.app.json"
}
}
}