Goldie Blocks
Coding Rules

File Size

Line-count targets, the deepening trigger, and why shrinking happens by extraction rather than splitting

Line count is a smell indicator, not a quality metric. These thresholds exist to prompt a look, not to score a file. Depth and cohesion win over counts every time.

The targets

Target ≤300 lines for new files. Single-purpose modules in src/shared/lib and packages/domain/src should sit well under this — a pure, framework-free module with node tests that approaches 300 lines is usually two modules.

≥500 lines is a prompt to split. A file crossing 500 lines should be split along its concerns, as ordinary work, without a separate design review (owner ruling 2026-09-28). File a Backlog task only when the right seam is unclear, or the split would change an Interface other code depends on.

In plain terms: when a file gets too big, divide it by topic, the way you would split a long brief into sections, and get on with it. Stop to ask only when it is not obvious where the dividing lines go.

Substantial new work on an over-trigger file splits it first. A fix, an adjustment or a prop can land in place. A new feature or subsystem goes into a file of its own, or the file is split before it lands.

How a large file shrinks

Split by concern. Each new file should hold one thing a reader can understand on its own: a hook that owns its state, a card or row component, a pure helper with its tests, a list of rows. The GB-84 split of drafting-editor.tsx (1,098 → 485 lines) is the model: drag, row assembly, the block card, the module card and the document sheet each went to their own file.

When split pieces need the same many values, bundle them. If several components need twenty values from their parent, give them one named Interface (a React context, or one typed object) instead of twenty props each. CanvasCardContext, BlockAuthoringActions and FolioCardActions are the precedents.

Two splits to avoid, because they add files without making anything easier to read:

  • a file that only forwards props it receives, byte for byte, to one child;
  • cuts at arbitrary line counts that leave every reader needing all the pieces open at once.

The honest test: after the split, can a reader answer a question about the moved concern by reading only the new file? If yes, the split is good.

Extraction can add lines

The settled refactors from candidates 5 and 6 slightly increase total line count while being unambiguous improvements. An Interface declaration, a barrel export, and a doc comment are all new lines that buy locality and a deletion test the previous shape could not pass.

If a proposed change is justified mainly by the line count it removes, that is a warning sign, not a case.

Tolerated outliers

These files are over the trigger and are named as tolerated: each has a stated reason its size is the right shape for now. They may still be split whenever a clean seam appears. No line counts are recorded here: they drift with every commit, and wc -l answers that question better than a table can. What does not drift is why each file is the shape it is, which is what the row carries.

FileStanding
src/widgets/drafting-editor/model/use-folio-writes.tsEvery persisted edit to an existing folio and the row its refusal speaks in, at both tiers. Over the trigger from birth and named here on purpose: the block builder and the module builder share arrangementRows and lifecycleRows, so splitting by tier would file one rule in two drawers. Roughly a third of it is contract prose
src/entities/block/ui/block-card/v4/block-fields-view.tsxFieldRow, the list and its add-field row, and eleven per-field callbacks, after the pure rules, name editor, type-mark menu and value cell went to their own files. Carried as a deepening Backlog task
src/entities/block/ui/block-card/v4/block-card.tsxWhat remains after the family branches, the palette, the role matrix and the header row came out, and the Folio menu and field acts became slots, is the BlockCardProps Interface — which stays beside BlockCard — and the shell wiring
packages/domain/src/folio/operation-resolution.node.test.tsThe catalogue of cases for one Interface, resolveFolioOperations, whose whole point is that every kind and every operation is answered in one place. Splitting it would file the same rule's cases in two drawers. Same standing as candidate 27's record for about-card.node.test.ts: do not re-file it on size alone

Story files are excluded from this table by kind, not by exception: a .stories file is a catalogue of cases, so its length tracks how many cases exist rather than how tangled the code is. A test file's length reads the same way, but it is named row by row above rather than excluded wholesale, because a test file can also be long for the ordinary reason — the code under it is tangled.

The two block-card/v4 rows must be judged by locality rather than line count alone. BlockCardProps is the component's Interface and stays beside BlockCard; a coherent single-consumer subtree may still move when its file Interface is small and carries no mutable refs, state setters or render closures. See Component File Partitions Must Preserve Locality.

Adding to this list is a decision, not a convenience: a file joins it only with a written reason, not by growing.

On this page