# DR-57 — A domain's public surface is one barrel file, and the lint rule enforces exactly that

**Raised:** 10 September 2026, while building notes and tags (prompt 3, M2).
**Status:** decided — Sky, 10 September 2026.
**Touches:** every domain. Amends the enforcement of ADR-01, not its intent.

## The question

The architecture says a domain may read another domain's data by calling that domain's service
client — interaction rule 2 calls it "the only operational path for cross-domain data". The enforced
lint rule, `tools/eslint-rules/no-cross-domain-imports.js`, has no such exception: it blocks **every**
import from one domain into another, including a domain's own exported query functions and
components.

Notes and tags are the first feature to attempt a cross-domain read, so nobody had hit the gap.

## Why it needed answering

**The rule and the architecture disagree, and the rule is the stricter of the two.** ADR-01's intent
is that contexts talk through *defined interfaces* rather than reaching into each other's tables. A
`cl_` page calling a `com_` query function is that interface working as designed. The rule cannot
tell it apart from reaching into another domain's internals, so it forbids both.

Left alone, this does not produce a purer codebase. It produces a codebase where the boundary is
respected in `src/domains/**` and quietly evaded next to it — because the work still has to ship,
and every route around the rule lands somewhere the rule does not look.

## Decision

**Each domain declares its public surface in one barrel — `src/domains/<domain>/index.ts` — and
`no-cross-domain-imports.js` permits a cross-domain import only from that path.** Every deeper path
stays blocked exactly as it is today.

"Defined interface" stops being a phrase in a document and becomes a file that can be opened, read
and reviewed. A domain with nothing to offer other domains has no barrel, and nothing changes for it.

**The data rule is untouched and is not what this relaxes.** No `cl_` file may query a `com_` table.
No PostgREST nested select may cross a prefix, however inviting the foreign key looks. The barrel
exports *functions*; the reading happens inside the domain that owns the tables. If anything the
rule gets easier to hold, because a legitimate path now exists and nobody needs an illegitimate one.

## Beat

**A domain-neutral component location** — moving the timeline and tag components to
`src/components/`. Rejected: the dependency does not disappear, it moves somewhere the linter does
not watch, and it inverts ownership. `DataTable` is genuinely shared because it knows nothing about
any domain; it takes columns and rows. A timeline knows what a note *is*. The failure mode is that
every future domain with UI needs the same exit, and `src/components/` becomes where domain code
goes to escape the rule.

**Composition at the route layer** — keeping pages domain-pure and injecting the other domain's
components from `src/routes/`. The most defensible of the alternatives, and a real pattern: the outer
layer wires domains together while each stays ignorant of the others. Rejected on scale. M3's detail
panels have tabs; a slot per cross-domain surface threaded through three page APIs today becomes many
more later. It also works partly because routes are not linted, and it leaves the rule still
contradicting the documentation.

**A filename-pattern allowlist** — permitting `services/*-client.ts`, `*-queries.ts`, `*.functions.ts`.
Rejected because it is a guess about what is public, and a guess about what is public is what
produced this gap.

## Consequences

- **Every domain that exposes anything needs a barrel written deliberately.** That is the work, and
  it is also the point: the surface is chosen rather than inferred from file names.
- **Barrels can create import cycles and blunt tree-shaking.** Worth watching rather than fearing at
  this size; if a cycle appears it is a signal that two domains are entangled, which is information.
- **The rule's error message must name the barrel.** A rule that says "no cross-domain imports" when
  a legitimate path exists sends the next person to the same three bad options. It should say: import
  from `@/domains/<domain>` — the domain's public surface — and nothing deeper.
- **Doing it now is cheaper than doing it in M3.** One consumer today; opportunities, activities and
  quotes all want it later.
- **Not reopened here:** `com_` maps to the *commerce* domain folder, so notes and tags live there.
  That mapping predates this decision and is left alone; if it starts to read oddly once notes attach
  to everything, it is a question for M3, not for this feature.

## Where it lands

- `tools/eslint-rules/no-cross-domain-imports.js` — the exception, and the message.
- `src/domains/commerce/index.ts` — the first barrel: the note and tag query functions and the
  components `cl_` screens render.
- A test for the rule itself: a deep cross-domain import still fails, a barrel import passes.
