# Sota Structure

> SOTA Structure

- Skill: `b4r7x/sota-structure` (Agent Skill, multi-file: 2 files)
- Install (CLI): `npx skillmds@latest add b4r7x/sota-structure`
- Raw SKILL.md: https://api.skillmd.com/api/skills/b4r7x/sota-structure/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: b4r7x (https://skillmd.com/u/b4r7x)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/b4r7x/sota-structure

---


# SOTA Structure

File/folder structure doctrine for TypeScript projects and monorepos, distilled from the 2025/2026 consensus (bulletproof-react, TkDodo, Kent C. Dodds, Turborepo, Hono, and the real source of the vercel/shadcn/astro/changesets CLIs), plus the audit procedure that enforces it. All numbers and citations: [references/evidence.md](references/evidence.md).

A repo's own contract (AGENTS.md / CLAUDE.md) overrides this skill — but when the contract conflicts with measured evidence, flag the conflict instead of silently following either side.

## Core Model

1. **Vertical slices over horizontal layers.** Organize by feature/domain. Technical-type dirs (`components/`, `hooks/`, `services/`) are allowed only inside a slice, in the shared tier, or in tiny apps.
2. **Unidirectional imports:** shared → features → app. Features never import each other; code promotes to shared on its **second** consumer (rule of two). The rule only exists if machine-enforced.
3. **Colocate first, extract later.** Code lives as close as possible to where it is used.
4. **The folder carries context; the file carries the concept.** Never repeat a path segment in the basename.
5. **Barrels only at real package boundaries.**
6. **A test lives next to what it tests.**

## Monorepo Layout

- `apps/` + custom top-level groups (`cli/`, `libs/`, `tooling/`) are all first-class — Turborepo/pnpm discover workspaces via globs, and vercel/turborepo itself uses custom groups. Never nest workspaces.
- Public CLI binaries: **unscoped + branded** (`turbo`, `vitest`, `shadcn`). Libraries and secondary CLIs: one consistent branded scope with **agnostic leaf names** (`@scope/core`, `@scope/add`). Nobody ships an `@acme`-style placeholder scope.
- Internal packages: compiled + publishable when any handoff/copy/declaration contract exists; just-in-time source exports only when every consumer transpiles.

## Per-Surface Taxonomy

Principles above apply everywhere; the bulletproof-react **dir taxonomy applies to UI surfaces only**.

| Surface | Structure | Never |
|---|---|---|
| React SPA / web app | `app/` (providers + router, thin routes); `features/<x>/{api,components,hooks,stores,types,utils}` created on demand; shared `components/`, `hooks/`, `lib/`, `types/`, `config/`, `testing/` | logic in route files; cross-feature imports; empty skeleton subfolders |
| Ink TUI | same as React — Ink IS React: `app/` (providers + router + **thin screen pages** in `app/screens/`/`app/overlays/` that compose features); `features/<x>/{components,hooks,…}` hold the substance; shared `components/`, `hooks/`, `lib/` | putting feature components/logic in `app/` instead of thin pages (pages compose, features hold); providers or router as a top-level sibling of `app/`; blindly copying web-only dirs (fetch-style `api/`) into a TUI |
| Command CLI | `commands/` — one file or folder per command, spec + handler split (vercel-style); domain logic in `utils/<domain>/` or lib packages, unit-testable without spawning the CLI | bulletproof taxonomy; business logic inside arg parsing |
| Hono / small server | one Hono instance per route group mounted via `app.route()`; `createApp()` factory separate from the serve entry; zod schemas colocated next to routes; `middlewares/`; SSE/streaming behind a transport→adapter→pipeline seam | Rails-style controllers — Hono's own docs warn they break path-param type inference |
| Publishable library | domain modules behind one `src/index.ts` public entry + granular `exports` subpaths; precise `sideEffects` | tests/stories leaking into dist or registry/copy output; `features/` taxonomy |
| Docs app | consumes the real packages | mirroring library source into the docs app |

Embedded server inside a CLI: the command is a thin launcher adapter; the server is its own module or workspace (astro/vercel pattern).

## Naming

- **kebab-case** for all files and folders; PascalCase/camelCase only for exported symbols. (TanStack core and vitest use camelCase files — a per-repo tradition; never mix conventions within one repo.)
- **Basename = the file's primary export**, kebab-cased. Ban grab-bags: `utils.ts`, `helpers.ts`, `common.ts`, `misc.ts`, `shared.ts`.
- **No hard hyphen cap exists anywhere** — linters cap case, never length. But elite repos sit at 82–99% of files with ≤1 hyphen (vercel CLI: 18% with 2+; bulletproof-react: 0%). The mechanism is the **folder-context move**, not abbreviation:
  - `features/review/hooks/use-review-results-keyboard.ts` → `use-results-keyboard.ts` — the path already says review.
  - 2+ hyphens in a basename = shorten-via-context candidate; 3+ = split/rename smell.
- **Path-echo is redundancy, not description.** A basename repeating a path segment is a finding. "The name reads fine standalone" is not a defense — files are read at their path.
- Exempt from hyphen counting: the `use-` hook prefix, the `<component>-<part>` compound idiom (`menu-item.tsx`), and tooling dot-suffixes.
- **Dots only for tooling-known suffixes** (`.test`, `.spec`, `.e2e`, `.stories`, `.config`, `.d`); hyphens separate words. Angular v20 dropped its `.component`/`.service` dot-types for dash-style names to converge with the React/TS ecosystem.
- **Dot-segments are not word separators.** `review.routes.ts`, `user.service.ts`, `foo.command.ts` (NestJS style) are findings. Apply the folder-context move instead: flat `routes/review.ts`, or at 3+ files a `routes/review/` folder containing `route.ts`, `schema.ts`, `stream.ts`. Inside a unit folder, files drop the unit name entirely: `commands/review/command.ts` + `handler.ts`, never `review.command.ts`.

