Skip to content

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 @typ values whose blocks may carry the key; any marks 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 @grpa is 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 with 409 — 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 @key it finds, so a hand-edit is never silently lost; Test-NerthusEntityIntegrity reports 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.

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, historical tags.
  • 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 Location after a 2026-07 move is Thuzal, not Ithan.
  • 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) and Thuzal/Rezydencja Tussal are 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_sesyjna claims, the log-derived @drzwi doors, the checkup-refreshed Mapa @url (Locations model owns the latter two derivations), and the @first_session a harvested NPC proposal is dated with.
  • The marker is not entity-only. A session's @Pliki section — 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.