Saltar al contenido principal

ADR-012 — App import boundaries: no app consumes another app, no app consumes the admin skin

:::note Contenido en inglés Esta página del wiki se sincroniza desde la base de conocimiento en inglés y todavía no está traducida. :::

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 admin type-* 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/ui already 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/ui when 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/admin then 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-imports forbids the promoted generic names from @tedos/ui/admin across apps/** except apps/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/admin no longer re-exports the promoted generics; every consumer (apps/admin included) imports them directly from @tedos/ui. No middle barrel anywhere (the app-local cn re-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 / Switch icon-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

OptionWhy not
Forbid the whole @tedos/ui/admin path from non-admin appsBreaks legitimate, intentional reuse (CSS tokens, screens, shell chrome); contradicts "do not silently rip out".
Turborepo boundaries tags instead of ESLintHeavier; can't express per-export (importNames) granularity that separates generic primitives from admin-customized ones on the same path.
Copy the primitives into each appViolates 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)