Write documentation¶
This page is the authoring contract for the site. Follow it whenever you add or edit a page in either tab. The documentation is the source of design truth: code is written to satisfy what these pages state, and when code and page disagree, the page is the bug report — fix one or the other deliberately, never silently.
Pick the tab and the register¶
The site has two tabs, one per audience. The first decision you make is which tab the page belongs to, and that decision settles the register. No page serves both audiences.
| Tab | Holds | Register |
|---|---|---|
Running the campaign (operators/) |
eleven self-contained articles, one per operator topic | operator |
Contributors (contributors/, explanation/, how-to/, reference/) |
architecture, how-to guides, the subsystem models, reference | contributor |
Inside the Contributors tab the Diátaxis distinctions still hold, as nested sections — Architecture, the tutorial, How-to guides, Models, Reference. They earn their keep there: a contributor genuinely wants the route table separate from the prose that explains it. The Running the campaign tab has no such split, deliberately.
An operator topic is one article. Steps first, the why woven into the step it changes. Never split an operator topic into a task page and a concept page.
Why the tabs replaced the quadrants¶
The site used to have four Diátaxis tabs, and each operator topic was split across two of them: a How-to page and an Explanation index.md twin. Read the rationale before you reach for that shape again.
- The twins were 65-90% byte-identical. The same examples, the same sentences, in two files.
- They had already drifted into eight contradictions: distribution described as automatic on one page and deliberate on its twin; PU owned by the player on one and by the character on the other; two triggers for a coin burn; two rules for who may be a transfer endpoint; two accounts of doors; two destinations for an adoption note; two scopes for speaker extraction; and a declined name that the twin's example contradicted.
- Every edit needed a matching twin edit, and five got missed. That is where the eight came from.
One article per topic per audience is the rule now. Ownership is settled per tab: an operator article and a model page may cover the same subject in their own registers — that is the two-tab design working, not duplication. Within a tab, one page owns a concept.
The operator register¶
The operator — a Koordynator, Namiestnik, Narrator, or Radny — authors sessions, players, entities, and locations by hand in Markdown. They do not read routes, cmdlet signatures, or C# types. Write for them:
- Show first, explain second. Every concept gets a fenced ```markdown example the operator could paste, followed by a plain explanation of what the tool recognizes in it. Grow from a minimal example to an extended one.
- Plain sentences. One idea per sentence. No em-dash chains, no nested parentheticals; use a hyphen
-where you need a break. - No implementer surface. No routes, no cmdlet signatures, no C# types, no on-disk tooling internals, no write-gate or version machinery. Cmdlet names are allowed only in Set up the repository, the one operator article that names commands. Discord slash commands (
/pu,/sesja zamknij) are operator surface, not implementer surface, and are fine anywhere in the tab. - Self-contained. An operator must be able to finish the task from the one article, without following a link. An operator page never links into
reference/,explanation/,how-to/, or anymodel.md. If the operator needs a fact that lives only there, the fact gets written into the operator article in operator register. A link the reader must follow to finish the task is a defect. - Teach the why only when it changes what they write. The operator needs to know a session header is a unique id, so they should not change it after recording — not how the parser stores it.
- No walls of prose. A paragraph with no nearby example is probably too abstract for an operator page.
The contributor register¶
The contributor needs exact contracts: cmdlet names, routes, request/response shapes, file formats, tag names, invariants. Write for them:
- Exact shapes — tables for fields, fenced blocks for Markdown and JSON. Quote real tag names and formats.
- Cmdlet names, routes, capability ids, and wire error ids belong here and nowhere else.
- Em-dashes are fine, but the shared style rules below still apply: one idea per sentence, ~35 words as the ceiling.
- Gloss every coined term, acronym, or Polish word outside the vocabulary table at its first use on the page.
- A contributor page may link to an operator article — a model page pointing at its plain-language counterpart is useful. The link ban is one-directional.
Keep Polish literals verbatim¶
Polish is core. There is no English-canonical layer and no alias-translation table. On disk, tags, entity types, status values, and roles are Polish literals in canonical diacritical form — and they appear on the page exactly that way:
- session metadata keys:
Lokacje,Logi,PU,Intel,Uczestnicy,Transfer,Pliki,Data,Tytuł,Narrator, - entity types:
Gracz,Postać,NPC,Grupa,Lokacja,Mapa,Przedmiot, - entity tags:
@lokacja,@należy_do,@ilość,@status,@alias,@plik,@tematy_zastrzeżone, - statuses:
Aktywny,Nieaktywny,Usunięty,Niepewny, - every entity, location, and denomination name.
ASCII variants (@nalezy_do, Usuniety) exist only as legacy input to the one-time adoption import, which normalizes them to diacritics on write — never author them into an example. Prose inside example bodies stays Polish, written the way an operator writes it.
When documenting language-aware behavior (name resolution, tag parsing), write in two registers at once:
- Conceptually — "the resolver understands Polish noun and adjective declension."
- Concretely — with Polish examples:
Erastera → Eraster,Tussalem → Lord Tussal,Tussalowi → Lord Tussal,Lorda Tussal → Lord Tussal.
Never imply Polish is one option among many. It is the only language.
Use the canonical vocabulary¶
Use these exact terms; do not invent synonyms.
| Term | Meaning | Never write |
|---|---|---|
| Encja / Entity | A record in the entities index | "object", "node" |
| Gracz | Player (real human, keyed by Margonem profile ID) | "user", "account" (except re: Margonem) |
| Postać | Character (in-fiction, owned by a Gracz) | "PC" alone, "hero" |
| Sesja / Session | One play session, keyed by its unique header | "game", "event" |
| PU (Punkty Umiejętności) | Skill points — the progression currency | "XP", "skill-points" alone |
| Narrator | Game master / session runner | "GM", "DM" |
| Koordynator / Namiestnik | The operator/administrator role | "admin" (except re: capability) |
| Radny | Council member (Rada Świata Fabularnego) | "moderator" |
| NADMIAR / STARTOWE / SUMA / ZDOBYTE | The four PU accounting fields | translations |
| Uprawnienia | The narrator-permissions table | "ACL", "permissions sheet" |
| Margonem ID | Authoritative player identity (profile id) | "user id" |
The seven entity types are always the Polish set: NPC, Grupa, Lokacja, Mapa, Gracz, Postać, Przedmiot. On a contributor page the entity Markdown format is owned by the entity model — link to it, never re-specify it. The operator-register statement of the same format is owned by Entities, which spells it out rather than linking, as an operator article must.
Give every concept one owner¶
Every shared concept has exactly one owner page within its tab. A non-owner page may state it in at most one sentence plus a link; anything longer is a bug. Mechanical check: search the site for a concept — it should hit one page per tab, plus links.
The one exception is the cheat sheet, recorded below.
Operator articles¶
| Owner page | Owns |
|---|---|
| Start here | what the tool is, signing in through Margonem and Discord, the four roles, the Uprawnienia ceilings table |
| Set up the repository | the one-time import, the unified index nerthus.entities.md, cmdlet names |
| Record a session | which file a session goes in, the header grammar, the body, Lokacje, Uczestnicy, Intel, the Data/Tytuł/Narrator overrides, closing and distribution |
| Grant PU | the PU block, the monthly award, the cap and NADMIAR, the four PU fields, all-or-nothing and the alias fix, STARTOWE seeding and its preview, election eligibility |
| Transfer coin and items | the Transfer block, the three transfer forms, who may be an endpoint, the three coins, holdings, physical against virtual coin, burn |
| Link session logs | the Logi block, session logs against Discord delivery and the internal streams, fetching and archiving, failed fetches, what is extracted from a transcript |
| Add, edit, and retire entities | the seven types, the block shape, the common tags, dated values, create/edit/retire/restore, @alias and how names are matched |
| Players and characters | the Gracz and Postać blocks, @margonemid, the durable roster sections, registering a player and adding a character, @tematy_zastrzeżone |
| Locations and maps | the location tree, Parent/Interior paths, Mapa against Lokacja, doors |
| What happens every month | what the monthly settlement does, what it repairs, what the operator must do |
| Cheat sheet | nothing — a deliberate recap card, see below |
Contributor pages¶
| Owner page | Owns |
|---|---|
| Architecture | daemon + thin-client contract, data-owner rule, deliberate non-goals, discovery and auto-spawn, hooks, self-heal, the C# substrate |
| API reference | the closed route table, cmdlet ⇄ route mapping, envelopes, middleware chain, status codes, wire error ids, jobs, SSE, the single write gate |
| Entity model | the entity Markdown format, index merging, soft-delete, hand edits, the @forma_sesyjna claim ledger and its (auto) markers |
| Tag schema | the closed Polish tag table, range grammar and temporal evaluation, the historical write rule, projection fields |
| Session model | the session header key's role, entity-driven distribution via @plik, integrity tiers, the participation graph |
| Session metadata | the exact syntax of the header and every session metadata key, legacy generations |
| Player model | the Gracz/Postać model, the durable roster sections, the Gracze.md seed, Margonem identity data, @prfwebhook, the Dodatkowe informacje entries |
| PU model | the four PU fields, the monthly batch, the PU timeline, election eligibility |
| Currency model | denominations, holdings, Transfer classification and apply, reconciliation, economy analytics |
| Location model | the location tree, the Mapa registry, the name stripper, deterministic normalization, traversal graphs, map checkup |
| Name resolution | the 4-stage resolver, -Within/-ActiveOn disambiguation, the name index |
| Permissions | tokens, identity linking, the capability ACL, roles, governance data (Uprawnienia) |
| Capabilities | the capability id list and role bundles |
| Adoption | the single-pass import, presence branching, idempotency, legacy recovery, the index-format version |
| Settlement model | pending-work derivation, the report shape, the settle CI driver, MR write-back |
| Set up pipelines | GitLab job templates, schedules, CI/CD variables and tokens, run triage |
| Deploy the VM daemon | the bot account, the SSH key, the host layout, sync.json, the systemd unit, health verification, maintenance |
| Sync | in-daemon git sync, the scheduler tick, publish modes and the ledger rule, SyncStale |
| Logs model | internal log streams + audit, game-log fetch and parse, Discord delivery |
| Configuration | config.json keys and defaults, data-table file locations, secrets policy |
| Data tables | the Council-editable tables — currency.json, roles.json, towns.json — and the seed-only-when-absent rule |
| Run tests | test conventions, the mock policy (real fixture bytes), fixture layout, forbidden test patterns |
| Code style | comment/naming/.NET patterns, error handling, the project code shapes |
| Glossary | one-line term definitions |
The cheat-sheet exception¶
The cheat sheet is the one page allowed to restate what other pages own, because it is a lookup card rather than a source of truth. It holds these limits, and an author must not relax them:
- Tables only.
- Every row is a name, a one-line gloss, and a link to the owning article.
- No examples and no fenced code blocks.
- No rules and no behavior. A gloss says what a thing is, never how it behaves.
Those limits are what stop the card drifting into a twelfth source of truth. A rule that reaches the cheat sheet is a rule that now lives in two places — which is the failure the whole restructure undid. When a row is not enough, the row's link is the answer.
Shape the page for its tab¶
Every page starts with one # H1 and a one-paragraph purpose. No YAML front matter. Use !!! note / !!! warning / !!! tip admonitions sparingly and purposefully. Then:
- Operator article: a goal sentence naming the task → numbered steps or short imperative sections → every concept shown with a fenced ```markdown example before it is explained → the why woven into the step it changes, never lifted into a section of its own → one growing example that ends by exercising the optional pieces → a short Related list of sibling operator articles.
- Contributor model page: Concepts (terms with Polish examples) → data model / contracts (exact shapes) → behavior / pipeline (step by step, invariants called out) → cmdlet surface (the relevant
Verb-Nerthus*names only, plus a link to the API reference — no per-page route tables) → examples with real repo data → see also. - Reference page: tables and terse definitions. Table cells hold one clause; behavior, caveats, and rationale live in prose under the table, never inside cells.
- Contributor how-to page: a goal sentence, numbered steps or short imperative sections, one worked example, links to the model page for the why. Short.
Keep each page focused on one subject; push shared concepts to their owner page and link — subject to the operator self-containment rule, which wins.
Follow the shared style rules¶
Both registers obey these:
- Active voice, present tense, name the actor. "The daemon rejects the write", not "the write is rejected".
- One sentence, one idea — ~35 words is the ceiling.
- State each fact once per page. In-page repetition is a bug, except in a deliberate closing recap.
- Cut what the reader doesn't need. Most important information first; an edge case that doesn't change the contract moves to its owner page or goes.
- Headings describe the content beneath them — scanning headings alone must not mislead.
- Concrete beats long. Every abstract claim gets a worked example with real repository data —
Eraster,Lord Tussal,Thuzal,Ithan,Anward,PU: 0.2— neverFoo,Player1, or an invented URL. - A negation must earn its place. Write "X, not Y" only when Y is a specific, plausible thing the reader might otherwise believe or do. If Y is just the mirror of X, the negation is bloat and the positive claim already carries it: "Polish is the domain language" needs no ", not a translation"; "one session" needs no ", not two". Keep the negation when it does the work — "nicks change over the years, the Margonem profile id never does" is the fact, and "retiring is a status change, never a deletion" corrects the reader's instinct. The same applies to pile-ups: two negations asserting one idea means the abstract one goes and the specific one stays.
Obey the consistency rules (the ones that bite)¶
- One name per entity across a page. If the metadata calls a character
Lord Tussal, every recognition sentence saysLord Tussal. When Polish prose uses a declined form (Tussalem), name the link once so the reader is not guessing. - Examples match their explanation. A location the closing text lists (
Rezydencja Tussal) is spelled exactly as in the example block above it. - Follow a stated format literally. If the transfer form is
amount denomination, source -> target, the example writes it that way, comma included, with canonical entity names. - A denomination or quantity reads the same everywhere on the page. Pick one form of a currency name and use it in the example line and the closing summary alike.
- Keep an intentionally-wrong example wrong. When a page shows a fix — a malformed header corrected by
Data/Tytuł/Narratoroverrides — the "before" keeps its errors verbatim; only the corrected values must agree with the rest of the page.
Worked example — the block and the sentence beneath it must agree:
Correct explanation: "Nerthus.Core moves 10 Korony from Eraster to Lord Tussal." Broken explanation: "10 koron goes to Tussala" — a declined denomination instead of the canonical Korony, a declined name, and no anchor back to the example.
Name the surface consistently¶
When a page touches the implementer surface, these names are fixed:
- Cmdlets:
Verb-Nerthus<Noun>, approved verbs only:Get Set New Remove Resolve Test Invoke Find Add Open Close Start Stop Initialize Send Compare Connect Grant Revoke. Domain nouns are English in the cmdlet but map to Polish concepts (Get-NerthusPlayer⇒ Gracz). - Routes:
/v1/api/<resource>/..., lowercase, kebab where needed (/workflows/award-pu,/name-index/rebuild). - C# namespace:
Nerthus.*(e.g.Nerthus.MarkdownScanner,Nerthus.DeclensionEngine). - Generated index file:
nerthus.entities.md— the one unified index; the importer writes it and never rewrites hand-authored Polish sources. - Capabilities:
<resource>.<action>[.own], e.g.entity.read,pu.award,session.write.own.
The cmdlet ⇄ route mapping lives in exactly one place: the API reference. A new route or cmdlet goes there in the same change that introduces it — see Add a feature.
Avoid the anti-patterns¶
- Any "pick your mechanic", "campaign-agnostic", "for other RPG systems", "swap the language", or plugin-install language. Nerthus is the one and only system.
- English-canonical tags or keys with Polish aliases. Tags and keys are Polish literals.
- An in-process, embedded, or direct-file-read fast path on the client. The client is always a REST client.
- A
@Zmianysession change-directive, or a skill-check / "sprawdzajka" subsystem — both are removed. Entity mutations are entity writes; advancement is a freeform note. - Implementation-plan identifiers. Name the contract or behavior, not the plan that introduced it.
- Inventing fields, routes, or cmdlets not consistent with Architecture and the API reference. A new one is added to those canonical lists too.
- Re-stating a concept another page in the same tab owns. One sentence plus a link, or nothing — the cheat sheet is the single exception, under the limits above.
- Splitting an operator topic across a task page and a concept page, or giving an operator article a "How it works" section that restates the steps above it. That shape is what produced eight contradictions and it is not coming back.
- An operator page linking into
reference/,explanation/,how-to/, or amodel.mdto complete a task. Write the fact into the article instead. - Conflating session logs (game transcripts archived under
nerthus.logs/) with Discord delivery or with the daemon's internal log streams — the logs model keeps the three apart.
Cross-link by relative path¶
Link only to pages on this site, by relative path from the current page. Across the tabs, linking runs one way only:
- A contributor page may link to an operator article. A model page pointing at its plain-language counterpart is useful, and nothing about a contributor's task breaks when they follow it.
- An operator page never links into contributor internals — not
reference/, notexplanation/, nothow-to/, not anymodel.md. An operator finishes the task from the one article. A fact that lives only on a contributor page gets written into the operator article in operator register. - Operator pages link to sibling operator articles freely. The Related list at the foot of each article is for exactly that, and the cheat sheet links to every owning article by design.
- Every contributor model page links back to Architecture and forward to the pages it depends on.