Work on Nerthus.Platform¶
Nerthus.Platform is one Python package
with a Rust core, one container image and one CLI, nerthus. This guide is how to build it on a
laptop, run its database, and write a test it will accept. How a change reaches main is
the working regime.
Set up¶
You need uv 0.12.11 or newer and rustup.
uv self update # or the installer at https://astral.sh/uv
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
uv python install # the exact CPython `.python-version` names
uv sync --frozen --group dev # builds crates/nerthus-core, installs the rest from uv.lock
uv run nerthus --version
--frozen installs exactly what uv.lock records and needs no credential: every package it
installs is on a public index. Never re-lock on a laptop; see The lockfile.
The interpreter¶
.python-version pins an exact patch release, and uv is the only thing that provides it:
python-preference = "only-managed" refuses a system CPython even of the same number. The
byte-identical guarantee of nerthus derive is a claim about one interpreter build, so
tests/test_python_pin.py fails when the running interpreter is not that patch release, or is not
uv's build of it. An older uv cannot download the pinned patch and would move an environment
backwards, which is why the uv version is a prerequisite. CI bakes the same interpreter into its
image and downloads nothing.
The Rust core¶
uv sync compiles crates/nerthus-core with the toolchain rust-toolchain.toml pins. On macOS
this is the only way: no macOS wheel is published, so macOS always builds from source.
A sync that succeeds does not prove rustup did the build. Without cargo on PATH, maturin's build
backend quietly bootstraps an unpinned toolchain of its own. Check what built:
cargo --version # the version rust-toolchain.toml pins
ls ~/Library/Caches/puccinialin # if this exists, something built without rustup
On Linux the compile is optional, because CI publishes two manylinux wheels:
The private index¶
morfeusz2 and pl_core_news_lg are on no public index; they come from the group's package
registry, and so do the published nerthus wheels. Only the language extra and
tools/sync.sh need that registry. tools/sync.sh takes the credential from
NERTHUS_PYPI_USER and NERTHUS_PYPI_TOKEN, or from the machine line for the registry's host in
~/.netrc (mode 0600), and hands it to uv as UV_INDEX_NERTHUS_USERNAME and
UV_INDEX_NERTHUS_PASSWORD. uv does not read a netrc for a named index on its own.
A failed resolution lies about its cause: the registry answers an unauthenticated read with a
redirect to pypi.org, which then reports the two packages as missing. nerthus doctor says which
of the two you are in, and never prints the credential.
The lockfile¶
uv lock resolves this project only with a credential for the group registry, and a laptop holds
none. A change to pyproject.toml is re-locked in CI: lock:check refuses a stale uv.lock, and
pressing the manual lock:update job on the merge request's pipeline pushes the refreshed lockfile
to the branch. A push made with the job token starts no pipeline, so run the merge request's
pipeline once more to measure the pushed head. The pipeline
has the rest.
A local database¶
The package carries PostgreSQL 17 and runs it, so a clone needs neither Docker nor a system Postgres.
uv run nerthus db init # unpack the server, create the cluster and the database
uv run nerthus db start # binds exactly what `database.bind` names
uv run nerthus db upgrade # the two schemas and every migration
uv run nerthus db status # non-zero when the server does not answer
uv run nerthus db psql # the bundled psql, against this instance
uv run nerthus db stop
The data directory is ~/.local/share/nerthus and the port 5432 unless the configuration says
otherwise; Configuration has every key.
The database is the model.
Tests¶
The suite starts a throwaway cluster on port 8802 + 10k, where k is NERTHUS_TEST_SLOT (or
CI_CONCURRENT_ID in CI), and prints the port it took. Parallel agents each take their own slot.
pytest -q prints no summary line here, because addopts already carries -q: read the exit code.
Which fiction a test reads¶
Unit tests read the Trylogia fixture and nothing else. tests/fixtures/trylogia/ is a
synthetic estate in the shape of the six corpora, populated from Sienkiewicz's Trylogia, which is
public domain. The real corpora appear in three jobs only: the parity harness,
nerthus derive --verify and the benchmarks, which fetch them at the commits reference.lock pins
and never commit a line of them.
Two reasons. A narrator wrote every line of the real corpora for a campaign; a word reaches the pipeline only with its author's consent, and a quotation committed to a test file cannot be withdrawn. And the real corpora move whenever somebody writes lore, so a test against them has a baseline with a clock in it.
| Guard | What it catches |
|---|---|
ci/realnames.py check (gates:real-names, tests/test_real_names.py) |
a name from the real corpora in the suite |
tools/build-fixture.py check (tests/test_fixture_build.py) |
a hand edit to the fixture that the builder would overwrite |
tests/test_fixture_census.py |
a parser shape that has left the fixture |
ci/real-names.sha256 holds the real corpora's names as hashes, never in clear. It is rebuilt by
hand against a checkout, never in CI:
python3 ci/realnames.py build --lore <checkout of repozytorium-fabularne-prodkopia> \
--ref refs/prod/master
Using the fixture¶
tests/fixtures/trylogia/CENSUS.md lists every parser shape the fixture carries and the file and
line that carries it; take the name, place or transcript line your test needs from there.
python3 tools/build-fixture.py write # rewrite the tree and CENSUS.md
python3 tools/build-fixture.py check # what CI runs: the tree matches the builder
python3 tools/build-fixture.py repos --out DIR # six git repositories with reproducible ids
Never hand-edit a file under tests/fixtures/trylogia/. Add the shape to SHAPES in the
builder, run write, and commit both; the builder refuses to finish when a declared shape is
missing from what it wrote. A test that needs the corpora as git repositories builds them with
repos under a temporary directory; the commit ids are the same on every machine.
Every new test states what would make it pass while being wrong¶
A test's docstring says how it could be green and still wrong, and what it does about that. A case over a set that can come out empty asserts the population separately. A skip names the dependency and the command that removes it.
Where things are¶
crates/nerthus-core/ Rust: parsers, temporal projection, name resolution, indexes, hashing
packages/nerthus/ Python: common secrets db fetch load language annotations domain
api cli worker bot
contracts/ openapi.yaml, cli.yaml: generated, committed, diffed
ci/ the pipeline's gates and checks, each a module with a test
tools/ the builders a contributor runs by hand
docker/ the instance image and the CI image
Imports run downward: common, secrets and db import nothing else in the package; domain
imports those and the Rust core; api, cli, worker and bot import domain; no adapter
imports another adapter.