## Barrels — Verdict Table

| Barrel | Verdict |
|---|---|
| Package public entry `src/index.ts` | ALLOWED — the one sanctioned barrel (TkDodo) |
| Granular `package.json` subpath exports (`./hooks`, `./git`) | PREFERRED over one fat entry for non-tiny packages |
| Per-component `index.ts` where each component folder is a distribution unit (shadcn-style registry copy) | ALLOWED — it IS a public entry, per component |
| Internal convenience barrel (`features/x/index.ts`, `hooks/index.ts` re-exports) | REMOVE — import concrete files |
| Importing your own package through its barrel/subpath from inside the package | FORBIDDEN — direct relative imports inside the boundary |

Measured cost of internal barrels: TkDodo −68% modules (11k→3.5k); Vitest −85% transformed files; Atlassian −75% CI build minutes; Vercel −28% builds, up to −40% cold starts. bulletproof-react removed its own barrels and now recommends importing files directly.

## Folders, Grouping, Colocation

- Default: **flat siblings** — `button.tsx` + `button.test.tsx` next to each other.
- A unit gets its own folder at **3+ files**. A folder never gets an `index.ts` just because it exists.
- Tests: colocated `<name>.test.ts(x)`; integration as `<name>.integration.test.ts`; e2e per app under `tests/e2e/*.e2e.ts`; never `__tests__/`, never a parallel unit-test tree.
- Stories/docs files colocate the same way; the build must exclude them from dist and registry/copy output.

## File Size & Splitting

- Target ≤200 lines, warn >300, hard review >350 — counted **per responsibility**, not mechanically. Generated/data files exempt.
- One main export per file + its small private helpers. Extract helpers only on proven reuse.
- Do NOT split cohesive sequential code — pipelines, state machines, protocol seams (Locality of Behaviour / Carmack's inlining argument). A cohesive 350-line state machine beats five fragments that force frame-switching.
- **Orchestrator split:** when a file genuinely splits, the original filename stays as the orchestrator re-exporting the unchanged public surface; the parts become siblings. Public imports never break.

## Decision Guide

"Where does this code go?"

1. Used by one feature → that feature's folder, named **without** the feature prefix.
2. Needed by a second feature in the same app → app-shared tier (`hooks/`, `components/`, `lib/`).
3. Needed by a second app/package AND generic outside the product → extract to the owning lib (rule of two, real named concept, clearer call sites).
4. Product-specific composition, copy, domain flows → stays in the app forever.

"Folder or file?" → file, until 3+ siblings. "Barrel?" → only at a package boundary. "Split?" → only at 2+ responsibilities, not at a line count alone.

## Enforcement — Wire It, Don't Prose It

- **dependency-cruiser**: layer boundaries, no-circular, no-orphans
- **knip**: dead files, exports, dependencies
- **Biome / eslint-plugin-unicorn** `filename-case`: kebab-case
- lint bans: internal barrels; self-package barrel imports; deep imports bypassing another package's public exports

## Structural Refactor Protocol (mass moves/renames)

1. **Pure-move commit first** — moves/renames with ZERO logic edits, so git rename detection survives and review shows renames, not delete+add. Splits and logic changes go in stacked commits after.
2. **Codemod with ts-morph** for cross-package moves — IDE import-update is unreliable across package boundaries (TypeScript #59136, closed not-planned).
3. **Lockstep handoff rule:** a rename touching a published/copy surface updates source, registry JSON, generated bundles, docs, examples, and consumers atomically in one PR.
4. **Pre-1.0 is the cheap window.** Every rename after first publish is a breaking contract change.
5. **Gates between phases:** full type-check (project refs make boundary breakage a hard error) → FULL test suite (not affected-only) → artifact/registry validation → smoke/e2e → `git diff --check`.

## Audit Procedure

When auditing structure against this bar:

1. **Measure** (exclude generated code): hyphen distribution per basename; file-size buckets (>200/>300/>350); barrel census (public entry vs internal re-export); test placement patterns; grab-bag basenames; path-echo names; boundary violations (cross-feature imports, shared→app); mirrored/duplicated trees.
2. Every finding needs file:line evidence and a concrete fix (exact target name/path).
3. Spec fixes in this phase order: pure moves/renames → DRY/extractions → barrel dissolution → splits & local fixes → enforcement wiring → docs. Gates between phases per the protocol above.

## Rationalizations — Counters

| Excuse | Reality |
|---|---|
| "Don't churn a working file over a name" | Pre-publish, a path-echo rename is a one-line codemod; post-publish it's a breaking change. Now IS the cheap moment. |
| "The long name reads fine standalone" | Files are read at their path. `features/review/hooks/use-review-results-keyboard.ts` says review twice. |
| "One more index.ts is convenient" | Convenience for one importer, measured cost for every build/test/bundle (−68…−85% module counts after removal). |
| "Split everything past 200 lines" | The threshold is per responsibility. Splitting a cohesive state machine is a regression. |
| "apps/ + packages/ is mandatory" | Turborepo discovers via globs; vercel/turborepo itself uses custom groups (`cli/`, `crates/`). Custom groups are first-class. |
| "Controllers will organize the server" | Hono's own docs: controllers break param type inference. Handlers stay at the route site. |
| "We'll enforce boundaries by convention" | Unenforced boundaries decay. dependency-cruiser or it didn't happen. |

