Skip to content

Seams

Some of your code is meant to be reused anywhere. Other code knows your business. The line between the two is a seam.

A bad import across a seam is caught by a boundary, which is a note saying which groups of files may reach which others. Domain knowledge has another way across, and that one leaves no import behind.

Here the domain owns a set of order states:

src/domain/order.ts
export type OrderStatus = "awaiting_payment" | "shipped" | "refunded";

And a component decides what an unpaid order looks like, importing nothing:

src/components/StatusBadge.tsx
export const StatusBadge = ({ status }: { status: string }): string =>
status === "awaiting_payment" ? "amber" : "grey";

Nothing links these two files. There is no import between them and no shared type, so a boundary has nothing to object to. The day awaiting_payment is renamed, the badge still compiles and it is wrong.

A seam rule is what catches it. You name the group of files that is meant to be generic, and the groups whose words it may not borrow. Those groups are zones, and a zone is a name you give to a group of files, chosen by where the files sit.

seams: [{ generic: "components", domain: ["domain"] }]

Now the run says so:

generic-code-names-no-domain-concept 1 error
src/components/StatusBadge.tsx:2:14 is components and names the value "awaiting_payment", which domain owns

You never list the words yourself. A domain zone owns two things: the names its files export, and the string values its sources contain. The rule is worked out from those on every run, so a new type is covered from the moment it exists, and a deleted one stops being covered.

The explain command prints what a file may not name:

<answer>
<paths>
* src/components/StatusBadge.tsx
</paths>
<zone>components</zone>
<may-reach>
* components
* hooks
* api
</may-reach>
<may-not-name owner="domain">
* Order
* OrderStatus
* isSettled
</may-not-name>
<may-not-repeat owner="domain">
* awaiting_payment
* refunded
* shipped
</may-not-repeat>
</answer>

Please do not rename the local status variable. That hides the leak, and the two files stay exactly as coupled as they were.

Have the badge component take a tone instead, warning or neutral, and let the domain side of the call decide which one to pass. The component then knows about badges and the domain keeps its states, which is what splitting the two zones was for.

Turn it on when you have a layer meant to be reusable: an engine, a renderer or a design system. The other half is a domain that layer is meant not to know about. If every zone in your project is domain code, there is no seam to guard.

Two settings tune it. Use allow for words the two sides genuinely share, and minLiteralLength for short strings that match by accident.

seams: [{ generic: "components", domain: ["domain"], allow: ["status"], minLiteralLength: 5 }]