{ "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//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" } } }