The roster model¶
Nerthus.Core (until cutover). This page describes the frozen system that runs today and is deleted at cutover. Replaced by: not yet written.
The roster subsystem defines the two people-facing entity types: Gracz — the real
person, keyed by their Margonem profile ID — and Postać — an in-fiction character
owned by exactly one Gracz. This page specifies the exact contracts: where a player
record lives and what its tags mean, the retired Gracze.md adoption source and its
seed parse rules, the roster invariants, and the API-managed Dodatkowe informacje
entries. For the plain introduction, start with People and roles and Player characters.
Concepts¶
- Gracz — the real person. Identity is the Margonem profile ID (an integer like
9826541, stable for as long as the person keeps that account); nicks are fungible, and each Margonem account caps at six game characters. The friendly block heading (e.g.### Eraster) is a display label. - Postać — an in-fiction character (e.g.
Eraster,Losso Minewit,Felkissian). Linked to its owner by@należy_do; has its own entity record, currency holdings, session participation, and PU accounting. - Active character — each Gracz designates one "current" Postać:
@status: Aktywnyon that Postać,@status: Nieaktywnyon the others. InGracze.mdthis was encoded by bold-linking exactly one character. - Discord channel — the player's personal Discord channel, carried as
@discordon their block in the contributor store; PU and Intel notifications for any of that player's characters are posted there by the bot, resolving the name to a channel id. Those notifications ask for the channel purposedgracz— a person who also narrates carries a second, differently purposed line on the same block (the contributor store). See Logs & Discord. - Tematy zastrzeżone — content triggers the player does not wish to see in
sessions they take part in (e.g.
Banany), carried as the typed scalar@tematy_zastrzeżoneon the person's block and projected asRestrictedTopics. Narrators consult it when planning a session.
The split is deliberate: PU, the Discord channel, triggers, and identity attach to the person; currency, reputation, sessions, and the character sheet attach to the character. One person, several characters, one active. That split is now also a storage split: the person's facts live in the hand-authored contributor store, the character's in the generated entity index.
The roster record: a person in the store, a character in the index¶
A player is a ### <nick> block in the ## Osoby section of
nerthus.contributors.md — the same block that carries their roles, their capabilities
and, if they narrate, their narrator channel. What makes them a player is
@rola: gracz: the daemon projects one Gracz record per such block into its
entity model, so @należy_do resolution, .own scoping, @Intel fan-out, transfers
and name resolution all behave exactly as before. The storage moved; the model did not.
Their characters stay in the ## Postacie Graczy section of nerthus.entities.md,
seeded from Gracze.md at adoption (Adoption) and daemon-written
thereafter.
The store is hand-authored and never regenerated, so the durable/machine section
split no longer applies to people at all — that distinction existed only because a
hand-authored thing was parked in a generated file. It still governs the index and is
specified in the entity model, along with the block format
(### heading + flat @tag bullets) both files share.
# nerthus.contributors.md
## Osoby
### Eraster
- @margonemid: 9826541
- @status: Aktywny
- @rola: gracz
- @discord: gracz-eraster, gracze, gracz
- @tematy_zastrzeżone: Banany
# nerthus.entities.md
## Postacie Graczy
### Eraster
- @typ: Postać
- @należy_do: Eraster
- @status: Aktywny
- @plik: /Postaci/Gracze/Eraster.md
- @pu_startowe: 20
- @pu_suma: 127.90
- @pu_zdobyte: 107.90
- @pu_nadmiar: 0.00
The person block carries no @typ — the section states it — and gains
@rola: gracz, without which they are somebody in the file who is not in the game.
The full ## Osoby block shape, with multiple roles and purposed channels, is
the contributor store's: it owns the block's authorization
tags — @rola, @dostęp, @klucz_api — while this model owns the person's
player-facing tags — @status, @tematy_zastrzeżone, @alias. The @discord line is
shared by facet: the contributor store owns its purpose grammar and delivery resolution,
this model owns it as the player's channel.
Eraster's other three Postacie (Losso Minewit, Felkissian, Klusp) follow the
same shape with @status: Nieaktywny; legacy decimal scale is preserved as written
(42.1, 0.00), and @pu_zdobyte is derived as SUMA − STARTOWE.
Field reference (Gracz / Postać entities; the full tag schema is in the tag reference):
| Tag | On | Authored in | Meaning |
|---|---|---|---|
@typ |
Postać | index | Postać. A person's block carries none — the ## Osoby section states it |
@rola: gracz |
person | store | what makes the person a player; the projection predicate |
@margonemid |
person | store | authoritative profile ID (identity key); multi + temporal — a retired account keeps its line with the range closed |
@należy_do |
Postać | index | owning Gracz (links character → person) |
@status |
both | store / index | Aktywny / Nieaktywny / Usunięty — soft-delete only (entity model) |
@alias |
both | store / index | alternate names (Lord Tussal known as Tussal); nick history on a person |
@plik |
Postać | index | the character source file — the distribution target for sessions |
@discord |
person | store | the player's Discord channel as kanał, kategoria[, cel] — the bot posts to it by name. Hand-authored; the importer seeds none, so a freshly seeded roster has no channels until the Rada fills them in |
@tematy_zastrzeżone |
person | store | the player's content triggers as a typed scalar; the block carries no freeform prose (projected as RestrictedTopics) |
@pu_startowe @pu_suma @pu_zdobyte @pu_nadmiar |
Postać | index | the four PU accounting fields — semantics owned by the PU model |
Two rows carry a design decision:
- A Gracz carries no
@plik, so session distribution skips a Gracz participant. The legacyGracze.mdroster carried@plik: Gracze.md— distribution would then recreate the deletedGracze.mdon every settle run. The import drops the tag. @discordresolution. Delivery asks for a channel by purpose —graczfor PU and@Intel. The chain is the entity's own@discord, then the person's channel for that purpose, then their unpurposed default slot, then the## Roleblock for that purpose, else it records a skip. The channel name is committed lore, but posting to it needs the out-of-band Discord bot token — not a committed webhook URL (Logs & Discord).
Why the person left the index — and why one block¶
Four reasons, all of them about where a statement belongs:
- One store for people. The contributor store was already keyed by
@margonemid, already parsed by the one scanner, already self-healed per request, and already carried a person's roles, capabilities and channel. The roster belonged there, in the same block — not beside it. - The durable-section rule existed only to protect a hand-authored thing parked in a generated file. With the roster in a file nothing regenerates, the rule has nothing left to do for people.
- A person's
@discordwas stored twice, in two files, with a three-step fallback chain whose middle step existed only because of the split. - A human who narrates and plays was two blocks pretending to be two people. One
block with two
@rolalines says it plainly, and lets each capacity have its own channel.
Identity over nick
The Gracz heading and @należy_do carry the friendly nick for human
readability, but identity always reconciles to @margonemid; a nick change
updates only the label and @alias. See
identity & permissions.
And when the account itself changes
A profile id is stable for an account. Somebody who moves to a
new Margonem account closes the range on their old @margonemid and adds a line for
the new one, in the same block. Live decisions resolve as of now, so the old account
stops authenticating on its date; history — grants, links, audit lines — still
resolves through the old id. The operator steps are
people and roles.
The retired seed source: Gracze.md¶
Gracze.md was the hand-authored player database and the roster's adoption
source. The import's bootstrap path reads it once and appends the ## Osoby section to
nerthus.contributors.md; once that section exists it is the roster record, and a
re-import preserves it by presence (the seed no longer runs). The tool never writes to
or deletes Gracze.md — retiring the file is the operator's own git commit after
adoption, and git history preserves the archive. Adoption aborts with a structured
RosterSourceMissing error only when there is neither a ## Osoby section nor a
Gracze.md (Adoption).
Seeded blocks carry no @discord. The initializer cannot know a channel name, and
guessing one would route a real person's private notifications to the wrong place — so
every seeded block ships without one and the Rada fills them in by hand afterwards. The
tool keeps the list of who still needs one.
A campaign whose nerthus.entities.md still carries a ## Gracze section is refused
rather than migrated: two rosters cannot be merged by guessing, and the manual recipe
is People and roles's.
The seed parses a fixed per-player block shape:
### Eraster
- Postaci:
- **[Eraster](/Postaci/Gracze/Eraster.md)**:
- PU: NADMIAR: 0.00, STARTOWE: 20, SUMA: 127.90
- Wręczył Tussalowi łapówkę w postaci dziesięciu koron
- [Losso Minewit](/Postaci/Gracze/Losso Minewit.md):
- PU: NADMIAR: 0.00, STARTOWE: 40, SUMA: 42.1
- [Felkissian](/Postaci/Gracze/Felkissian.md):
- PU: NADMIAR: 0.00, STARTOWE: 41, SUMA: 52.95
- [Klusp](/Postaci/Gracze/Klusp.md):
- PU: NADMIAR: 0.00, STARTOWE: 64.00, SUMA: 65.40
- Tematy zastrzeżone: Banany
- ID Margonem: 9826541
Parse rules the seed applies:
| Source line | Meaning | Maps to |
|---|---|---|
### Eraster |
friendly label for the player block | Gracz heading (display name) |
**[Char](…)** (bold link) |
the active character | Postać @status: Aktywny |
[Char](…) (plain link) |
an inactive character | Postać @status: Nieaktywny |
(/Postaci/Gracze/Char.md) |
the character's source file | Postać @plik |
PU: NADMIAR: …, STARTOWE: …, SUMA: … |
per-character PU baseline | @pu_nadmiar / @pu_startowe / @pu_suma (+ derived @pu_zdobyte) |
Aliasy: Tussal |
alternate names | Postać @alias |
free bullet under a character (Wręczył Tussalowi łapówkę…) |
narrator note | moved into the charfile's **Dodatkowe informacje:** |
Tematy zastrzeżone: Banany |
content triggers (player-level) | the person's @tematy_zastrzeżone (typed scalar) |
other player-level bullets (e.g. Tussal's - Inne:) |
roster notes | moved into the player's active-character **Dodatkowe informacje:** |
ID Margonem: 9826541 |
authoritative identity | the person's @margonemid (a #char profile URL is not carried onto the block) |
Every seeded block also gets @status: Aktywny and @rola: gracz.
Reading notes:
- The seed parses only the
## Listasection and skips fenced example blocks and unrecognized lines. The narrator-note bullets under each character are moved into that character's file, deduplicated against notes already there; the move mechanics (fail-early, per-bullet logging) belong to the import model. - The PU line accepts
ZDOBYTE:in place ofSUMA:— the seed reconstructsSUMA = STARTOWE + ZDOBYTE. Decimals accept dots or commas (42,1→42.1). BRAKis the uninitialized sentinel (this page owns its semantics):STARTOWE: BRAK— or a missing PU line — emits no PU tags, except that a numericSUMAbesideSTARTOWE: BRAKstill emits@pu_suma;NADMIAR: BRAKseeds@pu_nadmiar: 0. A ripple worth knowing: on such a block the first monthly award computes@pu_zdobyteagainst a missing STARTOWE (treated as 0), so it lands as the full legacy total — set@pu_startoweby hand or API first when the baseline matters for a new-character STARTOWE preview.ID Margonemmay be a bare id or a profile URL (…/profile/view,9826541#…) — the numeric id is extracted either way and kept as@margonemid. The full URL's#charcharacter-sheet anchor is not carried onto the person's block: the character's own**Karta Postaci:**link is that sheet's home, and git history keeps the retiredGracze.mdverbatim.- The PU values seeded here are the adoption baseline only; going forward PU is
computed and authoritative from session
@PU(PU model). - A player-character file with no roster entry still seeds as a Postać —
@typ/@status/@plikonly, no@należy_do. Files underPostaci/Gracze/Martwi/seed asNieaktywny.
The character file: Postaci/Gracze/<Char>.md¶
Each Postać has a hand-authored Markdown file. It is a human-authored Polish source —
the daemon reads it, distributes sessions into it (resolved via @plik), and
manages the Dodatkowe informacje entries in place — but it never rewrites the
freeform prose. Bold-label fields:
| Label | Contents |
|---|---|
**Karta Postaci:** |
link to the freeform character sheet on the Margonem profile (e.g. https://www.margonem.pl/profile/view,9826541#char_22788,nerthus) |
**Tematy zastrzeżone:** |
optional character-level trigger restatement |
**Stan:** |
current condition (Zdrowy., Brak., injury notes) |
**Przedmioty specjalne:** |
narratively significant items (everyday holdings live in entity currency/items) |
**Reputacja:** |
three buckets — Pozytywna / Neutralna / Negatywna, each a per-town list (Eder, Torneg, Ithan, …) |
**Dodatkowe informacje:** |
standing facts and hooks (blessings, debts, owed items) — API-managed entries, below |
**Opisane sesje:** |
a [[_TOC_]] followed by one block per session |
The Karta Postaci is freeform and external (the Margonem profile); the tool tracks only the link — modeling statistics or the diceless ruleset is a deliberate non-goal (Architecture).
Under **Opisane sesje:**, session distribution appends one block per session the
character took part in, keyed by the universal session header and copied verbatim
from the session record — the character file is a projection, not an authority.
Distribution mechanics: the session model.
Dodatkowe informacje — the API-managed entries¶
Each bullet under **Dodatkowe informacje:** is one entry the daemon lists, adds,
edits, and removes in place (the reversed legacy label **Informacje
dodatkowe:** is accepted on read — one legacy charfile uses it), through
Get-NerthusCharacterInfo, Add-NerthusCharacterInfo,
Set-NerthusCharacterInfo, and Remove-NerthusCharacterInfo. An entry may carry
the standard temporal (from:to) range, filtered by the all-active rule
(entity model). An edit or removal must restate the
entry's current text and is refused on a mismatch, so concurrent operators never
silently overwrite each other. Entry text is a single line by contract — the API
refuses embedded newlines, so an entry can never smuggle a fake session heading
into the sheet. The listed text is authoritative; the range-stripped value
also drops a trailing colon-bearing parenthetical, while matching and filtering
stay exact. The import moves the Gracze.md narrator notes into this section
during adoption — a character's own notes into that character's file, and each
player's free roster notes (- Inne: …) into their active character's file
(Adoption). Routes, bodies, and capabilities:
the API reference.
Behavior & invariants¶
- One owner, one active. Every Postać has exactly one
@należy_do. Setting a Postać's@statustoAktywnydemotes the owner's previously active Postać toNieaktywnyin the same write; both changes are audited. Creation is the one path that does not demote — a new Postać seeds the entity-create default@status: Aktywnywithout touching siblings, so promote/demote explicitly after creating an additional Postać. Status is a temporal tag — each flip is a dated append, never an overwrite (entity model). - Soft-delete only. Removing a Gracz or Postać is a
@status: Usuniętytransition (entity model); the record and its file persist. - The store is the roster record, and it is hand-authored. A person is changed by
editing their block in
nerthus.contributors.mdand committing — no route writes it. Character mutations still land in## Postacie Graczy; the tool never touchesGracze.md. - PU is computed, not edited. Roster cmdlets do not set PU; it flows from the
monthly batch (PU model). Creating a Postać seeds no
PU tags —
Get-NerthusNewPlayerCharacterPUCountpreviews the policy-derivedSTARTOWE, which the operator passes explicitly (e.g.-Tags @{ pu_startowe = '41' }). - Margonem id at creation. Creating a Gracz does not require
@margonemid, but a Gracz without one cannot be identity-linked (the Margonem/Discord login, identity & permissions) — pass it in-Tagsat creation. - Player writes do not exist. Every route that mutated a
Gracz— create, edit, soft-delete, set the Discord channel — now refuses and namesnerthus.contributors.mdas the file to edit (API reference). Reads are unchanged. The capability consequence, stated plainly:player.write.ownno longer lets a player set their own Discord channel — they ask the Rada, who edits the store and commits.player.writeandplayer.write.ownkeep their ids and still gate every character route, where ownership scoping applies exactly as before (identity & permissions; capability reference). - Single write gate. Every character mutation passes the write gate and fires the write hooks — see the API reference. A read-only host still answers at that gate first, before any roster refusal.
Cmdlet surface¶
Players: Get-NerthusPlayer reads. New-NerthusPlayer, Set-NerthusPlayer,
Remove-NerthusPlayer and Set-NerthusPlayerDiscordChannel still exist and now surface
a refusal naming the file to edit (API reference) —
kept deliberately, so an
existing script fails with the explanation rather than with a missing command. Player
Discord channel: Get-NerthusPlayerDiscordChannel reads (and answers null for a block
the Rada has not filled in yet — that is normal, not an error). Characters:
Get-NerthusCharacter fetches one by its own name, Get-NerthusPlayerCharacter -Player
lists everyone a Gracz owns, and New-NerthusPlayerCharacter,
Set-NerthusPlayerCharacter, Remove-NerthusPlayerCharacter write. A Gracz and their
Postać routinely share a name, so the two reads are worth telling apart. Character info
entries: Get-NerthusCharacterInfo, Add-NerthusCharacterInfo,
Set-NerthusCharacterInfo, Remove-NerthusCharacterInfo. Starting-PU preview:
Get-NerthusNewPlayerCharacterPUCount. A character's owning Gracz is also
resolvable through the API's entity surface. Routes, parameters, envelopes, and
capabilities: the API reference.
Examples¶
Eraster is one Gracz (@margonemid: 9826541) owning four characters, with
Eraster active:
PS> Get-NerthusPlayerCharacter -Player Eraster | Format-Table Name, Status, PuSuma
Name Status PuSuma
---- ------ ------
Eraster Aktywny 127.90
Losso Minewit Nieaktywny 42.1
Felkissian Nieaktywny 52.95
Klusp Nieaktywny 65.40
The Gracz and their active Postać share the name here, which is the case the parameter name
guards: -Player Eraster lists all four, Get-NerthusCharacter -Name Eraster returns the
one character.
PS> Get-NerthusCharacter -Name Eraster | Format-Table Name, Type, Owner, PuSuma
Name Type Owner PuSuma
---- ---- ----- ------
Eraster Postać Eraster 127.90
Switching the active character (demotes Eraster, promotes Felkissian in one write):
Previewing the starting PU for a new character of Eraster's, then passing it at creation:
PS> Get-NerthusNewPlayerCharacterPUCount -Player Eraster
PS> New-NerthusPlayerCharacter -Name 'Felkissian' -Tags @{ 'należy_do' = 'Eraster'; plik = '/Postaci/Gracze/Felkissian.md'; pu_startowe = '41' }
Reading Eraster's Discord channel and adding a Dodatkowe informacje entry:
PS> Get-NerthusPlayerDiscordChannel -Name Eraster
PS> Add-NerthusCharacterInfo -Name Eraster -Text 'Wręczył Tussalowi łapówkę w postaci dziesięciu koron'
Setting that channel is an edit and a commit, not a call — add the line to Eraster's
block in nerthus.contributors.md:
### Eraster
- @margonemid: 9826541
- @status: Aktywny
- @rola: gracz
- @discord: gracz-eraster, gracze, gracz
Why it works this way¶
- Nick-change history. Old nicks are retained as
@aliason the person's block; the live index always answers with the current nick. - Player-level vs character-level triggers. The Gracz value is authoritative; character-file restatements are informational.
- Creation vs the one-active flag. The one-active-character rule is enforced on update, not creation — see "One owner, one active" above.
- The roster record. Adoption retires
Gracze.md; the store's## Osobysection is the roster record, and deleting the file is the operator's commit. - Discord channel storage.
@discordon the person's block in the committed store names the player's channel, purposed for the capacity it serves; the bot posts to it by name, and the bot token — not any committed URL — is the one out-of-band secret (Logs & Discord).
See also¶
- Entity model — the 7-type model, the durable/machine index split, soft-delete; temporal scopes and the closed tag table are in the tag reference
- Session model — session headers and entity-driven distribution into character files
- PU model — the
STARTOWE/SUMA/ZDOBYTE/NADMIARaccounting and the monthly batch - Adoption — the bootstrap path and the notes move
- Name resolution — resolving nicks, aliases, and declined forms
- Identity & permissions — Margonem identity and capabilities
- Logs & Discord —
@discordchannel delivery - The contributor store — the
## Osobyblock shape, several roles per person, and purposed channels - Architecture · Glossary