Skip to content

Import & placement rules

These govern the dependency graph and where files live, the boundaries that keep a layered architecture from rotting.

denyImports / allowImports

ts
{
  name: 'ui-primitive',
  files: 'components/ui/**/*.tsx',
  denyImports: ['services/**', 'store/**'],
  allowImports: ['components/ui/**', 'react'],
}

Each import specifier is reduced to a governed path before matching:

Specifier formReduced toTreated as
relative (./, ../)resolved, relative to rootinternal
alias @/services/xservices/xinternal
bare react, lodashunchangedexternal
  • denyImports matches against both the governed path and the raw specifier, so ['axios'] catches a package and ['services/**'] catches a path.
  • allowImports governs internal imports only: an internal import that matches no allow glob is flagged. Bare packages are out of its scope.

Reported as ARCH_DENIED_IMPORT.

The alias @/ is treated as the root prefix out of the box. Configurable aliases are on the roadmap.

belongsHere: the inverse check

Every other rule validates files that are already in the governed folder. belongsHere catches a file that looks governed but lives in the wrong place, the way an agent evades a rule by writing elsewhere.

ts
{
  name: 'ui-component',
  files: 'components/ui/**/*.tsx',
  belongsHere: { exportsType: 'React.FC', importsNone: ['services/**'] },
}

A file anywhere in the source universe that exports a React.FC and imports nothing from services/**, but is not under components/ui/, is flagged ARCH_MISPLACED_FILE and told to move.

FieldMeaning
exportsTypeA top-level export must be annotated with this type (folded through sameType).
importsNone…and the file imports none of these globs.

requires: mandatory siblings & companions

ts
{ name: 'block', files: 'components/block/**/*.tsx', requires: ['index.ts'] }

Each matched file must have the named siblings next to it, or ARCH_MISSING_SIBLING.

The token {stem} expands to the file's name before the first dot: the same stem definition naming.files and filename conventions use (orders.module.tsorders, Button.tsxButton). That's what dotted-role naming needs, "every feature folder holds its module, controller and service":

ts
{
  name: 'feature-module',
  files: '*/*.module.ts',
  // orders.module.ts → requires orders.controller.ts + orders.service.ts
  requires: ['{stem}.controller.ts', '{stem}.service.ts'],
}

For a plain (non-dotted) basename {stem} is the whole name, so it also covers the classic "every component has a matching story" case:

ts
{
  name: 'ui-component',
  files: 'components/ui/**/*.tsx',
  exclude: ['**/*.stories.tsx'],     // don't apply the rule to the stories themselves
  requires: ['{stem}.stories.tsx'],  // Button.tsx → requires Button.stories.tsx
}

Closing the escape hatch

Combine belongsHere + unmatched: 'error' and an agent cannot create an unchecked file: either it's under a governed glob (and must match that rule) or it's unmatched (an error) or it looks governed but is misplaced (also an error).

Released under the MIT License.