fix(boundaries): allow feature-to-feature imports of public exports

turbo.json's boundary config already allows `feature -> feature`, and
the cross-feature event system depends on it — a consumer must import
the publisher's event contract from `@repo/<publisher>`. But the ESLint
boundaries config, ADR-010, and AGENTS.md still declared
`feature -> [core, tooling]`, contradicting turbo.json and the shipped
code (marketing-pages imports @repo/auth).

Align all three to turbo.json: a feature may import another feature's
published public exports. Internals stay sealed by the `exports` map,
and cross-feature behaviour still flows through IEventBus.
This commit is contained in:
2026-05-21 14:30:37 +02:00
parent f69b53d73e
commit f3182537c1
3 changed files with 36 additions and 11 deletions

View File

@@ -64,7 +64,7 @@ See `docs/guides/runbook.md` for the full workflow.
| app | app, core, core-composition, feature, tooling |
| core-composition | core, core-composition, feature, tooling |
| core | core, core-composition, tooling |
| feature | core, tooling |
| feature | core, feature, tooling |
| tooling | tooling |
### Composition exceptions
@@ -81,7 +81,7 @@ No other cross-package boundary deviations are permitted.
2. **`exports` maps** feature packages expose `.`, `./ui`, `./cms`, `./api`, `./di/bind-production`, `./di/bind-dev-seed` only; no deep source paths exist.
3. **ESLint `eslint-plugin-boundaries`** (lint-time) configured in `packages/core-eslint/`:
- Enforces the five-tag rules at linting
- Feature packages may import from `core` and tooling only.
- Feature packages may import from `core`, tooling, and other features' public exports (the `@repo/<feature>` contract barrel e.g. an event contract a consumer subscribes to). They must not reach another feature's internals (the `exports` map seals those) or call its use cases directly cross-feature behaviour flows through `IEventBus`.
- `core-shared`, `core-ui` may not import any feature.
- `core-api` restricted to `@repo/<feature>/api` imports.
- `core-cms` restricted to `@repo/<feature>/cms` imports.

View File

@@ -38,6 +38,7 @@ ADR-006 mentioned three tags. This ADR refines the model to five, distinguishing
- **tooling** — `packages/core-eslint`, `core-typescript`
Why `core-trpc` is `core-composition`:
- `core-trpc` imports `@repo/core-api` (the tRPC app router)
- `core-api` imports feature routers from `@repo/<feature>/api`
- Therefore, `core-trpc` transitively depends on features through the `AppRouter` type
@@ -51,17 +52,40 @@ Why `core-trpc` is `core-composition`:
{
"boundaries": {
"tags": {
"app": { "dependencies": { "allow": ["app", "core", "core-composition", "feature", "tooling"] } },
"core-composition": { "dependencies": { "allow": ["core", "core-composition", "feature", "tooling"] } },
"core": { "dependencies": { "allow": ["core", "core-composition", "tooling"] } },
"feature": { "dependencies": { "allow": ["core", "tooling"] } },
"app": {
"dependencies": {
"allow": ["app", "core", "core-composition", "feature", "tooling"]
}
},
"core-composition": {
"dependencies": {
"allow": ["core", "core-composition", "feature", "tooling"]
}
},
"core": {
"dependencies": { "allow": ["core", "core-composition", "tooling"] }
},
"feature": {
"dependencies": { "allow": ["core", "feature", "tooling"] }
},
"tooling": { "dependencies": { "allow": ["tooling"] } }
}
}
}
```
> **Amendment (2026-05-21) — feature → feature.** The `feature` tag's allow-list
> includes `feature`. A feature package may depend on another feature's
> _published public exports_ — the `@repo/<feature>` contract barrel (types,
> schemas, errors, event contracts). This is required by the cross-feature
> event system (ADR-015): a consumer must import the publisher's event
> contract. Two guardrails remain: each feature's `exports` map seals its
> internals (only `.`, `./ui`, `./api`, `./cms`, `./di/bind-*` are reachable),
> and cross-feature _behaviour_ still flows through `IEventBus` — a feature
> never imports and invokes another feature's use cases directly.
2. **Per-package `turbo.json`** declares the package's tag:
```json
// packages/blog/turbo.json
{ "extends": ["../../../turbo.json"], "tasks": { /* ... */ } }
@@ -76,11 +100,12 @@ Why `core-trpc` is `core-composition`:
Both ESLint and Turborepo enforce the same five-tag rules, but for different reasons:
| Layer | Runs | Sees | Exempts via |
|---|---|---|---|
| --------------------------------- | ---------------- | ------------------------------------------------------- | ---------------------------------- |
| ESLint `eslint-plugin-boundaries` | lint-time | Direct imports per file | `// @boundaries-ignore` comments |
| Turborepo `boundaries` | build-graph time | Entire workspace dependency graph including transitives | None (graph-based, not per-import) |
**Why both?**
- **ESLint** provides fine-grained control (file-level exemptions) and immediate feedback during development
- **Turborepo** catches transitive issues (e.g., feature reach through composition packages) and missing declarations

View File

@@ -96,7 +96,7 @@ export default [
from: "app",
allow: ["app", "core", "core-composition", "feature", "tooling"],
},
{ from: "feature", allow: ["core", "tooling"] },
{ from: "feature", allow: ["core", "feature", "tooling"] },
{ from: "core", allow: ["core", "tooling"] },
{ from: "core-composition", allow: ["core", "feature", "tooling"] },
{ from: "tooling", allow: ["tooling"] },