Skip to main content
Amended by 0233, decisions 4 and 5: cross-folder imports use # aliases only, and ../ is banned.
Amended by 0128: enums, classes and component folders follow that ADR.
Amended 2026-10-21 (STA-219): decision 6 (utils/) is withdrawn and schema files take the .schema.ts suffix. See the amendment at the end of this page.

Context

File names, how many things a file exports, how barrels look and how modules name each other are decided on the first file or re-argued on the twentieth. Four earlier decisions covered these separately. They are one convention, and it is frozen before packages are created in bulk, because a codemod over many packages costs far more than agreeing now.

Decision

  1. Kebab-case for every file and directory under src/. No PascalCase, camelCase or underscores.
  2. One exported declaration per file, named for it, with a kind suffix where the kind is not a plain function: token.class.ts, scope.enum.ts, class-provider.interface.ts, payment.error.ts. Barrels are exempt.
  3. Barrels re-export and nothing else. index.ts is the only barrel name; it names every symbol (export type { Token } from "./token.class"), never export *, so the public surface is readable without compiling.
  4. Relative specifiers carry no extension and no trailing /index. Exempt: package specifiers, non-code assets (.css, .svg, .json), generated output, raw Node .mjs and .cjs scripts under tools/, and packages/tooling/* (loaded by plain Node, which needs full specifiers).
  5. Climbing imports use package subpath imports. A member with a src/ tree declares imports in package.json (#core, #application/*, …). A relative specifier may stay in its folder or descend; it never climbs with ../. The imports map is generated by a script, never hand-written.
  6. A helper used by a second file moves to utils/<name>.util.ts. A single-use private helper stays where it is used.
  7. Conventions change by ADR only. No package-local exceptions.

Consequences

  • A file’s name tells the reader what it exports; a rename is a move.
  • Wildcard barrels are not available, so adding a symbol is a visible diff in the barrel.
  • Subpath imports need a bundler and Metro that resolve package.json imports; native builds are verified before the first native entry ships.

Enforcement

  • typescript/consistent-type-imports in the base oxlint preset (off only in the decorators preset, see 0005).
  • Planned: a validator check for kebab-case names, one export per file and barrel shape; an import-specifier check (port basalt tools/scripts/import-style.mjs); an imports map drift check against the generator.

Amendment 2026-10-21: no utils/; schema files are *.schema.ts (STA-219)

Context. Decision 6 put a helper used by a second file in utils/<name>.util.ts. ADR 0126 (decision 3) bans utils/ and helpers/, and the two cannot both stand. Schema files were also called *.contract.ts (the preview repository’s name), which collides with the .contract test tier of ADR 0182. The owner settled both on 2026-10-21. Decision.
  1. Decision 6 is withdrawn. There is no utils/ folder and no .util.ts suffix, in any member.
  2. Helpers are placed by how many packages use them.
    • A helper that a second package needs moves to @stackra/support, one subpath per concern.
    • A helper shared by two or more files inside one package lives in internal/<name>.ts. It is not exported from any barrel, and its test sits beside it in internal/__tests__/. The internal/ folder is in the kind-folder list of 0130 amendment 2026-10-21, decision 2.
    • A single-use helper stays in the file that uses it.
  3. A schema file is <name>.schema.ts and carries one exported schema and its inferred type. .contract is not a file suffix. It names only the .contract test tier of 0182.
Consequences.
  • utils/ and helpers/ are banned by ADR 0126 and this amendment; the validator’s banned-folder list (checkFileKinds) carries both.
  • A reviewer reads .schema.ts as data shape and .contract.test.ts as a test; the two never share a name.
Enforcement. planned — checkFileKinds in tools/scripts/validate-repository.mjs, reading the suffix and banned-folder tables of tools/scripts/structure.mjs (STA-220). Nothing checks these rules today.

Supersedes and sources

Supersedes: none. Sources: basalt adr/0010, 0020, 0022, 0032; figentra docs/adr/0010-module-specifiers, 0019-subpath-imports, 0035-conventions-freeze.