docs: reflect tooling-package rename + Turbo boundaries enforcement
- Rename eslint-config → core-eslint, typescript-config → core-typescript in all docs (package map, AGENTS.md, overview.md, dependency-flow.md, etc.) - Document the five-tag model (app, feature, core, core-composition, tooling) — refinement of ADR-006's three-tag mention - Document core-trpc's core-composition tag (transitively reaches features through core-api's AppRouter type) - Note Turborepo boundaries as a second enforcement layer alongside ESLint - Add ADR-010 explaining the two-layer enforcement decision and five-tag refinement Files updated: - docs/architecture/overview.md: package map, enforcement layers, five-tag section - docs/architecture/dependency-flow.md: boundary rules, enforcement strategy - docs/architecture/vertical-feature-spec.md: package names, five-tag model - AGENTS.md: package map, boundary rules, commands - CLAUDE.md: tooling package names, quick-start command - packages/core-eslint/AGENTS.md: tag clarification - packages/core-typescript/AGENTS.md: tag clarification - packages/core-api/AGENTS.md: core-composition tag - packages/core-cms/AGENTS.md: core-composition tag - packages/core-trpc/AGENTS.md: core-composition tag + rationale - docs/decisions/adr-010-turbo-boundaries.md: new ADR Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
123
docs/decisions/adr-010-turbo-boundaries.md
Normal file
123
docs/decisions/adr-010-turbo-boundaries.md
Normal file
@@ -0,0 +1,123 @@
|
||||
# ADR-010: Turborepo boundaries alongside ESLint enforcement
|
||||
|
||||
**Status:** Accepted
|
||||
**Date:** 2026-05-04
|
||||
**Supersedes:** ADR-006 (partial refinement; not a contradiction)
|
||||
|
||||
## Context
|
||||
|
||||
ADR-006 established the vertical-feature monorepo with three tags (`app`, `feature`, `core`). ESLint with `eslint-plugin-boundaries` was introduced to enforce direct-import boundaries at lint time, catching violations like:
|
||||
|
||||
- Feature package importing from another feature
|
||||
- Core package importing from a feature
|
||||
- Deep imports past public `exports` map boundaries
|
||||
|
||||
However, ESLint has two intrinsic limitations:
|
||||
|
||||
1. **No transitive enforcement** — If `core-trpc` imports `core-api` (which imports feature routers), ESLint sees only the direct edge `core-trpc` → `core-api`. The transitive reach into features is invisible. A package can declare `@repo/feature-x` as a dependency without ever importing it directly.
|
||||
|
||||
2. **No declared-dep enforcement** — A `package.json` dependency that doesn't match an actual import goes undetected. Conversely, a `package.json` dependency might be missing but the transitive graph still allows the code to work.
|
||||
|
||||
The result: two types of dependency drift escape lint-time checking:
|
||||
|
||||
- A composition package's transitive reach into features (real problem: `core-trpc` → `core-api` → `feature-blog-routers`)
|
||||
- Incorrect or missing `package.json` declarations
|
||||
|
||||
## Decision
|
||||
|
||||
Add Turborepo's `boundaries` feature as a second enforcement layer running at **build-graph time** (before build), parallel to ESLint's lint-time checks.
|
||||
|
||||
### Five-tag model (refined from ADR-006)
|
||||
|
||||
ADR-006 mentioned three tags. This ADR refines the model to five, distinguishing composition packages explicitly:
|
||||
|
||||
- **app** — `apps/web-next`, `apps/web-tanstack`, `apps/cms`, `apps/storybook`
|
||||
- **core** — `packages/core-shared`, `core-ui` (pure foundation, no transitive feature reach)
|
||||
- **core-composition** — `packages/core-api`, `core-cms`, `core-trpc` (composition or transitively reach features)
|
||||
- **feature** — `packages/auth`, `blog`, `media`, `marketing-pages`, `navigation`
|
||||
- **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
|
||||
- This violates ADR-006's "core → NOT feature" rule if we treat `core-trpc` as plain `core`
|
||||
- Solution: tag `core-trpc` as `core-composition` to make the transitive reach explicit and allowed
|
||||
|
||||
### Implementation
|
||||
|
||||
1. **Root `turbo.json`** declares boundary rules as a `boundaries.tags` config:
|
||||
```json
|
||||
{
|
||||
"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"] } },
|
||||
"tooling": { "dependencies": { "allow": ["tooling"] } }
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
2. **Per-package `turbo.json`** declares the package's tag:
|
||||
```json
|
||||
// packages/blog/turbo.json
|
||||
{ "extends": ["../../../turbo.json"], "tasks": { /* ... */ } }
|
||||
// package.json
|
||||
{ "turbo": { "tasks": { "build": { /* ... */ } }, "tags": ["feature"] } }
|
||||
```
|
||||
|
||||
3. **CLI validation** — `pnpm turbo boundaries` runs the check in <1 second without building anything
|
||||
|
||||
### Two layers, not one
|
||||
|
||||
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
|
||||
|
||||
**Enforcement** — CI runs both: `pnpm lint` (includes ESLint) and `pnpm turbo boundaries`.
|
||||
|
||||
## Consequences
|
||||
|
||||
1. **Two independent checks** — developers get immediate ESLint feedback and a final Turbo gate in CI
|
||||
2. **Both must stay in sync** — when adding a new tag rule, update both:
|
||||
- `packages/core-eslint/base.js` (ESLint `eslint-plugin-boundaries` config)
|
||||
- Root `turbo.json` (`boundaries.tags` config)
|
||||
- Per-package `package.json` `turbo.tags` declaration
|
||||
3. **Stricter than before** — Turborepo sees transitives; some patterns that pass ESLint may fail Turbo:
|
||||
- Example: `packages/cms` might try to use `@repo/<dep>` indirectly (not importing, but depending)
|
||||
- Solution: declare the dependency explicitly or restructure the import
|
||||
4. **The five-tag model is a refinement, not a contradiction** — ADR-006 mentioned three tags; this ADR adds `core-composition` and `tooling` explicitly and moves them into the boundary enforcement
|
||||
5. **`core-trpc`'s new tag** — previously thought of as `core`, now explicitly `core-composition` to match its transitive reach; any code assuming `core-trpc` is `core` must be updated
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
1. **ESLint only** — Simple, immediate feedback. Downside: misses transitive issues; no build-graph validation.
|
||||
2. **Turborepo only** — Catches everything eventually, but slower feedback cycle; less granular exemptions.
|
||||
3. **Both in series** — ESLint first (fast), Turbo second (thorough). Chosen.
|
||||
4. **Custom graph validator** — Over-engineered; Turborepo's built-in feature is stable and designed for this.
|
||||
|
||||
## Related
|
||||
|
||||
- **ADR-006:** Vertical feature packages (the original three-tag model)
|
||||
- **ADR-009:** Integrations folder naming
|
||||
- **`packages/core-eslint/base.js`** — ESLint configuration
|
||||
- **Root `turbo.json`** — Turborepo configuration with boundaries rules
|
||||
- **`docs/architecture/overview.md`** — Package map and five-tag summary
|
||||
- **`docs/architecture/dependency-flow.md`** — Enforcement layers and rules
|
||||
- **`AGENTS.md`** — Per-package boundary documentation
|
||||
|
||||
## Timeline
|
||||
|
||||
- **2026-05-04** — Turborepo boundaries added alongside existing ESLint enforcement
|
||||
- **CI integration** — `pnpm turbo boundaries` added to lint stage
|
||||
- **Documentation** — Updated AGENTS.md, overview.md, dependency-flow.md, vertical-feature-spec.md
|
||||
Reference in New Issue
Block a user