Make the docs state things rather than argue them
Docs / docs (push) Successful in 37s
Playwright Tests / test-playwright (1, 2) (push) Failing after 1m35s
Playwright Tests / test-playwright (2, 2) (push) Failing after 17s
pre-commit / pre-commit (push) Failing after 2m8s
Test Backend / test-backend (push) Failing after 2m48s
Compose Smoke Test / test-compose (push) Failing after 13s
Playwright Tests / merge-reports (push) Failing after 2m25s

The site read as a design journal: rationale paragraphs, hedges
("deliberately", "on purpose", "genuinely"), meta-commentary about the docs
themselves, and one em-dash every ten lines carrying an aside.

Roughly twenty rationale blocks are gone or reduced to what a reader needs
in order to use the thing. Em-dashes go from 507 to 135, and what is left is
structural rather than prose: list and definition separators, table cells,
and four inside code blocks that quote what the CLI actually prints.

Also: api.example.com becomes api.fluksio.com (the emails stay, since
bootstrap.py really defaults to admin@example.com and RFC 2606 reserves it);
the mqtt table gains the two settings it had drifted behind on and inject's
wording matches the engine; llms.txt lists the two connector pages that were
in the nav but not in it; and the two device/device_policy notes now agree.

Builds clean under `zensical build --strict`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015YrQnKV3bnQd4K342y8tKj
This commit is contained in:
2026-08-31 10:49:58 +02:00
co-authored by Claude Opus 5
parent 2422a9b22b
commit bdad6d7fc2
25 changed files with 450 additions and 479 deletions
+62 -65
View File
@@ -8,12 +8,12 @@ Installs the engine and the `fluksio` command. Python 3.12 or newer, Linux or
macOS.
The MQTT and InfluxDB connectors, outbound mail and error reporting are
`pip install 'fluksio[server]'` a deployment talking to devices wants them,
`pip install 'fluksio[server]'`: a deployment talking to devices wants them,
and a laptop waiting on them to install does not. Everything else, the engine
and every python node included, is in the plain install; a node type whose
library is missing says which extra to add when one is actually built.
There is a second, smaller distribution `fluksio-worker` for a machine that
There is a second, smaller distribution, `fluksio-worker`, for a machine that
should only *run nodes* for an engine elsewhere. It has none of the engine in
it. See [Remote workers](workers.md).
@@ -26,7 +26,7 @@ instance, while `login`, `sync`, `run`, `runs`, `artifacts`, `sweep` and
`.fluksio` beside your code, found the way `.git` is: from the working
directory, or any directory above it. Two repositories on one machine are
therefore two engines, with their own flows, runs and token. `fluksio serve`
makes one where there is none, and it ignores itself from within a
makes one where there is none, and it ignores itself from within with a
`.gitignore` of `*`, so a database and a credential cannot be committed by
accident.
@@ -47,17 +47,17 @@ Docker.
At a terminal this opens a dashboard with the engine running under it; see
[below](#the-dashboard). `--plain` prints the log stream instead, which is
also what happens with no terminal in a container, under systemd, or in CI.
also what happens with no terminal: in a container, under systemd, or in CI.
The default port moves out of the way when something already has it 8001,
8002, and so on and says which one it took; the URL written to
The default port moves out of the way when something already has it (8001,
8002, and so on) and says which one it took; the URL written to
`client.json` is the one it is actually on. A port you *asked* for is never
moved off: `--port 9000` on a taken 9000 fails, because something else is
there and you named it.
What it will *not* do is start a second engine for the same instance. If
the port is held by an engine already serving this directory, it says so and
stops one SQLite database wants one engine. Another instance's Fluksio
stops, since one SQLite database wants one engine. Another instance's Fluksio
on that port is named, and the move happens as usual.
| Option | Default | What it does |
@@ -76,8 +76,8 @@ on that port is named, and the move happens as usual.
| `--max-workers N` | 4 | python worker processes (`FLOW_MAX_WORKERS`) |
| `--gpus N` | 0 | GPUs on this machine a node may be given (`FLOW_GPUS`) |
Cards are declared rather than detected asking a vendor's tooling would make
one dependency two so a machine with a GPU reports none until `--gpus` says
Cards are declared rather than detected, since asking a vendor's tooling would
make one dependency two, so a machine with a GPU reports none until `--gpus` says
otherwise, and a node asking for one is clamped to zero and runs alongside
every other. `--gpus 1` is what serialises them.
@@ -87,7 +87,7 @@ restart. It is skipped if the instance is already enrolled.
!!! warning "One process"
`fluksio serve` holds the flow engine. A second one is a *second engine*
`fluksio serve` holds the flow engine. A second one is a *second engine*:
duplicated subscriptions, duplicated cron ticks, two webhooks answering the
same path. Run one, and distribute work with
[workers](workers.md) instead.
@@ -95,7 +95,7 @@ restart. It is skipped if the instance is already enrolled.
!!! note "`$HOME` on a cluster"
A login node's home directory is often NFS, where SQLite's write-ahead log
does not work the database would be locked or corrupt. `fluksio serve`
does not work, since the database would be locked or corrupt. `fluksio serve`
warns when it notices; point `--data-dir` at local disk.
### What it prints
@@ -116,8 +116,8 @@ dashboard is served from there rather than here.
### The dashboard
At a terminal, `serve` draws the health overview, the recent runs, and the
engine's own log in a pane below the output above is in there, not replaced
by it.
engine's own log in a pane below, so the output above is in there rather than
replaced by it.
| Key | What it does |
|---|---|
@@ -131,7 +131,7 @@ by it.
| `e` | pair with a portal, without leaving the screen |
The engine is a child process rather than a thread, which is what makes those
possible and what makes `q` a way out of the screen rather than a way to
possible, and what makes `q` a way out of the screen rather than a way to
stop the engine. Running `fluksio serve` again reattaches to it.
An engine started elsewhere is adopted rather than duplicated, and can be
@@ -140,30 +140,27 @@ beside the data and a token this directory's key signed have to agree. Another
instance's engine is named and left alone.
The screen subscribes to the engine's event bus over the same websocket a
browser uses, so a run appears the moment it starts rather than at the next
poll — which is what used to make a run that started and finished inside five
seconds visible only in the history. It reconnects on its own, quietly: an
engine stopped from this screen is a normal state, not an error to fill the
log pane with.
browser uses, so a run appears the moment it starts. It reconnects on its own,
and an engine stopped from this screen is a normal state rather than an error.
#### Comparing runs
`enter` opens a comparison of the ticked runs: one metric's curve for each,
over a table of what differs between them.
The curves are drawn in braille, five to a chart, in five distinct colours
a deliberate departure from the browser's chart palette, where the five series
The curves are drawn in braille, five to a chart, in five distinct colours: a
departure from the browser's chart palette, where the five series
step one hue by lightness. Hue is what survives a terminal. Every curve is
named in the legend either way.
Two pickers sit above the chart. The first is the metric, named as the run
records it `train.loss` rather than `loss`, since a name is qualified by the
node that published it. The second is what it is plotted against: the step,
records it (`train.loss` rather than `loss`, since a name is qualified by the
node that published it). The second is what it is plotted against: the step,
`time (s)` measured from each run's own first reading so runs started hours
apart lie on top of each other, or another metric of the same runs.
The table below keeps only what actually differs the inputs whose values are
not the same in every run, and the seed when it varies beside each run's
The table below keeps only what actually differs (the inputs whose values are
not the same in every run, and the seed when it varies) beside each run's
status, duration and final numbers. A parameter every run shared is not a
column worth scanning.
@@ -186,7 +183,7 @@ fluksio enroll ABCD-1234
Get the code from the portal under **Instances → Add instance**. It is
single-use and expires in fifteen minutes. `--as` matters when the instance
has several superusers — without it, enrolment refuses rather than guessing.
has several superusers. Without it, enrolment refuses rather than guessing.
Afterwards, `fluksio serve` dials the portal as it comes up, and keeps dialling:
a portal that restarts, a wifi that changes, a laptop that suspends and wakes
@@ -198,11 +195,11 @@ minute. See [Accounts and the portal](../interface/portal.md).
## `fluksio worker`
Runs nodes for an engine elsewhere. Everything after `worker` belongs to the
agent's own parser it is the same program `fluksio-worker` installs, so the
agent's own parser: it is the same program `fluksio-worker` installs, so the
two are interchangeable:
```sh
fluksio worker --url wss://api.example.com/api/v1/workers/attach \
fluksio worker --url wss://api.fluksio.com/api/v1/workers/attach \
--token "$FLUKSIO_WORKER_TOKEN" --labels gpu
```
@@ -211,21 +208,21 @@ See [Remote workers](workers.md).
## Talking to an engine
The commands below are the client half: they run wherever you work, and
address an engine over its API rather than being one except under `--local`,
address an engine over its API rather than being one, except under `--local`,
which boots one inside the command instead.
### `fluksio login`
```sh
fluksio login --url https://api.example.com
fluksio login --url https://api.fluksio.com
```
For an engine somewhere *else*. One you started yourself needs no login:
`fluksio serve` writes the token as it comes up and says where it put it.
The token goes in this project's `.fluksio/client.json`, or with `--global` in
`~/.fluksio/client.json`. Every command below reads it from there nearest
first, walking up from the working directory or from `FLUKSIO_URL` and
`~/.fluksio/client.json`. Every command below reads it from there, nearest
first walking up from the working directory, or from `FLUKSIO_URL` and
`FLUKSIO_TOKEN`, or from its own `--url` and `--token`. A token an older
version wrote to `~/.config/fluksio/client.json` is still read.
@@ -240,14 +237,14 @@ each one with a generated import shim per node. A directory that is a package
is walked; a dotted name is imported as it stands; nothing is loaded from a
file path, because the shim has to import the same way.
A plain directory is walked all the way down, so one folder per study
`fluksio sync dev` over `dev/s1_baseline/study.py` needs no naming. Hidden
A plain directory is walked all the way down, so one folder per study
(`fluksio sync dev` over `dev/s1_baseline/study.py`) needs no naming. Hidden
directories, `__pycache__`, `node_modules` and virtualenvs are left alone.
Each file is imported under the name its path spells beneath the directory
being synced, so `dev/s1/study.py` and `dev/s2/study.py` are `s1.study` and
`s2.study` and a `study.py` per study collides with nothing. No `__init__.py`
is needed the directories in between are namespace packages which leaves a
is needed, since the directories in between are namespace packages, which leaves a
bare `from study import ...` in a test beside it working. A file at the top of
what is synced keeps its plain name.
@@ -258,18 +255,18 @@ what is synced keeps its plain name.
| `--force` | overwrite a flow, or a node body, that was edited on the canvas |
Every sync retires the engine's workers, including one that had nothing to
upload a worker holds your package in memory, so an edit to it is invisible
upload: a worker holds your package in memory, so an edit to it is invisible
until the process goes.
It also records, per node, which of your modules that node's function imports
its way to, and what they hash to. That is what the
[stage cache](../concepts/runs.md#stage-caching) keys on, so editing a
helper a node calls into is reported as that node changing `train: updated
(flow, fit)` and re-runs it, while editing something the node never reaches
helper a node calls into is reported as that node changing (`train: updated
(flow, fit)`) and re-runs it, while editing something the node never reaches
is left alone.
A sync that changed nothing says `unchanged`, which is the answer worth
having. `published` and `draft` are said only when there was something to
having. `published` and `draft` are said only when there was something to
publish or a draft was genuinely left behind. An engine too old to store what
a node's code reaches says so in a line naming both versions; until it is
upgraded its cache is keyed on the whole repository, as it was before. See
@@ -281,7 +278,7 @@ upgraded its cache is keyed on the whole repository, as it was before. See
fluksio run train --lr 0.05 --seed 7 [--wait]
```
Syncs the working directory *and everything under it*, then submits a run so
Syncs the working directory *and everything under it*, then submits a run, so
the command after an edit is this one and nothing else, from the repository
root as readily as from the study's own folder. A study that will not import
is a warning rather than a stopped run; the upload is already a no-op for a
@@ -290,7 +287,7 @@ flow nothing changed in, so what the walk costs is importing the others.
is not free, and `--no-sync` skips it entirely.
Flags that are not its own are the flow's inputs, typed by what the flow
declares them as so a name the flow has not got is refused by name, and
declares them as, so a name the flow has not got is refused by name, and
`--param lr=0.002` is told that one value is `--lr 0.002` and several is a
[sweep](#fluksio-sweep). `--wait` blocks until the run
finishes and exits non-zero if it failed. `--follow` waits as well, and prints
@@ -309,13 +306,13 @@ timeout.
on the run, so what a result came from is answerable later. It goes into the
digest that identifies a run's inputs, so two runs of one configuration with
different seeds are different runs rather than a cache hit. And if the flow
declares an input named `seed`, that is what fills it so the number the run
declares an input named `seed`, that is what fills it, so the number the run
is labelled with is the number your code actually drew from, instead of merely
looking like it. A flow that declares no such input still records it, and
nothing reads it. Sweep over seeds with `--param seed=1,2,3`.
Any input takes what a previous run produced, named rather than typed out
a checkpoint, but equally a config object nobody wants to paste into a shell:
Any input takes what a previous run produced, named rather than typed out: a
checkpoint, but equally a config object nobody wants to paste into a shell:
```sh
fluksio run evaluate --dataset @run:1758042000123-9f2ab41c.dataset
@@ -327,7 +324,7 @@ fluksio run train --meta @run:1758042000123-9f2ab41c.dataset_meta
type; a bare digest is the content itself, resolved into a reference. Both
spellings are reserved on every input, `str` included, so an input that has to
carry one of them literally cannot. Passing the value as JSON still works and
is what a script that already holds one does the same thing
is what a script that already holds one does, the same thing
`flow.submit(dataset=run.result["dataset"])` does from Python.
Run a flow with no parameters at a terminal and it asks for them, one line per
@@ -346,15 +343,15 @@ the command, skips the questions, and `--defaults` skips them explicitly.
`--no-sync` runs what is already on the engine. Worth it in a tight loop where
you know nothing changed, since syncing retires the workers and the next call
pays its imports again. A directory that declares no flows syncs nothing and
says nothing a flow drawn on the canvas is run the same way.
says nothing: a flow drawn on the canvas is run the same way.
`--no-cache` executes every node, including one an earlier run already
answered. See [Stage caching](../concepts/runs.md#stage-caching).
`--local` boots the engine inside this process instead of talking to a served
one, so there is no `fluksio serve` terminal to keep open. It is the same
instance either way the same `.fluksio`, the same database, artifacts
and run history so a run made this way and a run made through a served
instance either way (the same `.fluksio`, the same database, artifacts and run
history) so a run made this way and a run made through a served
engine cache against each other. It always waits, because the engine it starts
lives exactly as long as the command. Starting one costs a few seconds of
worker pool and module reconcile, against the ~15 ms of submitting to an
@@ -378,11 +375,11 @@ engine that accounts for nothing.
The portal reads one of three ways. `no portal` means this instance was
never enrolled. `portal hub.fluksio.com` means the link is up. `portal
unreachable` names the error, and is the one worth acting on the dashboard is
unreachable` names the error, and is the one worth acting on: the dashboard is
served from the other end, so nobody can reach it while that is showing.
`--watch` keeps it on screen and refreshes every five seconds until Ctrl-C
the cadence the dashboard polls at, since nothing here moves faster. It needs a
`--watch` keeps it on screen and refreshes every five seconds until Ctrl-C, the
cadence the dashboard polls at, since nothing here moves faster. It needs a
terminal; without one, run it without `--watch` and the output pipes cleanly.
`--local` reads the flows and history out of this directory with no engine
@@ -398,10 +395,10 @@ fluksio runs [--flow train] [--limit 20]
The runs an engine has recorded, newest first: id, status, flow, duration, how
long ago it was submitted, the commit of the repository it came from, and the
inputs it was given. Statuses are coloured when a terminal is reading the
output `ok` green, `error` red, `cached` cyan.
output: `ok` green, `error` red, `cached` cyan.
Only the inputs that *differ from what the flow declares* are shown, and they
are clamped to what is left of the terminal's width — a run that took the
are clamped to what is left of the terminal's width. A run that took the
defaults lists none at all, and a flow taking a few kB of JSON does not push
everything else off the line. `Client.runs()` and
[`fluksio export runs`](#fluksio-export) are where the whole value is read.
@@ -414,7 +411,7 @@ to be served.
fluksio flavors
```
The named sizes a node can ask for `@node(resources="gpu-small")` with the
The named sizes a node can ask for, as in `@node(resources="gpu-small")`, with the
cores, memory and cards each stands for. Editing them is the Workers screen or
`POST /api/v1/flavors`; this is the read.
@@ -424,7 +421,7 @@ cores, memory and cards each stands for. Editing them is the Workers screen or
fluksio sweep train --param lr=0.1,0.01 --param epochs=10,50 --wait
```
Every combination of the parameter lists, submitted as one group four runs
Every combination of the parameter lists, submitted as one group: four runs
above, sharing a `group_id` and executing in parallel. Values are typed by the
flow's inputs, the same as `run`'s are, and `--seed`, `--no-sync`,
`--no-cache` and `--local` mean what they do there. `--wait` blocks until all
@@ -438,8 +435,8 @@ fluksio export metrics --flow train --name train.train_loss --stride 10 -o curve
fluksio export runs --flow train --status ok > arms.csv
```
The two tables an analysis reads. `export metrics` is the long one a row per
run, metric and step which is what a plotting library takes without
The two tables an analysis reads. `export metrics` is the long one, a row per
run, metric and step, which is what a plotting library takes without
reshaping; `--name` keeps the metrics it lists and `--stride` keeps every Nth
point of *each* curve. `export runs` is the wide one: a row per run with its
inputs as columns, its final numbers, its status, its duration and the commit
@@ -457,12 +454,12 @@ cell blank.
A node usually returns a record rather than a scalar, so both sides take
dotted paths into one: `--metrics final_metrics.train_loss,test_metrics.known.perfect`
selects three fields rather than two blobs, and `--params model.ansatz` does
the same for an input. The defaults reach the same depth every number a
the same for an input. The defaults reach the same depth: every number a
result carries becomes a column wherever it sits, and a record's inputs are
taken leaf by leaf rather than as one blob.
Metric names are flow-qualified a node of `train` writing `train_loss`
records `train.train_loss` so `--list` prints the names the selected runs
Metric names are flow-qualified (a node of `train` writing `train_loss` records
`train.train_loss`) so `--list` prints the names the selected runs
carry when the spelling is not obvious.
Both take `--flow`, `--run ID` (repeat it), `--group`, `--status`, `--since`,
@@ -470,8 +467,8 @@ Both take `--flow`, `--run ID` (repeat it), `--group`, `--status`, `--since`,
back to the run page and to what the run made.
`--format` is `csv` (the default), `jsonl` or `parquet`; output goes to stdout
unless `-o FILE` names somewhere. Parquet keeps the types and needs pyarrow
`pip install 'fluksio[parquet]'` — and a file to write, since it is not a
unless `-o FILE` names somewhere. Parquet keeps the types, needs pyarrow
(`pip install 'fluksio[parquet]'`) and needs a file to write, since it is not a
stream. In a notebook, `Client.export_metrics()` and `Client.export_runs()`
answer the same rows as a list of dicts, which `pandas.DataFrame` takes
directly.
@@ -483,7 +480,7 @@ fluksio artifacts 1758042000123-9f2ab41c
fluksio artifacts 1758042000123-9f2ab41c weights -o model.npz
```
The files a run produced what `fluksio.save_artifact(...)` wrote, and any
The files a run produced: what `fluksio.save_artifact(...)` wrote, and any
artifact a node returned. Named alone it lists them: the message each left on,
its size, its media type and the filename the node gave it. Name one and it is
written here, under that filename unless `-o` says otherwise.
@@ -496,9 +493,9 @@ The message name is the one to pass, since it is what addresses the bytes;
```text
.fluksio/ (or ~/.fluksio, with `--global`)
├── client.json the token `serve` wrote, mode 600
├── .gitignore `*` a database and a credential, ignored from within
├── .gitignore `*`, so a database and a credential are ignored from within
├── fluksio.db SQLite: users, runs, metrics, observability, agents
├── flows/ a git repository one directory per flow
├── flows/ a git repository, one directory per flow
│ ├── house/
│ │ ├── flow.json the published structure
│ │ ├── nodes/*.py the published node code