Skip to content

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.

uv run python -VV                   # the string a merge request quotes as evidence

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:

NERTHUS_CORE_FROM_REGISTRY=1 ./tools/sync.sh

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

uv run pytest tests/test_config.py          # focused; the full suite is CI's
cargo test                                  # the Rust core

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.