ADR-012 — App import boundaries: no app consumes another app, no app consumes the admin skin
canonical· tech-lead · updated 2026-06-28 · source
- Status: Accepted
- Date: 2026-06-28 (escalated 2026-06-28, #1072)
- Owner: Tech Lead
- Related issue: #1061, escalated by #1072
Context
- Non-admin apps (
apps/console,apps/clients/comprender) were importing generic UI primitives (EmptyState,PageHeader,Input,Select,Segmented,Badge,SectionLabel, …) from@tedos/ui/admin— the admin skin package. That couples those apps to admin-specific customization (Cian accent, HugeIcons, the ink panel, the admintype-*scale) and undercuts the thin-app / "one core, many hues" architecture (ADR-008 A2/A4,color-scope.md). - José (2026-06-28): "imports between apps are mostly prohibited; outsource to a package if not present; admin inner components have their own customization."
- The neutral foundation
@tedos/uialready existed but (a) was missing some generics and (b) had stale duplicate copies of others that had diverged from the maintained admin versions — so a naive repoint would have regressed typography/contrast. This needs a recorded decision + a guard.
Decision
Generic, token-driven UI primitives live on the neutral foundation @tedos/ui; non-admin apps
import them from there, never from @tedos/ui/admin*. A lint guard enforces it.
- Promote a primitive to
@tedos/uiwhen it is skin-agnostic (no icon set, no Radix, no admin-only variant) — its visual values resolve from the consumer's token scope.@tedos/ui/adminthen re-exports it from neutral, so there is ONE source of truth. - Keep admin-only a primitive that bakes HugeIcons, Radix, or an admin-specific variant; the other app rolls its own or the primitive is later refactored to inject the dependency and promoted.
- Guard: ESLint
@typescript-eslint/no-restricted-importsforbids the promoted generic names from@tedos/ui/adminacrossapps/**exceptapps/admin/**. - The full decision tree + the promoted/kept inventory live in
.claude/rules/app-import-boundaries.md(the enforceable rule; this ADR is the why).
Scope boundaries — what this does NOT cover:
- CSS token entries (
@import '@tedos/ui/admin/styles.css') are allowed — tokens, not component reach. - Intentional whole-screen / shell reuse by white-label client apps
(
@tedos/ui/admin/screens, the admin shell) is left in place and flagged for a separate José decision — this ADR governs generic primitives + the guard, not the screens-reuse policy. (Resolved by the #1072 escalation below: each app owns its view shells.)
Escalation (#1072, José locked 2026-06-28)
The flagged screens-reuse question above is now decided. The rule escalates from "generic primitives only" to the full boundary:
- NO app consumes another app, and no app consumes the admin skin. admin/console/comprender are
all apps. Everything shared flows through
@tedos/ui(UI) +@tedos/core(logic); each app owns its customizations locally. - D1 — shared screen LOGIC →
@tedos/core(hooks/data/types/view-models); each app builds its own view shell consuming core. Not a shared neutral screen, not a blind fork. - D2 — the re-export barrel is killed.
@tedos/ui/adminno longer re-exports the promoted generics; every consumer (apps/admin included) imports them directly from@tedos/ui. No middle barrel anywhere (the app-localcnre-export shim was removed too). - Resolution tree for any app→app / app→admin smell: promote-to-
@tedos/ui(generic UI) · add-to-@tedos/core(shared logic — if it's not in core, add it; never borrow from another app) · customize-locally (genuinely app-specific). Full tree:.claude/rules/app-import-boundaries.md.
Landed in #1072 (this PR): D2 barrel kill (admin imports generics directly from @tedos/ui);
the guard gained an app→app ban (app package names + apps/** climbs, across all apps) and the
generics guard now applies to admin too; rule + ADR escalated.
Staged (tracked sub-issues of #1072) — sequential, large UI surface: extract screen logic to
@tedos/core; comprender owns its view shells (drop all @tedos/ui/admin*); relocate the console
chrome out of @tedos/ui/admin/console (it is console's own chrome, mis-homed, built on admin-only
primitives); then tighten the guard to ban the entire @tedos/ui/admin* subtree from non-admin apps.
Consequences
Positive
- Non-admin apps depend on the neutral surface; the admin skin stops leaking across app boundaries.
- Promoted primitives are single-sourced (admin re-exports neutral) → no more stale-copy divergence.
- The boundary is enforced by
pnpm lint, not by review discipline.
Negative / trade-offs
- The forbidden-name list in the guard must grow each time a generic is promoted (one-line PR cost).
- A few conceptually-generic primitives (
ReadError,Switch,DataTable*) stay admin-only for now because they bake HugeIcons / a table family; non-admin apps still reach for them until they are made icon-injectable. Documented, not hidden.
Follow-ups required
- Make
ReadError/Switchicon-injectable and promote them (then extend the guard list). - José decision: should client apps keep reusing admin screens/shell, or own their screens?
Alternatives considered
| Option | Why not |
|---|---|
Forbid the whole @tedos/ui/admin path from non-admin apps | Breaks legitimate, intentional reuse (CSS tokens, screens, shell chrome); contradicts "do not silently rip out". |
Turborepo boundaries tags instead of ESLint | Heavier; can't express per-export (importNames) granularity that separates generic primitives from admin-customized ones on the same path. |
| Copy the primitives into each app | Violates monorepo rule #4 (one workspace per concern) and re-creates the divergence problem this ADR fixes. |
References
- Rule:
.claude/rules/app-import-boundaries.md .claude/rules/monorepo-turbo.md(workspace boundaries),.claude/rules/react-style-guidelines.md(ADR-008, thin apps),.claude/rules/color-scope.md(neutral foundation vs admin skin)decisions/log.md(2026-06-28)