Skip to content

The location and map model

The locations subsystem turns the hand-authored Polish geography of repozytorium-fabularne (Świat gry/Werbin, Świat gry/Eder, Świat gry/Ithan, …) into two queryable structures. One is a hierarchical location tree: every Lokacja knows its parent via the @lokacja chain. The other is a multi-source connectivity graph: where you can actually walk, fly, or teleport between places. It also models the Margonem map layer — the Mapa entity type, keyed by the in-game @margonemid, projected back down onto the lore Lokacja it depicts. Session mentions land in the tree through a claim ledger the index itself carries — routing is steered by editing the file. The daemon owns all of this: the client only ever asks for an already-assembled tree or graph. For the plain-language introduction, start at Locations.

Concepts

  • Lokacja — an in-fiction place: a town (Thuzal), a district (Werbin/Miasto), a building (Rezydencja Tussal), a room. One of the seven entity types (see the entity model).
  • Mapa — a Margonem game map (an image on a grid of 32×32-pixel tiles) that depicts a Lokacja. Identified by its in-game numeric @margonemid (e.g. Werbin is map 9, Eder 33, Ithan 1). A Lokacja can be depicted by several Mapy (floors, event-copy Kopia instances); a Mapa depicts exactly one Lokacja.
  • World root / exterior — a top-level Świat gry/ directory (Ithan, Eder, Werbin, Tuzmer, …): the cities and region containers the world map is made of. Everything else is an interior — contained, directly or transitively, in a root. A Mapa may additionally carry the authored marker @typ_lokacji: zewnętrzna / wewnętrzna (exterior/interior game map).
  • Outerior — the daemon's computed world root for a place: walking the @lokacja containment chain upward until a top-level Świat gry/ root is reached. Rezydencja Tussal's Outerior is Thuzal.
  • Claim (roszczenie) — a - @forma_sesyjna: <forma> line on a nerthus.entities.md block. It claims that literal session form for the block: the normalizer routes every mention of the form there. Together the claim lines are the mention-routing plan — the plan IS the index, versioned with it in git.
  • Traversal graph — the merged "what connects to what" graph, assembled from entity tags, session metadata, and parsed game logs.

Polish is core here as everywhere: the tags, the @typ_lokacji values (zewnętrzna/wewnętrzna), the claim-lint findings (konflikt-roszczeń, roszczenie-bez-wzmianki), and the Margonem map-name suffixes the stripper understands (p.2, sala 1, piwnica, poziom: trudny) are Polish literals.

Data model — the Lokacja entity

A Lokacja is an entity block carrying the location tags. Hand-authored geography (the Świat gry/ directory tree) and session-derived interiors both land in the single generated nerthus.entities.md. Its ## Lokacja section union-merges the two sources into one block per name; the ## Mapa registry is the file's last section. The section is hand-editable: the importer merges per block, re-derives only its machine-owned tags, and preserves operator lines verbatim (the merge contract belongs to the entity model). The importer never rewrites the hand-authored Świat gry/**.md prose. Relevant tags:

Tag Meaning Example
@typ The entity type — always Lokacja @typ: Lokacja
@lokacja Parent location — the containment chain @lokacja: Thuzal
@outerior Computed world root (never authored by hand) @outerior: Thuzal (2026-07:)
@drzwi Door/passage to another Lokacja — authored, or log-derived with (auto) (see Doors) @drzwi: Thuzal
@koordynaty Tile position X, Y on the world map / parent Mapa grid @koordynaty: 125, 80
@alias Alternate names — indexed by the resolver @alias: Gildia Teologów
@forma_sesyjna A claim: routes every mention of this literal form to this block; (auto) = machine-appended @forma_sesyjna: Thuzal/Rezydencja Tussal (auto)
@status Aktywny / Nieaktywny / Usunięty; Niepewny marks quarantine @status: Aktywny
@plik Authoring location (the Świat gry/ directory) @plik: /Świat gry/Thuzal/Rezydencja Tussal/

