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

@@ -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": { /* ... */ } }
@@ -75,12 +99,13 @@ 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) |
| 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