Tag schema¶
Nerthus.Core (until cutover). This page describes the frozen system that runs today and is deleted at cutover. Replaced by: not yet written.
Lookup reference for the closed, validated, Polish-only @tag schema carried on entity blocks in nerthus.entities.md and its overflow files. It covers the recognized tag set, link-value format, the temporal range grammar and its evaluation rules, the historical write rule, and the projected entity record. The surrounding model — index files, merging, the claim ledger — is the entity model.
The closed tag table¶
Every recognized tag, its kind, and the entity types that may carry it:
- multi — repeatable; evaluation returns every active entry.
- scalar — single current value; last-active-wins.
- scalar, historical — scalar whose updates close-and-append instead of replacing (see the historical write rule).
- Types — the
@typvalues whose blocks may carry the key;anymarks a key legal on all eight.
| Tag | Meaning | Kind | Types |
|---|---|---|---|
@alias |
Alternative name, added to the resolver's name set — never consulted for mention routing | multi | any |
@lokacja |
The entity's place: its containing location, or on a Mapa the place the map depicts | scalar, historical | NPC, Grupa, Lokacja, Mapa, Postać, Przedmiot, Region |
@drzwi |
Physical access edge to another Lokacja/Mapa (a "door"); log-derived lines carry (auto) (Locations model) |
multi | Lokacja, Mapa |
@typ |
Entity type (authoritative over the ## Category heading) |
scalar, historical | any |
@należy_do |
The entity's owner — commonly a Postać's owning Gracz | scalar, historical | NPC, Postać, Przedmiot |
@grupa |
Group / faction membership. A value that names a Grupa block is an organisation; any other value is a tier (the importer's Heroic tier) — readers tell them apart by the value, never by the tag. The lineage autopilot writes (auto) lines of the first kind only (Entities) |
multi | NPC, Postać, Przedmiot |
@status |
Aktywny / Nieaktywny / Usunięty; quarantined location buckets also use Niepewny |
scalar, historical | any |
@ilość |
Quantity for stackables (currency, items) | scalar, historical | Przedmiot |
@rasa |
The character's race as its file states it (elf, człowiek, kitsune, wampir, krasnolud, drow); read by the Kto to? engine to list every cast member an epithet fits (sessions). Several lines for a mixed heritage |
multi | NPC, Postać |
@plik |
Source file path — the distribution target for session writes (Sessions model) | scalar | any |
@ref |
The file the entity's operator-authored properties are read from — where @plik says where session copies land, @ref says where facts live; semantics, file shapes, and the per-key split are the entity model's |
scalar | any |
@generyczne_nazwy |
Comma-list of generic names added to the resolver (Korony Elanckie, Korony) |
multi | any |
@nazwa_nerthus |
Canonical Nerthus display name when the heading is a variant | scalar | any |
@koordynaty |
On a Lokacja its position on a parent Mapa (grid x,y); on a Mapa the top-left tile of its render on Margonem's world grid |
scalar, historical — snapshot on a Mapa | Lokacja, Mapa |
@slug |
Margonem URL slug | scalar | Lokacja, Mapa |
@margonemid |
Margonem profile ID (a person, authored in nerthus.contributors.md) or map ID (Mapa, in the index) — authoritative identity. Scalar means last-active-wins, not single-entry: a person who changed Margonem account carries one line per id with the retired one's range closed |
scalar, historical | Mapa, Gracz |
@url |
Primary image URL of any entity (Mapa render, token art, portrait); on a Mapa the CDN image is versioned, so updates keep history | scalar, historical | any |
@wymiary |
Dimensions in tiles (W×H) |
scalar | Mapa |
@pu_startowe |
PU STARTOWE — starting skill points | scalar | Postać |
@pu_suma |
PU SUMA — total skill points (daemon-computed snapshot) | scalar | Postać |
@pu_zdobyte |
PU ZDOBYTE = SUMA − STARTOWE (derived) | scalar | Postać |
@pu_nadmiar |
PU NADMIAR — unbounded monthly-cap overflow pool | scalar | Postać |
@url_nerthus |
Nerthus-hosted mirror of the primary image | scalar | Mapa |
@url_grafika |
Alternate/additional image URL (battle map, old portrait, variant) | multi | any |
@url_dokument |
Linked external document (Google Docs sheet, pastebin) | multi | any |
@url_strona |
Linked website/page (nerthus.pl article, forum thread) | multi | any |
@typ_lokacji |
Exterior/interior marker (zewnętrzna/wewnętrzna) |
scalar, historical | Lokacja, Mapa |
@outerior |
The chain root (topmost Świat gry/ directory) a Lokacja resolves to — importer-computed; its distinct values are also the first level of the generated Lokacje/ registry |
scalar, historical | Lokacja, Mapa |
@inheritsfrom |
Reserved: an interior inheriting its world position from its Outerior | scalar | Lokacja, Mapa |
@isexterior |
Whether a map is outdoors — the one exterior marker any code reads (true/tak/1); the seam rules and the world grid consult it, @typ_lokacji is read by nothing. Written as a by-product of placing a map on the world grid, not as a bulk chore (Maps) |
scalar | Lokacja, Mapa |
@pochodzenie |
The session a block's name was first read in — the header of that session, as GET /entities/{name}/lineage reads it off the transcripts (rulings H126.5 / H127.8). Written (auto) by the lineage autopilot under its two-session / two-log guard, or by hand; a hand-written value stands and the autopilot never replaces it |
scalar | NPC, Postać, Grupa |
@first_session |
First session date that attests the entity. Emitted with (auto) on a harvested NPC proposal (the character harvest); elsewhere first-mention dates surface as the (YYYY-MM:) scopes on @lokacja/@outerior |
scalar | NPC, Postać |
@floor |
Recognized on read only: floor number parsed off a p.N name suffix — never machine-emitted (the importer derives the floor from the map name itself); a hand-authored line is honored on read and projected onto the entity record |
scalar | Lokacja, Mapa |
@base_name |
Recognized on read only: base name with the floor/room suffix stripped — never machine-emitted (the importer derives it from the map name and emits the base name as @alias); a hand-authored line is honored on read, projected onto the entity record, and joins the resolver's name set |
scalar | Lokacja, Mapa |
@forma_sesyjna |
A claim: routes one literal form to this block — a session @Lokacje form on a Lokacja, a transcript speaker nick on an NPC; a (auto) suffix marks a machine-appended claim (the claim ledger) |
multi | Lokacja, NPC |
@duplikat |
Source files of duplicates dropped at import — machine-owned, re-derived at every merge | multi | any |
@discord |
Discord channel kanał, kategoria the bot posts notices to, resolved by name. On an index block (a Postać) it is two-component and unpurposed. A person's channels are not index tags at all: they are authored on their ## Osoby block in nerthus.contributors.md, where the value takes an optional third purpose component (contributor store reference) |
scalar | NPC, Grupa, Gracz, Postać |
@region |
The region a Lokacja (or nested Region) belongs to, as of a date; the value names a Region block (Region model) |
scalar, historical | Lokacja, Region |
@tematy_zastrzeżone |
A person's content triggers (Tematy zastrzeżone) as a typed field — authored on their ## Osoby block in nerthus.contributors.md and carried onto the projected Gracz record (Players model) |
scalar | Gracz |
Note
The PU tags are a snapshot the daemon maintains; PU itself is computed monthly from session PU metadata and is authoritative — PU model. Currency @ilość and transfers: Currency model.
Type scope is checked on read, never on write. The create and update routes validate a tag key against the flat schema above; neither consults the Types column, so @wymiary on the Postać Lord Tussal lands in the block like any other tag. Integrity reports it afterwards as OffTypeTag:wymiary, at Warning. An off-type @region line draws RegionMemberType too. GET /v1/api/schema (API reference) publishes the map: tagsByType keyed by the eight types, alongside universalTags for the keys every type carries. A client derives the Types column from those two fields rather than hardcoding it.
Containment has exactly one direction: the child's @lokacja. The former parent-side @zawiera convenience is retired from the closed schema; a leftover @zawiera line is an UnknownTag integrity error like any other unrecognized key.
On a Mapa, @koordynaty and @wymiary together form the rectangle the world-grid layout's overlap refusal checks — two maps claiming one tile is refused (Maps). Unplacing a map removes the @koordynaty line rather than writing an empty value. @wymiary is recorded in tiles, converted from the render's pixel size; a PNG whose sides are not an exact multiple of 32 px is refused (NotTileMultiple) rather than rounded.
On a Mapa, @lokacja is depiction, not containment. It names the place the map depicts — the importer links a map to the Lokacja carrying its base name, and the location tree and the containment graph read Lokacja blocks only, so a Mapa is never a node in the world hierarchy. One place may be depicted by several maps (floors, event copies); each map depicts exactly one place, and map-level @drzwi doors project onto locations through this link (Locations model).
@należy_do names the owner, not only a character's player. A Przedmiot — a coin holding included — names its holder this way, and that holder may be a Postać, an NPC, a Grupa, a Gracz, or a Lokacja; currency classes a holding by its holder's type (a Postać's coin is physical Margonem items, every other holder's is RP bookkeeping). A holding hangs off exactly one of @należy_do or @lokacja (Currency model).
Closed-schema validation¶
The recognized-tag set is a fixed list in code, enforced at two seams:
- On write: unknown tag ⇒ reject. A tag update naming
@faction(English) or a typo@grpais refused with a validation error (HTTP 422) before anything touches disk. There is no generic catch-all. Entity creation validates its seed tags against the same schema, additionally rejects an unknown@typ, and refuses a(name, type)pair a block already carries with409— a duplicate heading would merge into one entity at parse time and shadow the new block from every write path. - On read: unknown tags are reported, not dropped. The scanner keeps every
@keyit finds, so a hand-edit is never silently lost;Test-NerthusEntityIntegrityreports the full finding set:
| Finding | Severity | Meaning |
|---|---|---|
UnknownTag |
Error | A tag key outside the closed schema |
UnknownType |
Error | A @typ value outside the eight types |
InvalidRange |
Error | A (from:to) suffix whose date tokens do not parse |
InvertedRange |
Error | A range with from > to |
UnresolvedReference |
Warning | @należy_do/@lokacja/@drzwi/@region naming no entity in the model (lore may legitimately lag); a @region value must name a Region block specifically — a same-named Lokacja does not resolve it |
InvalidUrl |
Warning | A @url_* value whose first token is not an absolute http(s) URL |
LocationCycle |
Error | A @lokacja containment chain that loops instead of reaching a root. A Mapa is exempt: its @lokacja names the place it depicts, so a map named after that place points at itself by design |
OffTypeTag:<key> |
Warning | A recognized tag on an entity type the Types column does not list it for |
RegionMemberType |
Warning | @region on an entity that is neither Lokacja nor Region |
RegionCycle |
Error | A @region nesting chain that loops instead of ending |
RefBroken:<Reason>:<path> |
Error | An @ref whose target cannot supply properties; the entity serves its inline tags only. <Reason> is one of the closed classes — Missing, Empty, AbsolutePath, EscapesRepo, PrivateDir, ChokeFile, IndexFile, NotMarkdown, Symlink, InvalidForm, NoMatchingBlock, Chain, MultipleRefs (the entity model) |
Diacritics are canonical. On disk a tag is its full Polish form: @należy_do, @ilość, @status: Usunięty, type Postać. The daemon writes diacritics only. Exactly two legacy ASCII spellings — @nalezy_do and @ilosc — are recognized on read (model build and write bodies) and normalized to the diacritic key; no other ASCII tag variant is mapped. Legacy-read normalization of values at adoption belongs to the import model.
Link values (the @url_* family)¶
The URL tags split by role and by what the link points at, so a renderer knows an image from a plain link without fetching it:
| Tag | Role | Kind |
|---|---|---|
@url |
The entity's one primary image; the Margonem CDN versions map images, so a newer render closes the old line and appends — the map checkup writes the new value with (auto) |
scalar, historical |
@url_nerthus |
Nerthus-hosted mirror — replace in place | scalar |
@url_grafika |
Alternate images | multi |
@url_dokument |
Documents | multi |
@url_strona |
Websites | multi |
A link value is: an absolute http(s) URL, then an optional label in double quotes, then the optional (auto) marker, then an optional temporal range — in that order. The URL is the first whitespace-delimited token; the label is display text for the link. Temporal ranges work as on every tag, so retired art stays queryable:
### Rezydencja Tussal
- @typ: Lokacja
- @url_grafika: https://i.imgur.com/QVi9jT2.png "Mapa poglądowa Rezydencji Tussal"
- @url_grafika: https://i.imgur.com/1Lcwrm8.png "Mapa z ogrodem" (:2026-06)
- @url_dokument: https://docs.google.com/document/d/1AbC "Zamówienie na serwis do herbaty"
Integrity validates the shape: a value whose first token does not parse as an absolute http(s) URL is reported as InvalidUrl (Warning — legacy lore may hold odd links). A Gracz profile link needs no tag: it derives from @margonemid.
Temporal ranges¶
Any tag value may carry a parenthesized validity range after the value:
- @lokacja: Ithan (:2026-07) # active until Jul 2026 (open start)
- @lokacja: Thuzal (2026-07:) # active Jul 2026 onward (open-ended)
- @status: Aktywny (2022-01:2026-06) # active Jan 2022 – Jun 2026
- @grupa: Gildia Teologów (2021-01:) # joined Jan 2021, still a member
- Format:
(VALID_FROM:VALID_TO). Either bound may be omitted for open-ended ranges. A value with no suffix is always-active. - Partial dates resolve to bounds:
YYYY→ the full year,YYYY-MM→ the full month (end bound uses the real day count),YYYY-MM-DD→ an exact day. - Range parsing is uniform — the suffix is recognized on every tag; integrity checks its validity, and the historical write rule below applies only to the ten
scalar, historicaltags. - No seasons. The only temporal primitive is the dated range.
Evaluation rules¶
"Current" is a computed query, never a stored field. Given an -ActiveOn date (default: now), the daemon evaluates the history:
- Scalar tags: last-active-wins — among entries whose range covers the query date, the one with the latest start bound. Undated entries tie-break by load order: the later file wins, giving overflow files their primacy over the base index. So Opat Perrin's current
Locationafter a 2026-07 move isThuzal, notIthan. - Multi-valued tags (
@alias,@grupa,@drzwi,@generyczne_nazwy, …): all-active — every entry whose range covers the query date, as a deduplicated set.
The historical write rule¶
For the ten scalar, historical tags (@lokacja, @status, @należy_do, @ilość, @koordynaty, @typ, @typ_lokacji, @outerior, @url, @region) an update never overwrites the active line. It closes that line's validity range at the write's effective month — the current month, unless the caller passes an explicit date — and appends the new value dated from that month. The block keeps the full timeline. Moving Opat Perrin to Thuzal in July 2026:
- @lokacja: Ithan (:2026-07) # was: Ithan — closed, not erased
- @lokacja: Thuzal (2026-07:) # appended
The rule is type-aware for one tag: on a Mapa, @koordynaty is a snapshot replaced in place, never close-and-append — the game does not slide a map sideways, so a repaint gets a new version suffix and there is no history to keep; only a Lokacja's @koordynaty stays scalar, historical. A block whose @typ the write cannot resolve falls back to the temporal rule, so history is never silently dropped.
One sanctioned variant: session transfers append their @ilość deltas as new lines dated to the session month, leaving the prior line as is — last-active-wins still resolves the newest balance, and the history keeps every step (Currency model).
A first-ever value is written undated, exactly as the importer writes it; setting the already-active value is a no-op. All other scalar tags (@pu_*, @slug, @wymiary, @plik, …) are tool-refreshed snapshots and replace in place; multi-valued tags always append a new line.
Soft-delete¶
Removal is the historical write applied to @status: the active status line is range-closed and Usunięty is appended, keeping every bullet. Semantics and consequences are in the entity model; the operator recipe is Add, edit, and retire entities.
The (auto) marker¶
A value carrying (auto) is machine-written and re-derivable; an unmarked value is operator-authored and preserved verbatim by every merge — the full authorship contract is the entity model. Grammar facts:
- The marker sits directly after the value (and its label, on a link value), before any temporal range:
- @drzwi: Thuzal (auto),- @url: https://…/rezydencja_tussal.1.png (auto) (2026-07:). That order keeps a marked line range-closable. - Matching folds the marker off —
Thuzal/Rezydencja Tussal (auto)andThuzal/Rezydencja Tussalare the same claim with different authorship. - The daemon strips the marker from every projected value except
SessionForms, which keeps claim lines raw. - Four entity-tag families carry it today:
@forma_sesyjnaclaims, the log-derived@drzwidoors, the checkup-refreshed Mapa@url(Locations model owns the latter two derivations), and the@first_sessiona harvested NPC proposal is dated with. - The marker is not entity-only. A session's
@Plikisection — the distribution target list the tool writes and rewrites whole — carries it on the section line:- @Pliki: (auto)(Sessions model). The section has no value of its own, so the marker sits where a value would. It means what it means everywhere else: the tool authored this and owns it, so an operator edit is overwritten on the next pass.
The projected entity record¶
The daemon parses the Markdown into a structured object; cmdlets return this shape with temporal fields already resolved to the requested -ActiveOn date. All values are the raw tag strings — the daemon does no numeric coercion (Quantity is "50", not an integer).
| Field | Source tag(s) | Notes |
|---|---|---|
Name |
### heading text, overridden by @nazwa_nerthus |
Canonical name |
Heading |
### heading text |
The literal block heading |
Type |
@typ (fallback: the ## Category) |
One of the eight types |
Category |
the ## Category heading |
Grouping only |
Status |
@status (default Aktywny) |
See the tag table |
Aliases |
@alias + @generyczne_nazwy |
All-active alternative names |
FilePath |
@plik |
Distribution target |
DuplicatesDropped |
@duplikat |
Duplicate sources dropped at import |
Those are universal — every record carries them. The fields below are type-scoped,
present when the entity's @typ declares the tag or the block carries it
(declared(type) ∪ present, the entity model):
| Field | Source tag(s) | Notes |
|---|---|---|
Owner |
@należy_do |
The entity's owner |
Groups |
@grupa |
All-active memberships |
Origin |
@pochodzenie |
The session header the name was first read in |
Location |
@lokacja |
Last-active place |
Doors |
@drzwi |
All-active access edges |
Quantity |
@ilość |
Stackables/currency |
Coordinates |
@koordynaty |
Grid position on parent Mapa |
RestrictedTopics |
@tematy_zastrzeżone |
A Gracz's content triggers |
SessionForms |
@forma_sesyjna |
Claim lines, raw — the (auto) marker included |
Tag-presence fields are added to the record of any entity carrying the tag — a Gracz has a MargonemId; a Mapa has MargonemId, Url, Slug, Dimensions. The set: PuStartowe, PuSuma, PuZdobyte, PuNadmiar (the Postać PU snapshot), plus MargonemId, Url, UrlNerthus, Slug, Dimensions, Outerior, InheritsFrom, IsExterior, Floor, BaseName, Region (@region, resolved as of the query date), and the all-active link arrays ImageUrls (@url_grafika), DocumentUrls (@url_dokument), WebsiteUrls (@url_strona) — link values raw, the quoted label included.
Ref provenance fields ride the record of an @ref-carrying entity only: RefFile (the @ref value as written, repo-relative), RefForm (2.1 for a leading-list target, 2.2 for an index-shaped one), and — on a broken ref — RefBroken (true) with RefBrokenReason (the failure class from the integrity table above). An entity without @ref omits all four; the entity model owns the semantics.
Routes, envelopes, and the cmdlet ⇄ route mapping for entity reads and writes: API reference. How @alias and @generyczne_nazwy feed the resolver: Name resolution.