The full closed tag schema, the link-value format, and the temporal range grammar live in the tag schema reference. The retired projection tags @mention_count, @scalono, and @nadpisanie are stripped at merge — attestation counts and merge lineage live in git history now. @duplikat (duplicate-source provenance, owned by the entity model) stays.

Hierarchical canonical name (the @lokacja chain)

A location's canonical name is its hierarchical path: each Lokacja's name, prefixed by its parent's, parent's parent's, and so on up the @lokacja chain until a world root. This is what makes a bare room name unambiguous repo-wide:

Rezydencja Tussal
    @lokacja: Thuzal
        (Thuzal is a top-level Świat gry/ directory → world root)
⇒ canonical path: Thuzal/Rezydencja Tussal

A cycle in the @lokacja chain is a hard integrity Error (Test-NerthusEntityIntegrity); the chain must terminate at a world root.

@koordynaty — the tile position

@koordynaty is X, Y, two integer tile coordinates (each tile is 32×32 px). It is authored geometry; exterior status comes from world-root membership. Like every value it is temporal (see the tag schema reference) — a town that moved on the map carries its history with per-value date ranges:

### Thuzal
- @typ: Lokacja
- @koordynaty: 125, 80
- @koordynaty: 130, 85 (2025-01:)

Exterior enrichment — @outerior (implemented), @isexterior/@inheritsfrom (deferred)

For every session-derived Lokacja the importer walks the containment chain (the parent edges known to the ground-truth sources, below) up to a world root and records the result as @outerior. That tag is the exterior a place resolves to, the disambiguator -Within <city> reads, and the anchor for homonym splits. It is computed enrichment, never an authored tag.

