whileai is a scientific SDK (CONSTITUTION.md
says what that means). The code a user writes with it should read
the way PyTorch and DSPy read: a few nouns, a few verbs, objects that carry
their configuration, and one line per idea. This page is the coding
standard for every public name in the package. New code follows it. Old
code is brought under it one PR at a time, and the ratchet in
tests/api/test_style_ratchet.py refuses any PR that moves the other way.
The two models we copy:
- PyTorch. One universal noun (the tensor), and every op returns one.
Stateful things are objects:
nn.Moduleholds config in__init__and does its one job inforward;optim.SGD(params, lr=)then.step(). Namespaces are few and orthogonal:torch,torch.nn,torch.optim. Usability over cleverness, Python first, explicit over implicit (PyTorch design philosophy). - DSPy. Fifteen names at
dspy.*.dspy.configure(lm=)once, then modules:dspy.Predict(signature),dspy.Evaluate(devset=, metric=),optimizer.compile(program, trainset=). Behaviour is declared ("question -> answer"), not toggled with flags.
The target front page
This is what the first twelve lines of the README should be. It is the bar every public call is measured against.pass_at is a property, not a call; judge_trust takes rows and
a judge, and its gold= names the label key rather than the labels; and
export takes a path, with format="trl" for the shape SFTTrainer
loads. A bar nobody can run is a wish, so the snippet is kept executable
and the delta below says what is still missing.
Today the same program is import whileai.simulations as wai, a
(rows, report) tuple out of optimize, and format_* twins to print
anything. The rules below are the delta.
Rules
Each rule names the PyTorch or DSPy habit it copies, then what it means here. “Public” means any name in an__all__ or documented under
docs/api/.
1. One import, one prefix. import whileai as wai. The top level
holds the loop verbs (simulate, grade, judge_trust, select,
train, serve), the nouns they pass around (Rows, Report, Judge,
Verifier), configure, and the sub-namespaces wai.verify, wai.data,
wai.platform. Under thirty names. Everything else lives one dot down,
grouped by stage, never by implementation file. whileai.simulations is
the legacy path and gains no new names. (torch / torch.nn / torch.optim;
dspy.* is fifteen names.)
2. Objects carry configuration; calls carry data. Anything a user sets
up once and applies many times is a class: judges, verifiers, selectors,
trainers, exporters. The constructor takes the configuration; one verb
method, or __call__, takes the rows. Verifier already does this. A
function is for a pure transform over rows with no reusable setup.
(nn.Module.__init__ then forward; dspy.Evaluate(devset=, metric=)(program).)
simulate
takes forty-three today; that is the ratchet’s starting line, not a
licence. (optim.AdamW has eight; dspy.Predict has three.)
4. One noun flows through every stage. Rows (today SimulationData
and ScoredData) is the tensor. Every stage takes it and returns it, or
returns a Report. What you do to rows is a method on rows:
data.grade(), scored.select(), rows.export(), rows.push(),
rows.decontaminate(eval_set). A stage that only works on rows is not a
free function with rows as the first argument. attach_* and stamp_*
mutate rows in place, so they become methods and stop being free
functions. (every torch op returns a tensor; x.mean(), x.to().)
5. Results are objects that print themselves. A measurement returns a
dataclass with __str__ for the terminal and _repr_html_ for a
notebook. There is no format_x twin for a x_report; printing is the
object’s job. No public call returns a (rows, report) tuple or a bare
dict the user has to know the keys of. (PassAt already prints
pass@1 0.67 [0.55..0.78]; do that everywhere.)
A report that was a dict first becomes a whileai.report.Report, which
is a dict: every key, .get, json.dumps and == against a plain dict
keep working, and __str__ is the block the format_* twin writes. That
is the migration step that costs a caller nothing. judge_trust,
hack_scan, delta_report (wai.compare) and leak_report are through
it; decontaminate, export and preflight are not.
6. Public names are the verb a scientist says. simulate, grade,
select, compare, train, serve, push. Implementation words are
private: run_judge, normalize_judge_result, resolve_topology,
stamp_stage, build_*, load_* get a leading underscore or move under
a namespace the user never imports from. A name that needs a suffix to
say what it returns (*_rows, *_of) is two names doing one job.
7. Declare behaviour; do not toggle it. A mode is one argument with a
small set of values (mode="rl"), or an object. It is never three booleans
that interact (grade=, llm_grade=, simulator=False). If two flags
have to be read together to know what happens, replace them with one
value. (dspy signatures: "question -> answer".)
8. Settings once, override per call. wai.configure(agent=, judge=, api_key=) sets the session. A with wai.context(judge=...) block scopes
an override. A kwarg on the call wins over both. No public call reads an
environment variable the user did not name in the docs. (dspy.configure
and dspy.context.)
9. Every default is named, sourced and tunable from the call. The
existing rule, kept: a number lives in defaults.py with a # NAME = value: why (source) comment, and the call that uses it exposes it as a
kwarg. scripts/check_no_hardcoding.py enforces the first half; the
ratchet enforces the second on new calls. (every torch.optim default is in
the signature and the docstring.)
10. Errors and warnings name the fix. A ValueError says the kwarg,
the bound and the value. A warning says the one call that changes the
outcome. No warning is emitted twice for the same cause in one run.
11. Typed, importable, cheap. Every public signature is fully typed
and py.typed ships. import whileai takes under 200 ms, makes no
network call, and pulls in nothing beyond requests and pydantic; numpy
and torch are imported inside the functions that need them.
12. Docstrings teach then prove. Line one says what the call computes.
Then the parameters in prose, the return type, and a Reference line with
the numbered source from the README. A method with no source says
“convention, untested”. The example in the docstring runs offline.
What the ratchet checks
tests/api/test_style_ratchet.py pins today’s counts and fails when any
grows:
Lower a number in the test when you retire a name. Never raise one. A PR
that has to raise one says why in the body and gets a second reviewer.
Migration
The path from today’s surface to the front page above, in the order that pays off first:whileai/__init__.pyre-exports the loop verbs and nouns; README and docs switch toimport whileai as wai. Nothing is removed.optimizereturns aSelectionobject with.rowsand__str__;SimulationData.select()wraps it.format_*functions become__str__on their report and are deprecated with a one-release warning.Judge(model=, rubric=, ...)absorbs the sixteen kwargs ofgrade.configure()andcontext()land; per-callagent=/judge=keep working.simulatekeeps its signature for one release behind aSimulationobject that holds the forty kwargs in named groups (world=,sampling=,budget=).