Function contracts and flow charts¶
Started 2026-09-29. The aim: every function in the solver core says what it takes, what it gives back, what it refuses, and what it does at its edges, and every edge it names has a test that fails when that edge breaks. A description alone does not make a result reliable; a stated rule that the code checks, and a test at each edge, does.
This page is the standard. The gate is tests/unit/test_function_contracts.py;
the working rule is in CLAUDE.md ("Function contracts").
The four sections¶
A contract is part of the function's docstring, after the one-line summary.
This one is solver/kernels.py::pipe_min_dt's, abridged:
Inputs:
U_int: interior conservative state, shape (N, 3); any values (checked
here, see Refuses).
dx: cell width [m], > 0 (``Pipe`` guarantees it).
cfl: Courant number, > 0; not clamped (the implicit drivers pass > 1).
Returns:
the stable time step [s], > 0; 1e300 when no cell limits it.
Refuses:
rho <= 0 or p <= 0 in any cell -> returns the 1e300 sentinel, and
`pipe_min_dt_checked` raises with the pipe and cell named.
Edges:
N == 0 -> 1e300 (`test_empty_pipe_has_no_dt_limit`).
one bad cell among healthy ones is not skipped
(`test_a_negative_pressure_cell_is_refused`).
Inputs. Every argument, with its unit in brackets, its shape for arrays,
and the range it must be in. Say who guarantees the range: the function
itself (then it is in Refuses) or the caller (name it). "Verify against call
sites" (CLAUDE.md) applies: a range the callers guarantee is only true if
every caller does.
Returns. What comes back, with unit and shape, and any sentinel value.
State mutated on an argument (a Pipe's arrays, a report) belongs here too:
a caller must be able to see every effect.
Refuses. Each input it will not accept and what happens: the exception
type and what its message names, or the sentinel. none is allowed when
the function accepts everything its type admits.
Edges. The cases this repo has shipped bugs in: zero, empty, NaN,
negative, the boundary cell, a unit or basis mix-up (mass vs mole, K vs °C),
a silent default. One line each, with the behaviour and the test that pins it
in backticks. none only for a function with no edge worth a test (a plain
accessor).
What the gate checks¶
- Every function in the scope has all four sections. The count without one,
MAX_WITHOUT_CONTRACT, may only fall and must be exact. - Every test a contract names exists.
- Every function a flow chart names exists.
It cannot check that a contract is true. Writing one means reading the
function and its callers, as a review-ledger sweep does, and fixing what does
not match (CLAUDE.md: a found bug is fixed, not filed).
Scope and order¶
Every shipped function is in scope: all of src/aeolus1d/ and aeolus1d_gui/ (about
7,000 functions, 2026-09-29 to 2026-10-06, in six batches: the solver core first, then
io/, analysis/ and control/, then the rest of the package, then the GUI shell and
main window, then the rest of the GUI). A new function needs its contract in the same
commit; the gate's count MAX_WITHOUT_CONTRACT is 0. For a Qt slot the Refuses: section
also says what the slot does with an exception (a dialog, the status bar, the console, a
log line, or "printed by Qt, nothing shown"), because an exception in a slot does not stop
the event loop.
Flow charts¶
One chart per feature, not per function, under docs/flows/: what enters,
which function decides what, where each refusal happens, and what comes
out. Charts are Mermaid blocks, so they live beside the text and are
reviewed in the same diff:
```mermaid
flowchart TD
A[case TOML] --> B{load_case}
B -- invalid --> X[CaseParseError]
B --> C[build_network]
```
Each chart ends with a Functions list of the functions its boxes stand
for, as ledger keys in backticks (`src/aeolus1d/io/build.py::build_network`).
The gate checks each one exists, so a rename that is not carried into the
chart fails the build instead of leaving a wrong picture. The charts are
indexed at Flow charts.