Two further enrichments are deferred; their tags are reserved in the closed schema. @isexterior would be a three-state classification from active @koordynaty and depicting-Mapa @typ_lokacji evidence. @inheritsfrom would let an interior inherit its effective world-map position from its Outerior (a room is "at" its town's coordinates). Revisit both with the map checkup below.

Data model — the Mapa entity

A Mapa carries the Margonem game-map identity and links to the Lokacja it depicts via @lokacja:

Tag Meaning Example
@typ The entity type — always Mapa @typ: Mapa
@margonemid In-game map id (the registry join key) @margonemid: 4120
@url Source Margonem image URL @url: https://micc.garmory-cdn.cloud/obrazki/npc/rezydencja_tussal.0.png
@url_nerthus Nerthus-hosted mirror of the image @url_nerthus: https://nerthus.pl/mapy/rezydencja_tussal.png
@url_grafika Alternate image (labelled, repeatable) @url_grafika: https://i.imgur.com/QVi9jT2.png "Mapa poglądowa"
@slug Stable Margonem image slug (from the URL) @slug: rezydencja_tussal
@wymiary Map dimensions in tiles W×H @wymiary: 96 x 96
@koordynaty Map's anchor tile on the world map @koordynaty: 125, 80
@typ_lokacji zewnętrzna / wewnętrzna (exterior/interior map) @typ_lokacji: wewnętrzna
@alias The stripped base name (when it differs) @alias: Rezydencja Tussal
@lokacja The Lokacja this map depicts @lokacja: Rezydencja Tussal
@drzwi Passage to another Mapa, log-derived with (auto) (see Doors) @drzwi: Thuzal (auto)

The importer emits @margonemid, @url, @slug, @alias, the @lokacja projection, and the log-derived @drzwi (auto) doors; @wymiary, @koordynaty, @typ_lokacji, @url_nerthus, and @url_grafika are the operator write surface (New-NerthusMapEntity / Set-NerthusMapEntity). A note that fits no tag is written as prose in the block (entity model). The link-value format (URL, optional "label", optional range) is specified in the tag schema reference. @margonemid is the registry's join key: the bootstrap snapshot ingest keeps the last entry per id, and the index read-back resolves a duplicate id last-loaded-wins.

The Mapa → Lokacja projection

The relation is many-to-one: a Mapa depicts exactly one Lokacja, and a Lokacja may be depicted by any number of Mapy. Margonem draws the world in map-sized pieces, and a lore place is routinely bigger than one piece — every floor of Rezydencja Tussal is its own map with its own @margonemid, and all of them are the one residence. Event copies of a city and floor-by-floor dungeons are the same shape. Nothing but the game's rendering identity lives on a Mapa: sessions, PU, transfers, containment, and the location tree all attach to the Lokacja, and the tree reads Lokacja blocks only, so a Mapa is never a node in the world hierarchy.

Every Mapa resolves through its @lokacja to exactly one Lokacja. This is what lets the daemon answer "which lore place is the player standing in?" from a raw Margonem map name in a game log, and what lets the traversal graph collapse map-level movement to location-level movement — the collapse is the many-to-one relation doing its job. The projection is a single exact (case-insensitive) match: if the map's stripped base name is one of the authored Świat gry/ location names, the map gets @lokacja: <base name>. That base name is what fans many maps into one place: interior maps with distinct ids (Rezydencja Tussal p.1 id 4120, Rezydencja Tussal p.2 id 4121) both strip to Rezydencja Tussal, so both project onto that Lokacja and both carry it as their base-name @alias; the resolver then finds them all under the building's name, and a -Type Lokacja query also admits Mapa owners (see Name resolution). An unmatched map carries no @lokacja until an operator sets it (Set-NerthusMapEntity).

The map registry and the name stripper

Margonem exposes the authoritative list of map ids via the game's map registry. Once the index carries a ## Mapa section, that section is that registry: every index rebuild reads it back via Read-NerthusIndexMaps. Only Mapa blocks carrying @margonemid and a currently covering @url join — the id is the registry identity and the URL its render, so a block missing either is not registry material (the fixpoint merge preserves it as disk-only state instead). Soft-deleted (Usunięty) blocks stay out the same way. Each record's URL is the block's current @url — the covering value under temporal resolution, checkup-fresh where the legacy snapshot had rotted — and a duplicate id resolves last-loaded-wins, mirroring the legacy reader's last-occurrence rule.

When the index has no ## Mapa section yet, the registry bootstraps from the legacy repo's cached snapshot .robot/res/maps.md instead:

## Mapy

- 2024-03-21 20:40 (UTC+01:00):
    - Id: 9; Nazwa: Werbin, Url: https://micc.garmory-cdn.cloud/obrazki/miasta/werbin.10.png
    - Id: 33; Nazwa: Eder, Url: https://micc.garmory-cdn.cloud/obrazki/miasta/eder.7.png
    - Id: 1; Nazwa: Ithan, Url: https://micc.garmory-cdn.cloud/obrazki/miasta/ithan.8.png
    - Id: 766; Nazwa: Ratusz Ithan p.1, Url: .../ratusz-ithan-p.1.png
    - Id: 4120; Nazwa: Rezydencja Tussal p.1, Url: .../rezydencja_tussal.0.png

The importer ingests each entry — line shape - Id: N; Nazwa: <name>, Url: <url> (semicolon after the id, comma before the URL) — into a Mapa entity (@margonemid: N, @url: <url>, name from Nazwa, @slug from the image filename).

Once the index carries ## Mapa, that section is the sole registry and the bootstrap snapshot is no longer read; retiring the still-present .robot/res/maps.md is the operator's own commit — the same pattern as the retired Gracze.md roster seed (see the roster model). A new map enters the registry via entity creation: a Mapa block carrying @margonemid + @url (New-NerthusMapEntity, or a hand-authored block), until a live-registry diff ships (deferred — see the checkup below).

To derive the base name a raw Margonem name projects from, the importer runs the Polish/Margonem name stripper (Get-NerthusStrippedMapName), an iterative pass that re-applies all patterns until the name is stable — so stacked suffixes strip fully:

Pattern stripped Example
Difficulty (poziom: <wartość>) — any value, anywhere in the name Lezysko Baraniego Kanoniera (poziom: trudny)Lezysko Baraniego Kanoniera
Trailing floor p.N Rezydencja Tussal p.2Rezydencja Tussal
Trailing sub-area s.N / - sala N / - Sala … Kopalnia Bandytów p.2 - sala 1Kopalnia Bandytów p.2Kopalnia Bandytów
Trailing piwnica Karczma - piwnicaKarczma
Trailing cardinal after a hyphen (- wsch., - zach., - płn., - płd., - północ, …) Las - wsch.Las
Kopia <name> - <event> instance wrapper Kopia Ithan - Urodziny 2023Ithan
Trailing separators -, --, --- Chata bandytów -Chata bandytów

These are proper Margonem nouns, not inflected Polish prose, so declension/fuzzy matching is deliberately not applied here (that belongs to Name resolution) — only literal suffix stripping, to avoid false positives between similarly named maps.

The deterministic location normalizer

The importer's location stage emits the enrichment into the ## Lokacja section of nerthus.entities.md, derived from the interiors named in session @Lokacje (see the session model). A place already skeletoned from the Świat gry/ directory (a dateless @lokacja parent plus the @plik directory path) receives the enrichment in that same block: the temporal (YYYY-MM:) @lokacja/@outerior lines, @alias resolver tokens, and machine-marked @forma_sesyjna claim lines. Enrichment-only places and quarantined (@status: Niepewny) blocks follow in the same section.

Only mentions with an exterior prefix feed the enrichment — a bare top-level mention is already a Świat gry location or a Mapa. Because the session parser records movement as raw strings, that set is noisy. It holds:

  • concatenated traversal paths (Mury/kwatera główna/Mury/zbrojownia);
  • floor/room subdivisions (Cytadela p.1, Pokój 202);
  • spelling/case/declension variants of one place (Karczma pod Złotą Wywerną / Karczmą pod Złotą Wywerną);
  • parent/child inversions (### Nithal carrying @lokacja: Izba chorych);
  • prose fragments that were never locations (rozwijanie prędkości na prostych odcinkach).

The importer folds that noise into a clean set before emitting. First it reads the claim ledger back from the previous index — the routing plan the operator may have edited. Unclaimed forms it reconciles against three ground-truth sources:

  1. The map registry — the canonical surface names + their stripped base names (the stripper above feeds this). This is the ## Mapa index section read back via Read-NerthusIndexMaps once it exists, else the legacy maps.md snapshot.
  2. The Świat gry hierarchy — the hand-authored Name → @lokacja tree and the world roots (the top-level Świat gry/ directories).
  3. The NPC containment treePostaci/NPC/<exterior>/<interior>/… directory nesting is a real (if partial — it cannot cover shared interiors) containment oracle. Postaci/NPC/Thuzal/Rezydencja Tussal/Ogród Tussala/ tells us Ogród Tussala ⊂ Rezydencja Tussal ⊂ Thuzal.

Nerthus.LocationNormalizer (the mechanical primitives — path split, sibling-fan detection, subdivision strip, prose detection, fold key) and Get-NerthusLocationNormalization (the orchestrator that knows the ground-truth sources and the claim ledger) run this pipeline per interior. Mentions accumulate in buckets — one working group per (interior, exterior) pair — so each city's mentions stay separable. Routing precedence per mention:

Stage Behavior Example
0 — Claim routing A claimed form routes to its block, no heuristics; a form claimed by two blocks quarantines as konflikt-roszczeń Willa Tussala → the Rezydencja Tussal block
1 — Prose / city-list quarantine A name or parent that reads like a sentence fragment, or a route of world roots, is quarantined dzień po zamachu na wiceburmistrz
2 — Path decomposition Split on / and pick the destination segment (rules below) Mury/kwatera główna/Mury/zbrojowniaMury
3 — Registry resolution exact-fold → subdivision-strip → declension (single-token names) → fuzzy (BK-tree, distance-bounded, direction-guarded) Karczmą pod Złotą WywernąKarczma pod Złotą Wywerną
4 — Parent / @outerior repair Ground-truth containment overrides the session-derived parent; a route's exterior is the final one (rules below) Ogród…: @lokacja: Rezydencja Tussal, @outerior: Thuzal
4b — Split-homonym fallback A name that is the shared bare @alias of ≥ 2 live blocks qualifies by session exterior; no resolvable exterior → quarantine homonim-bez-miasta Gildia Teologów in an Ithan session → Gildia Teologów (Ithan)
5 — Accumulation Variants collapse by folded bucket key; the highest-mention spelling wins the heading unless the bucket is pinned; every surface form is harvested (below) 27 + 11 + 2 + 1 mentions → one bucket
Quarantine Prose / city-list garbage and claim conflicts are kept as @status: Niepewny konflikt-roszczeń

Claim-routing rules: a mention entry can carry several literal forms — its leaf name plus each raw @Lokacje string. The normalizer checks them in a deterministic order (leaf first, then the raw forms sorted); the first claimed form routes the whole entry. A claim-routed mention takes the claiming block's own @lokacja as its parent — the heuristics below are never consulted.

Path decomposition rules: a segment repeated inside one path is the sibling-fan root — the hub the in-game walker kept returning to (Mury/kwatera główna/Mury/zbrojownia fans around Mury). A path of three or more distinct segments, at least three of which resolve to world roots, reads as a city list, not a route, and quarantines — even when the leaf is a non-root interior (Ithan/Karka-Han/Werbin/Torneg/Eder, but equally Ithan/Werbin/Eder/zbrojownia): a string of cities is a visited-cities list far more often than a genuine route, so the pass deliberately quarantines rather than guessing a parent from weak evidence. Otherwise the last segment that resolves is the destination.

Parent-repair rules: an inverted root has its parent dropped — a world root never hangs under its own child. @outerior is the world root reached by walking the repaired chain. For a travel route the true immediate exterior is the resolvable segment before the leaf, not the first hop the parser took: Ithan - Thuzal/Gildia Teologów places the guild in Thuzal, not Ithan. When the route-fix still guesses wrong, the operator moves that form's claim line to the right block (below).

Pinning rules: a claim-routed or split-qualified bucket is pinned — a higher-mention spelling never renames a block the operator addressed. A residual fuzzy pass after the merge folds leftover typo pairs neither of which is canonical; it never absorbs a pinned bucket, while an unclaimed typo bucket may still fold into one (typos gravitate to the claim).

Harvesting every session name — claims and @alias

The pass harvests every distinct literal string any session used for a place — the leaf name plus each full @Lokacje raw form (which retains the exterior/route prefix the parser drops). Every harvested form becomes a claim line on the emitted block, machine-marked: - @forma_sesyjna: <forma> (auto). The next run reads those lines back as the routing ledger, so routing is stable run-over-run.

A clean alternative name (no /) — a spelling, case, or declension variant — is additionally projected as @alias and indexed by the name resolver, so a later query that writes Karczmą pod Złotą Wywerną resolves straight to the canonical Karczma pod Złotą Wywerną. A route/path form (Nekropolia Karka-han/Karka-han/Karczma pod Złotą Wywerną) is never indexed as a name — feeding it to the resolver would pollute the index. @alias is never consulted for routing; only claim lines route. The exterior segments a route form records are real adjacency evidence and feed the connectivity graph.

Two Polish-specific guards keep the fuzzy stages honest:

  • Directional conflict — opposing cardinal markers never merge: Izba chorych płn. (północna, north) and Izba chorych płd. (południowa, south) stay two entries.
  • Parent disagreement — two look-alike names whose resolved parents differ are not merged: Mury (⊂ Werbin) stays distinct from Mury Nithal (⊂ Nithal).

The pass is fully deterministic and idempotent — sorted output, fixed thresholds, no timestamps, no LLM. Re-running the importer against unchanged sources and an unchanged index reproduces nerthus.entities.md byte-for-byte: the block merge is a fixpoint. Disk claim lines survive the merge verbatim, and the machine appends (auto) claims only for forms new this run. Names the registries cannot anchor (genuine session-only interiors such as Dom Glieve or Gabinet Przemian Estetycznych) are kept as-is under their resolved parent; only genuinely-ambiguous entries are flagged Niepewny for review.

Operator edits — what the deterministic pass can't decide

Same-named mentions may be homonyms: sessions attest Gildia Teologów in Thuzal and Ithan — two distinct guild-halls sharing one name — while the deterministic pass keys by folded name and would collapse them. Whether two mentions are the same place is world knowledge, so it is the operator's call, made by editing claim lines in nerthus.entities.md. The claim ledger, authorship marker, gesture table (adopt / reject / re-route / merge / split / rename), lint findings, and the reindex workflow are owned by the entity model; the location-specific rules live here:

  • A split keys by the session-stated city — the ground truth may know only one chapter (the registry knows only Thuzal's), so keying by it would wrongly pull the Ithan mentions to Thuzal. Move each claim line where its own prefix points. Once ≥ 2 live blocks share the bare @alias, stage 4b qualifies a new bare mention by its session exterior instead of resurrecting a bare block.
  • Binding fixes the destination itself — leaf and city derivation are bypassed. A custom lore name that denotes a known place — sessions write Willa Tussala, the world knows Rezydencja Tussal — is one claim line on the Rezydencja Tussal block. A private instance carved out of a shared interior is a new hand-authored block that claims its literal form. A claim on a form no session has used yet is pre-registration: it routes the form on its first appearance.
  • Quarantined garbage stays in the file as @status: Niepewny blocks — the pass keeps every mention. Adopting a quarantined form is a claim on the right block; the machine's quarantine ghost then disappears on the next reindex, while a hand-authored Niepewny block is preserved.

Test-NerthusClaims lints the ledger read-only, and Invoke-NerthusReindex (a gated write behind the Council-level location.override capability) applies hand edits without a daemon restart — it re-runs the full import so moved or new claims re-route their mentions, and its response embeds the lint so a conflict never takes effect silently.

How a split affects later resolution

A split makes the bare name ambiguous on purposeGildia Teologów now indexes two owners (one per city, via the shared @alias). Resolution disambiguates by exterior context:

Resolve-NerthusName 'Gildia Teologów' -Type Lokacja                 # Ambiguous=True, 2 candidates
Resolve-NerthusName 'Gildia Teologów' -Type Lokacja -Within Thuzal  # -> Gildia Teologów (Thuzal)
Resolve-NerthusName 'Gildia Teologów' -Type Lokacja -Within Ithan   # -> Gildia Teologów (Ithan)

-Within <city> filters the homonym's owners by exterior context — the mechanics (including the ambiguity report without -Within) are owned by Name resolution. The session integrator passes the session's current exterior as -Within, so "the guild" inside a Thuzal session resolves to the Thuzal hall.

Temporal scope — a same-named place appearing in a new city later

Each session-derived Lokacja carries an observation window from the session dates that mention it, written as an open-ended temporal scope on its containment (temporal value semantics: tag schema reference):

- @lokacja:  Thuzal (2026-07:)     # first seen 2026-07; open-ended = presumed ongoing
- @outerior: Thuzal (2026-07:)

Open-ended is deliberate — absence of later mentions is not evidence of closure, so a place stays resolvable "now", while -ActiveOn before that month correctly excludes it (it didn't exist yet). A new Gildia Teologów opening in Eder in 2027 needs no operator edit: the bare name is already a split family's shared @alias, so the normalizer emits Gildia Teologów (Eder) keyed by the session city, and the scopes keep the places apart in time:

Resolve-NerthusName 'Gildia Teologów' -Type Lokacja -Within Thuzal -ActiveOn 2026-07-01  # -> (Thuzal)
Resolve-NerthusName 'Gildia Teologów' -Type Lokacja -Within Eder   -ActiveOn 2027-06-01  # -> (Eder)

-Within reads the containment as of -ActiveOn, so space and time compose for free. A guild that genuinely closes when another opens is an operator decision — a hand edit closing the containment range; the importer can only observe first appearance from sessions.

Doors — log-derived @drzwi on Mapa and Lokacja

A door is a physical passage between two places, carried as an @drzwi line on both endpoint blocks — every door is bidirectional. Operators author doors by hand; the importer derives the rest from game logs at every import/reindex, marking its lines (auto).

Map doors. A basic-format transcript's location headers are raw Margonem map names. The importer resolves each header against the map registry by a single exact fold match on the map's RawName or stripped base name — no fuzzy, and a header two distinct maps share (the bare Baraki of Baraki p.1/p.2) resolves to nothing. Two consecutive resolved headers attest a passage: the pair lands as - @drzwi: <other map> (auto) on both Mapa blocks. Rich log-saver transcripts carry no location headers (see the log model), so coverage grows as basic transcripts are fetched into the committed nerthus.logs/ archive — reindex after a fetch sweep picks them up.

Location doors. Each map-door pair projects through its endpoints' @lokacja links (operator-set links included — the importer reads them back from the previous index and its overflow siblings, the claim-ledger pattern; a range-closed @lokacja never counts as the current link): maps of two different locations put - @drzwi: <other location> (auto) on both Lokacja blocks. A pair the hierarchy already implies (a containment edge) or an existing door line already covers — active or range-closed, on either endpoint — is never minted. One limitation: the auto line lands only on machine-emitted blocks, so an endpoint that exists solely as a hand-authored disk block gets no line — author its reciprocal door by hand.

Operator gestures. A door is one pair, carried as two lines — gestures edit both endpoint blocks (the merge dedups per bare value, marker and range folded off):

  • Adopt a machine door: delete the (auto) marker on both lines.
  • Reject one: close the range on both lines — - @drzwi: Ithan (auto) (:2026-07) — and the rejection survives every re-import. Closing only one side leaves the other side's line active (the pair still counts as covered, so nothing is re-minted — but the graph keeps the half-door). Deleting both lines instead resurrects the pair on the next reindex, since the log evidence persists.

The connectivity graphs (two views)

Two read routes serve connectivity, both directed:

Get-NerthusLocationGraph — the structural graph, straight from entity tags:

Edge type Built from Meaning
Containment @lokacja chain parent → child (Thuzal → Rezydencja Tussal)
Door @drzwi tags explicit authored passage between two Lokacje

Get-NerthusMapTraversalGraph — the observed movement graph, merged from two sources of location→location transitions:

  • the ordered @Lokacje entries of each session — each entry contributes its interior part, after the parser normalizes A -> B arrows and - separators into / segments;
  • each archived game-log transcript's resolved location-segment chain — consecutive duplicates collapsed, one chain per distinct log URL, so sessions sharing a log never double-count.

Every edge carries a Weight and an Origins set (session / log) recording who attests it, so a move seen in both a route and a transcript is one strong edge. Edge typing:

Edge type Meaning
Movement a move between locations that ARE structurally adjacent (a containment edge or a @drzwi exists, in either direction)
Teleport a move between structurally non-adjacent locations (Plany Astralne, or simply an undocumented passage) — the cue that a @drzwi is missing. The log-derived doors close most of these on reindex; what logs can't see stays for the Council to confirm via Set-NerthusTraversalEntities

Margonem map checkup — the CDN version sweep

Margonem republishes a repainted map under the same base name with a bumped version suffix: werbin.10.png becomes werbin.11.png, and a never-repainted eder.png becomes eder.2.png. The checkup (POST /workflows/map-checkup, Invoke-NerthusMapCheckup) HEAD-probes the next version of every active Mapa's @url; on a hit it walks upward to the newest version, closes the active @url line at the current month, and appends the new value marked (auto)@url is scalar-historical (see the tag schema reference), so the retired render stays queryable — then refreshes @slug from the new filename:

- @url: https://micc.garmory-cdn.cloud/obrazki/npc/rezydencja_tussal.0.png (:2026-07)
- @url: https://micc.garmory-cdn.cloud/obrazki/npc/rezydencja_tussal.1.png (auto) (2026-07:)

A current @url that itself answers 404 with no successor is reported as failed[].reason = 'gone' — report-only, the cue of a retired or renamed map; the checkup never touches @status. Politeness is config (margonem.map_checkup, configuration reference): a host allow-list, a per-request delay, a probe-step cap, and a failure budget — a 404 is the expected "no update" answer and never spends the budget. -WhatIf (?dryRun) reports without writing; maxMaps/cursor slice a batched sweep. Scheduling — daily on the VM serve loop, per scheduled pipeline in CI — is owned by Sync & the VM and the pipelines guide.

The rest of the designed checkup — re-fetching the live Margonem map registry and diffing it against the index (new ids, renames, stale maps) — stays deferred. The ## Mapa index section is the source of Mapa identity truth (see the map registry); a new map enters via entity creation until the live diff ships.

Cmdlet surface

Reads: Find-NerthusLocation, Get-NerthusLocation, Get-NerthusLocationGraph, Get-NerthusMapTraversalGraph, Test-NerthusClaims. Writes: New-NerthusLocationEntity, Set-NerthusLocationEntity, New-NerthusMapEntity, Set-NerthusMapEntity, Set-NerthusTraversalEntities, Invoke-NerthusReindex, Invoke-NerthusMapCheckup. Routes, parameters, envelopes, and capabilities: API reference. Resolution queries (Resolve-NerthusName -Within -ActiveOn) are owned by Name resolution.

Examples

Resolve and inspect an interior location:

Get-NerthusLocation 'Rezydencja Tussal'
# Name          : Rezydencja Tussal
# Type          : Lokacja
# Location      : Thuzal          (parent)
# Outerior      : Thuzal
# Doors         : { Thuzal }
# Children      : { }
# SessionForms  : { Thuzal/Rezydencja Tussal (auto) }

List the interior locations of Thuzal:

Find-NerthusLocation -Parent 'Thuzal'
Find-NerthusLocation -Outerior 'Thuzal'   # transitively, by world root

Register a Margonem map by hand (id + geometry the importer doesn't know):

New-NerthusMapEntity -Name 'Rezydencja Tussal' -MargonemId 4120 `
    -Url 'https://micc.garmory-cdn.cloud/obrazki/npc/rezydencja_tussal.0.png' `
    -Wymiary '48 x 48' -Typ 'wewnętrzna' -Lokacja 'Rezydencja Tussal'

Split a homonym by hand — the stray route form pinned by its claim line:

### Gildia Teologów (Thuzal)
- @typ: Lokacja
- @lokacja: Thuzal
- @alias: Gildia Teologów
- @forma_sesyjna: Thuzal/Gildia Teologów
- @forma_sesyjna: Ithan - Thuzal/Gildia Teologów
- @status: Aktywny

### Gildia Teologów (Ithan)
- @typ: Lokacja
- @lokacja: Ithan
- @alias: Gildia Teologów
- @forma_sesyjna: Ithan/Gildia Teologów
- @status: Aktywny
Test-NerthusClaims        # findingCount 0 — the ledger is consistent
Invoke-NerthusReindex     # re-route the mentions; the response embeds the lint

Find missing doors from observed traversals:

(Get-NerthusMapTraversalGraph).Edges | Where-Object Type -eq 'Teleport'
# Ithan → Thuzal (Weight 4, Origins: session, log) — candidate @drzwi via Plany Astralne

Decided and deferred

  • Override files and a suggest/confirm two-step — retired: claim lines in the index replaced the separate override store; hand-edit governance is git/MR review, and the reindex response embeds the claim lint.
  • @url_nerthus mirror policy — decided in scope as a tag; when the daemon populates it (on import vs. on a fetch job) is deferred to the log-fetch pipeline.
  • Auto-applying inferred doors — the importer writes transcript-attested doors as (auto) lines at every reindex — re-derivable, rejected by a range-close. A Teleport edge attested only by session routes (no transcript) still promotes manually via Set-NerthusTraversalEntities.
  • @zawiera vs @lokacja — resolved: containment has exactly one direction, the child's @lokacja; the parent-side @zawiera convenience is retired from the closed schema.
  • @margonemid uniqueness — decided not validated: both registry sources key by id and keep the last entry per id (snapshot ingest and index read-back alike).
  • @isexterior / @inheritsfrom — deferred enrichments, reserved in the closed schema; revisit with the map checkup.

See also