Helper functions ordering
A file is read top to bottom. Put the caller first and its helpers under it. The reader meets the shape before the detail.
Not optional
This rule is required. The hook helper_order.py blocks on it.
The shape
Bad — you meet doA1 with no idea why it exists:
function doA1() {}
function doA2() {}
function doA() {
doA1();
doA2();
}
function doB() {}
function doAll() {
doA();
doB();
}
Good — every function is explained by the one above it:
function doAll() {
doA();
doB();
}
function doA() {
doA1();
doA2();
}
function doA1() {}
function doA2() {}
function doB() {}
The rule
Depth first. After a function, write everything it calls, in call order, before you move to its sibling.
doA1 and doA2 sit under doA, not under doAll, because doA is what calls
them. A helper belongs to its caller, not to the file.
When two functions call the same helper, put the helper below the first caller, at that caller's call position. Many callers is not a reason to promote a helper to the top of the file.
references/bad_examples.md works this through a real 44-function file.
Where a new function goes
Directly under the line that calls it. Not at the top of the file, not at the bottom, not in the alphabetical slot.
The top of the file is for the entry point — the function that names what the file is for.
Why this and not the reverse
Bottom-up ordering asks the reader to hold five unexplained helpers in their head until the payoff arrives. Top-down gives them the summary first, and each step down is optional detail they can stop reading at.
It also makes an unused helper visible: nothing above it calls it.
The only exception
A language that requires a definition above its use. C without a prototype, some Pascal dialects, a shell script calling a function before it is sourced.
Python, JavaScript and TypeScript are not on that list. A function declaration
hoists, and a const arrow called from inside another function resolves at call
time, not at definition time. Order is free, so order for the reader.
If the module runs the call at import time — a top-level app = build_app() —
the definition must come first. That is the language rule again, not a
preference.
The hook checks half of it
helper_order.py detects one thing: a helper defined above its caller. A file
can pass it and still read backwards, because the hook does not check the order
of a caller's own helpers.
The function directly below a caller must be the one that caller calls first. Then its second, and so on. Nothing tells you when you get this wrong. Read the caller's body top to bottom and check the definitions follow it.
A file you touch is a file you order
The hook reads the whole file, not the lines you added. That is deliberate.
When it fires on code you did not write, reorder it. You already have the file open, you already understand the call graph, and the move costs nothing at runtime. Take the opportunity.
Reordering is pure movement. Cut a function, paste it below its caller,
change nothing inside it. No renames, no signature edits, no logic. If you find
something else wrong on the way, report it — see minimal-scope-plan.
Commit the move on its own. A reorder and a behaviour change in one diff are unreviewable: every line looks changed, and the one line that matters hides in the noise. Move first, commit, then make your change.
Defer only when the move would collide with work in flight — an open pull request on the same file, or a rename already under way. Then say so and leave it.
The escape hatch
allow-helper-order: <reason> inside the helper. The reason that counts names
the language constraint. "It was already there" is not a reason; that file is
now yours to order.