Project

Skaii-Pathfinder

A Pathfinder 2e rules corpus, the .NET tool that stocks and queries it, and the character-build guides both exist to make defensible.

Reproducibility as a correctness property

This project answers rules questions, and an answer is only worth having if it can be arrived at again. That turns out to be a statement about the environment rather than about the code. If the toolchain drifts, the same query can return a different answer with nothing in the repository having changed, and there is no way to tell which of the two runs was wrong.

So nothing is installed on the host. The .NET SDK, PostgreSQL, and the pgvector extension all run from container images pinned by digest, not by tag. A tag moves. A digest names one exact image and cannot be repointed underneath you. The digests were captured by pulling each tag on a real machine and reading back what came down, and re-pinning them is treated as an amendment rather than a routine upgrade.

The reason the SDK pin is load-bearing goes further than convenience. The snapshot writer compresses, and compression output differs between zlib versions. Pinning the SDK image by digest is what pins that zlib, which is what makes a corpus snapshot byte-identical across machines. Take the digest away and the snapshot stops being a fact and becomes a build artefact.

One browser engine, because whether a page fits is the engine's opinion

The character sheets render to PDF. Whether a sheet fits on a page is not a property of the document, it is a property of the layout engine reading it, so the renderer accepts exactly one engine and that engine is pinned by digest like everything else. Adding a second engine would not add flexibility, it would add a second answer to the only question the renderer exists to settle.

What it actually produces

The sheet below is real output, not a mockup of one. It is the committed result for one of the ten builds in the repository, rendered by the pinned engine from the same stylesheet every other sheet uses, and it is the thing the one-engine decision above exists to protect: every rule on it fits where the layout engine put it, and it fits there again tomorrow.

The first page of a rendered Pathfinder 2e character sheet: build summary, ancestry, background and class notes, an attribute row, a defenses block, the spell DC, a skills table, spellcasting slots and strikes.

First page of a six-page sheet, rendered to PDF by the pinned browser engine. The footer is the interesting part: it stamps the corpus snapshot the build was checked against and the date it was checked, so a sheet says what it was true of rather than merely what it claims.

The guide is the other half. Every option in it carries a citation back into the corpus, and the prose is written around what came back from a query rather than around what somebody remembered. That is the whole reason the corpus exists.

Setup that admits what it does not know

The host side is two POSIX shell entry points and the library behind them. It checks prerequisites and reports each one as satisfied, unsatisfied, or genuinely unknown, where unknown means a dependency was not satisfied so the check never ran. An unknown row is required to name what blocked it. An unknown without a blocker would be indistinguishable from a check that simply failed, which is the ambiguity the three states exist to remove.

The same discipline governs what setup is allowed to touch. Every container, volume and network it creates carries its own label. A name that is already taken and does not carry that label belongs to somebody else, and the answer is to stop, print what is in the way, and change nothing. There is no port to argue over either: the data service publishes no host port at all, so a person's own PostgreSQL on 5432 cannot collide with this one. That is a design decision, not an omission.

A sibling repository, skaii-pathfinder-mutation-test, exists to check the corpus tool against deliberate mutations. It is part of the same body of work rather than a project of its own.

How it is put together

  • A .NET 10 solution behind a single command-line tool: one project pulls and resolves the corpus, one embeds and searches it, one is the command surface.
  • The two working projects deliberately reference neither each other nor anything that reaches the other. An architecture test fails if that fence is taken down.
  • PostgreSQL with pgvector for semantic search over the corpus, and a committed snapshot with its manifest so a query has something fixed to run against.

Source on GitLab