The entity model¶
Every tracked thing in Nerthus is one encja (entity): a named record carried in
Markdown and exposed by the daemon as a structured object. This page is the
contributor model of that record: the index files and how they merge, machine
authorship (the (auto) marker), and the claim ledger that makes
nerthus.entities.md hand-editable. For the plain introduction, start at the
entities overview; the closed tag table, the temporal range grammar,
and the write rules live in the tag schema reference.
Concepts¶
- Encja / Entity — one named record. Its identity is its canonical name plus its
type (e.g.
NPC/Opat Perrin,Lokacja/Ithan). Names are matched through the Polish-aware resolver; see Name resolution. - Type — one of seven fixed Polish kinds (below); the set is closed.
- Tag (
@tag) — a typed attribute carried as a flat- @tag: valuelist item under the entity heading. No tag takes nested child bullets. The recognized set is a closed schema, validated on write. - Prose — any line in a block that is not a tag bullet. It is operator comment: the model ignores it, the merge preserves it (below).
- Claim (roszczenie) — one
- @forma_sesyjna: <forma>line: it claims that literal session form for its block and routes mentions there. - Temporal value — a tag value may carry a
(from:to)validity range. - Soft-delete — an entity is retired with
@status: Usuniętyand stays in the index with its history.
The seven entity types¶
The seven types (NPC, Grupa, Lokacja, Mapa, Gracz, Postać, Przedmiot — the
operator descriptions are in the entities overview) are baked in. An
entity's type is its @typ tag; the ## Category heading only groups entities
in the file. At import, Grupa entities are derived from the
Organizacje/{Graczy,NPCów} directory tree — one Grupa per organization directory.
The Markdown shape¶
Entities live in the index nerthus.entities.md (and its overflow siblings, below).
The index is split: ## Gracze and ## Postacie Graczy are durable —
hand-editable, API-written, never regenerated — while ## NPC, ## Grupa,
## Lokacja, and ## Mapa are machine-derived from the sources, yet still
hand-editable: regeneration merges each block with its disk twin instead of
overwriting it (see the fixpoint merge below). The file opens with a short Polish
header (# Indeks encji Nerthus) that points at this documentation rather than
restating it (below). Each entity is a level-3 heading naming it, followed by a flat
list of @tag items, optionally interleaved with operator prose; an optional
level-2 category heading groups entities for human readability:
## NPC
### Opat Perrin
- @typ: NPC
- @alias: Perrin
- @lokacja: Ithan (:2026-06)
- @lokacja: Thuzal (2026-07:)
- @grupa: Gildia Teologów (2024-01:)
- @status: Aktywny
- @plik: /Postaci/NPC/Thuzal/Opat Perrin.md
- The same tag may repeat for multi-valued and time-scoped attributes (note the two
@lokacjalines above); evaluation rules are in the tag schema reference. - The tag list is flat. Every bullet is a
- @key: valueline, and an indented bullet under one is not read as anything.
Prose in a block¶
A block line that is not a - @tag: bullet is operator prose — a free comment, in
the same relationship to the tag list that a session's body has to its metadata. The
scanner ignores it (MarkdownScanner.ScanEntityIndex skips any non-bullet line), so it
carries no attribute, resolves no name, and routes nothing. It exists for the human
reading the block.
The merge preserves it. A prose run rides as its own entry, positioned among the tags it sits between, and is re-emitted fenced by exactly one blank line on each side — that canonical fencing is what makes the fixpoint byte-stable, since a block already spaced that way re-imports unchanged. A run is deliberately not glued to a neighbouring tag: a machine-owned tag that gets stripped at merge must not take an operator's comment down with it.
The fencing is a normalization, so blank-line spacing inside a block is the one thing about a hand edit that is not preserved: a run written with no blank line around it gains one, and a run padded with several loses the extras. Line breaks within a run are kept, so a multi-line comment stays multi-line. Blank lines are the block's only structural separator, which is why they are the merge's to set and not the operator's.
The importer reads the hand-authored Polish sources (Postaci/, Świat gry/,
Organizacje/, …) and emits nerthus.entities.md; when the index already carries
the durable roster sections, it carries them verbatim and regenerates only the
machine sections, merging each block with its disk state
(adoption). It never rewrites the sources, and files are written
UTF-8 no BOM with the original newline style — the data-owner contract is the
architecture's.
Index files, merging, and where writes land¶
The daemon scans two kinds of index file, in this load order: nerthus.entities.md
(the base index) first, then the hand-managed overflow files
nerthus-*-ent.md (sorted).
Blocks with the same name and the same type (from @typ, else the ## Category)
are merged into one entity across the base index and the overflow files: their tag
histories are concatenated, never replaced. An overflow block must therefore
repeat the @typ (or sit under a matching category heading) to merge; a same-named
block of a different type stays a distinct entity.
Overflow primacy — base first, overflow wins. Because scalar resolution tie-breaks prefer the later-loaded entry (below), an undated hand-edit in an overflow file overrides the bulk-generated base value without rewriting the base — for every type, locations and maps included. Hand-edits are picked up automatically — self-heal is the architecture's contract.
Writes land in the file the block actually lives in. A tag update or soft-delete
resolves the entity's source file (base or overflow) and splices that file. New
entities are appended to the base index under their type's category heading; new
Mapa blocks land in the base index's ## Mapa section. The daemon never creates
overflow files itself.
Writes into the durable ## Gracze / ## Postacie Graczy sections are permanent —
the roster is Players & characters's. The machine sections
are durable in a different way: a reindex (Invoke-NerthusReindex) rebuilds all
four from the sources and then merges each rebuilt block with its on-disk twin —
the disk block's tag lines win per tag key. A hand edit or API write into a machine
section therefore survives regeneration; only the machine-owned keys are re-derived.
The tag schema, in brief¶
Every recognized tag has one of three kinds:
- multi — repeatable; evaluation returns every active line (
@alias,@grupa,@drzwi,@forma_sesyjna, the link tags, …). - scalar — a single current value that tools replace in place (
@plik,@slug,@margonemid, the@pu_*snapshot, …). - scalar, historical — a scalar whose updates close-and-append instead of
replacing, keeping the full timeline (
@lokacja,@status,@należy_do,@ilość,@koordynaty,@typ,@typ_lokacji,@outerior,@url).
The tag schema reference owns the full contract:
the closed tag table, the link-value grammar, the temporal (from:to) range
grammar and its evaluation rules (last-active-wins for scalars, all-active for
multi tags), the historical write rule, the closed-schema validation seams with
the integrity finding set, the diacritics rule, and the projection field table.
Two structural rules belong here:
- Containment has exactly one direction: the child's
@lokacja. The former parent-side@zawieraconvenience is retired from the closed schema; a leftover@zawieraline is anUnknownTagintegrity error like any other unrecognized key. - A Gracz's content triggers are the typed tag
@tematy_zastrzeżone(projected asRestrictedTopics) — the one Gracz-block field for them (Players & characters).
Two evaluation facts the merge sections below rely on: undated scalar entries
tie-break by load order — the later file wins, giving overflow files their primacy
(above) — and "current" is always a computed query over the history, evaluated at
-ActiveOn (default: now).
Soft-delete¶
Removal is the historical write applied to @status — a transition, not a
deletion. Retiring a location in July 2026 closes the active status and appends
Usunięty:
### Ogród Tussala
- @typ: Lokacja
- @status: Aktywny (2024-01:2026-07)
- @status: Usunięty (2026-07:)
Remove-NerthusEntity performs exactly this edit; no bullet is ever deleted. A
Usunięty entity stays in the index with its full history queryable. The name
resolver stops resolving to it, listings can exclude it via the status filter, and
a soft-deleted currency holding leaves the supply as burned coin
(Currency & economy). The operator recipe is
Add, edit, and retire entities.
The entity record the daemon exposes¶
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). Core fields include Name, Type, Status, Aliases,
Owner, Groups, Location, Doors, Quantity, FilePath,
RestrictedTopics, and SessionForms; tag-presence fields (MargonemId,
Url, Slug, Dimensions, the Pu* snapshot, the link arrays, …) are added to
the record of any entity carrying the tag. The full field ⇄ tag projection table
is in the tag schema reference.
Machine authorship & provenance — the (auto) marker and git history¶
A tag line carries its authorship in the line itself. A value carrying the
(auto) marker is machine-written: re-derivable, and free to land differently
on a from-scratch rebuild. An unmarked value is operator-authored:
authoritative, preserved verbatim by every merge. Matching always folds the marker
off — Thuzal/Rezydencja Tussal (auto) and Thuzal/Rezydencja Tussal are the same
claim with different authorship — and the daemon strips it from every projected
value except SessionForms, which keeps claim lines raw.
The marker sits before any temporal range (exact grammar in the
tag schema reference), so a marked line stays
range-closable — retiring it edits only the range. Three tag families carry it
today: @forma_sesyjna claims (below), the log-derived @drzwi doors, and the
checkup-refreshed Mapa @url
(Locations & maps owns the latter two derivations).
Lineage lives in git history. The index is a committed file, so every
merge, split, rename, and re-route is a reviewable diff. The three projection
tags that once carried an in-file provenance log — @nadpisanie,
@scalono, @mention_count — are retired: a pre-claims index may still hold
them, and the first fixpoint merge strips them for good. @duplikat stays
machine-owned and is re-derived at every merge.
Identity vs. presentation — the derived path¶
The stable identity that references point at is the entity name (for Gracz/Mapa,
the opaque @margonemid); classification and topology (@typ, @lokacja,
@drzwi) are temporal tag values that move freely. A directory-style path
like Postaci/NPC/Thuzal/Opat Perrin is only a derived view of those tags, never
the identity. Get-NerthusEntityPath computes it from the type +
containment/ownership chain as of -ActiveOn, so the path moves on its own over
time. A Postać that becomes an NPC returns Postaci/Gracze/<Gracz>/<name> on an
early date and Postaci/NPC/<lokacja>/<name> later — no rename, no broken
references. A shared interior (one corridor seen under several exteriors in its
@forma_sesyjna) returns several paths (Shared = $true) — the "symlink" view.
Operator hand edits — the claim ledger¶
The operator channel is the index itself. nerthus.entities.md is
hand-editable end to end: the durable roster sections by design, and the machine
sections through the fixpoint merge below, which regenerates them around
operator edits. Every correction is a plain edit of the block it concerns.
The claim ledger — @forma_sesyjna routes mentions¶
Every - @forma_sesyjna: <forma> line on a block claims that literal session
form for the block. The ledger is the mention-routing plan: when the importer
meets a session @Lokacje form, it consults the claims before every
heuristic — a claimed form goes to its block, and only unclaimed forms fall
through to the deterministic pipeline
(Locations & maps). A (auto) suffix marks a
machine-appended claim; an unmarked line is an operator claim. Two rules keep the
ledger honest:
- A form claimed by two blocks is a routing conflict. The mention is
quarantined — a
@status: Niepewnyblock with reasonkonflikt-roszczeń— and the lint reports it (below). Nothing routes on a contested claim. - Deleting a claim line does not forget the form. The form becomes unclaimed, and the next reindex routes it heuristically again. To take a form away from a block, move its line to the right block — don't delete it.
Routing consults only claim lines; @alias remains the resolver-token tag for
clean alternative names (Name resolution). Every distinct
session literal — traversal routes and clean variants alike — lands in
@forma_sesyjna as a claim, so a clean variant may appear under both tags with
different roles: as @alias it resolves names, as @forma_sesyjna it routes
mentions.
The operator gestures¶
Every correction is a file edit; the block is the unit:
| Gesture | Edit |
|---|---|
| Adopt a machine guess | delete the (auto) marker on the line (claim or door alike) |
| Reject a machine door | close its range on both endpoint blocks — - @drzwi: X (auto) (:2026-07) stays matched at merge, so the rejection survives re-import (Locations & maps owns the pair semantics) |
| Reject a machine claim | close its range the same way — a range-closed @forma_sesyjna line still matches at merge and is never re-appended |
| Re-route a mention | move the claim line to the right block |
| Merge two places | move the loser's claim lines onto the survivor (note below) |
| Split a wrongly merged place | create city-qualified blocks and distribute the claim lines (next subsection) |
| Rename | edit the heading; keep the old name as @alias |
| Retype | edit @typ — the machine will not re-mint the name in the old section |
| Reparent | edit @lokacja |
| Correct anything else | edit the tag line |
| Pre-register a future entity | author a new block by hand |
Notes:
- Merge: after moving the loser's claim lines, add the loser's name as
@aliason the survivor, then soft-delete the loser (@status: Usunięty). Its mentions follow the moved claims. - Retype: a block owns its name across all machine sections — after the
@typedit the regeneration never mints a same-named twin under the old## Category. - Future entity: a hand-authored block may claim a form no session has used
yet; the claim routes it on first appearance. Until then the lint flags it as
roszczenie-bez-wzmianki— for a pre-registration that finding is fine.
A hand-authored block the sources don't produce — a future entity, a renamed or retyped survivor — is a disk-only block; the merge preserves it verbatim (below).
Homonyms and the split¶
Block headings must be unique. Two blocks with the same name and type
chimera-merge at parse time — the cross-file merge sees one entity — and only the
first is reachable by writes; the lint flags the duplicate heading, and the API
refuses to create one outright with 409
(tag schema reference). Homonyms are therefore
qualified by city. To split a wrongly merged place, create one block per
city — ### Gildia Teologów (Thuzal), ### Gildia Teologów (Ithan) — give
each the bare name as a shared @alias (- @alias: Gildia Teologów), and
distribute the claim lines: each city-prefixed form moves to its city's block.
The shared bare alias is also the split signature the heuristics key on.
When ≥ 2 live blocks share a bare @alias, a new, unclaimed mention of that
bare name never resurrects a bare block: the normalizer qualifies it by the
session's exterior city and routes — or mints — Gildia Teologów (Miasto). A
mention whose exterior city cannot be resolved has no home in the family and is
quarantined with reason homonim-bez-miasta.
The fixpoint merge — how edits survive re-import¶
Every regeneration entry point — the importer and the reindex — builds the machine
sections (## NPC, ## Grupa, ## Lokacja, ## Mapa) fresh from the sources,
then merges them per block with the on-disk index:
- The disk block wins per tag key. Its tag lines are carried verbatim — order, dates, formatting. The machine appends only whole tag keys the block lacks.
- Claims accumulate. The machine appends a
(auto)claim line only for a form new this run; a form already claimed on the disk block — marked or not — is never duplicated or overwritten. - Doors accumulate the same way.
@drzwimerges at value level with the marker and any range folded off: the machine appends an(auto)door only for a target new this run, and a range-closed door line still matches — so an operator's rejection (the range-close gesture above) is never re-appended. - Machine-owned keys are re-derived.
@duplikatis stripped from the disk block and re-emitted fresh; the retired projection tags@mention_count,@scalono, and@nadpisanieare stripped the same way and never re-emitted. - Disk-only blocks are preserved verbatim, their prose runs included. One
exception: a machine quarantine ghost — a block whose last undated
@statusisNiepewny— is dropped once the fresh build stops emitting it, i.e. once its form got claimed or resolved.Niepewnyis a machine-only status with the importer as its sole writer, so the status alone identifies the ghost; hand-authoring it on a block makes that block droppable. - The durable sections are untouched —
## Graczeand## Postacie Graczyare carried verbatim.
Idempotence is fixpoint stability: import(import(X)) == import(X),
byte-identical. It is not history-independence — a from-scratch rebuild without
the previous index may place its (auto) claims differently than the incremental
history did. The machine sections are state with a durable operator overlay —
the durable-section precedent extended to all four.
The in-file header¶
The header the importer writes says only that the file is editable, and points at the documentation for the rest:
# Indeks encji Nerthus
# Ten plik można edytować ręcznie — zmiany przetrwają ponowny import.
# Zasady edycji, znaczenie tagów i kontrola spójności: dokumentacja Nerthus.Core,
# "Running the campaign" → "Add, edit, and retire entities".
It used to be an instruction card restating the whole hand-edit contract in Polish at the top of every generated index. That made the contract two documents: the card and this model, each free to drift from the code and from each other. A pointer cannot drift. The contract lives in Add, edit, and retire entities for operators and here for contributors, and the file names where to find it.
Lint and apply¶
Workflow: edit nerthus.entities.md → Test-NerthusClaims (lint, read-only) →
Invoke-NerthusReindex (apply now: full regeneration + model refresh, no daemon
restart — the response embeds the same lint, so a conflict never takes effect
silently) → verify via Get-NerthusEntity / Get-NerthusLocation. The lint
findings, each with a plain-language Polish reason:
| Finding | Meaning |
|---|---|
konflikt-roszczeń |
one form claimed by ≥ 2 blocks — mentions quarantine until one line remains |
roszczenie-na-usuniętym |
a claim on a soft-deleted block — move the line to a live block |
zdublowany-nagłówek |
the same ### heading appears twice — only the first block is reachable; qualify by city |
roszczenie-bez-wzmianki |
an operator claim matching no observed session form — a typo (near-miss suggestions attached) or a pre-registered future entity (fine) |
Governance is git review: the index is a committed file, so hand edits travel
as commits and merge requests like any lore change, and every applied plan is its
own diff. There is no proposal workflow; location.override gates the reindex
route. Routes, envelopes, and capabilities: the
API reference.
Real examples¶
A Grupa — Gildia Teologów, with a distribution target so session writes that
name it land in its own file. Membership is carried on each member's own block
(- @grupa: Gildia Teologów on Opat Perrin), never on the Grupa itself:
## Grupa
### Gildia Teologów
- @typ: Grupa
- @alias: GT
- @lokacja: Thuzal (2023-01:)
- @status: Aktywny
- @plik: /Organizacje/NPCów/Gildia Teologów/Opis Ogólny.md
Zakon teologów działający na terenie Thuzal.
A Lokacja — a residence in the world hierarchy, placed under its town, carrying a door edge, its Margonem binding, and the machine claims for the session forms that route here:
## Lokacja
### Rezydencja Tussal
- @typ: Lokacja
- @lokacja: Thuzal (2021-01:)
- @drzwi: Thuzal (2021-01:)
- @slug: rezydencja-tussal
- @forma_sesyjna: Rezydencja Tussal (auto)
- @forma_sesyjna: Thuzal/Rezydencja Tussal (auto)
- @status: Aktywny
- @plik: /Świat gry/Thuzal/Rezydencja Tussal.md
A split homonym family — the theologians' guild chapters after an operator
split by hand: city-qualified headings, the shared bare @alias (the split
signature), and the claim lines distributed per city. The unmarked line is an
operator decision; the (auto) lines stayed machine:
### Gildia Teologów (Thuzal)
- @typ: Lokacja
- @alias: Gildia Teologów
- @lokacja: Thuzal (2024-01:)
- @forma_sesyjna: Thuzal/Gildia Teologów (auto)
- @forma_sesyjna: Gildia Teologów
- @status: Aktywny
### Gildia Teologów (Ithan)
- @typ: Lokacja
- @alias: Gildia Teologów
- @lokacja: Ithan (2024-03:)
- @forma_sesyjna: Ithan/Gildia Teologów (auto)
- @status: Aktywny
The operator claimed the bare Gildia Teologów for the Thuzal block, so bare
mentions route there. Had that line been deleted instead, a new bare mention would
fall to the split fallback — qualified by its session city, or quarantined as
homonim-bez-miasta.
Cmdlet surface¶
Thin Verb-Nerthus* wrappers; routes, parameters, envelopes, and capabilities are
in the API reference.
- Read & validate:
Get-NerthusEntity,Find-NerthusEntity,Get-NerthusEntityPath,Get-NerthusEntityHistory,Test-NerthusEntityIntegrity. - Write:
New-NerthusEntity,Set-NerthusEntity,Set-NerthusEntityName(rename: the heading changes, the old name stays as@alias, exact@należy_doreferences retarget, and an already-taken name is refused),Remove-NerthusEntity(soft-delete).New/Set-NerthusLocationEntityandNew/Set-NerthusMapEntityare type-specialized wrappers with their own dedicated routes and handlers. Gracz and Postać writes go through the dedicated player/character cmdlets — Players & characters. - Claims & reindex:
Test-NerthusClaims(lint the ledger),Invoke-NerthusReindex(apply hand edits now).
Every mutating route passes the single write gate and fires the
BeforeWrite/AfterWrite hooks — the API reference
owns the middleware chain.