Skip to content

Name resolution

Nerthus.Core (until cutover). This page describes the frozen system that runs today and is deleted at cutover. Replaced by: not yet written.

Every name in the lore is Polish and inflected — a session note writes Erastera, Lordowi Tussalowi, Opatowi Perrinowi, and the resolver maps each surface form back to the canonical entity. It runs a seven-stage pipeline (exact → computed alias → declension → consonant alternation → title strip → annotated lemma → fuzzy) over an in-memory name index built from the entity model. This page covers resolving one name, resolving a batch, inspecting a single index token, and rebuilding the index. The examples use the canonical session fixtures: the characters Eraster and Lord Tussal, and the NPC Opat Perrin.

Resolution reads need entity.read; rebuilding the index needs admin.index. Every example runs against a live daemon — see how the reference is tested. The resolution model is specified in name resolution.

Routes

Method Path Cmdlet Cap Write
POST /resolve Resolve-NerthusName entity.read
POST /resolve/batch Resolve-NerthusName (array input) entity.read
GET /name-index/lookup/{token} — (raw index diagnostic; not a resolver) entity.read
POST /name-index/rebuild — (in-memory rebuild) admin.index

Cap is the required capability ( = public, no token); Write () marks routes that pass the write gate; a in Cmdlet is reached directly, no wrapper. Paths are relative to /v1/api; the cross-cutting contract — middleware, envelopes, status codes — is on the API reference index.

POST /resolve body: name (or queries[] on /resolve/batch), typ (alias type), within, activeOn, noFuzzy, topN, maxDistance — resolver semantics in Name resolution.

Resolve one name

POST /v1/api/resolve takes { name } and returns the winning owner: { name, typ, plik, stage, confidence, ambiguous, candidates }. stage records which pipeline step matched (exact here); ambiguous is true only when a homonymous token has more than one owner. An unresolvable name returns 200 with a null body.

POST /v1/api/resolve
Content-Type: application/json
Authorization: Bearer <token>

{ "name": "Eraster" }
await fetch("https://evocation.nerthus.pl/v1/api/resolve", {
  method: "POST",
  headers: { "Content-Type": "application/json", "Authorization": `Bearer ${token}` },
  body: JSON.stringify({ name: "Eraster" }),
}).then((r) => r.json());

Response 200:

{ "name": "Eraster", "typ": "Postać", "plik": null, "stage": "exact",
  "confidence": 1.0, "ambiguous": false, "candidates": ["Eraster"] }

Resolve a multi-word name

The full canonical form resolves at the exact stage too — the whole string is a priority-1 token in the index.

POST /v1/api/resolve
Content-Type: application/json
Authorization: Bearer <token>

{ "name": "Lord Tussal" }
await fetch("https://evocation.nerthus.pl/v1/api/resolve", {
  method: "POST",
  headers: { "Content-Type": "application/json", "Authorization": `Bearer ${token}` },
  body: JSON.stringify({ name: "Lord Tussal" }),
}).then((r) => r.json());

Response 200:

{ "name": "Lord Tussal", "typ": "Postać", "stage": "exact",
  "confidence": 1.0, "ambiguous": false, "candidates": ["Lord Tussal"] }

Resolve a batch

POST /v1/api/resolve/batch takes { queries } and returns the { count, items } envelope, one { token, resolved, typ, stage } per query in request order. A miss keeps its token with resolved: null and stage: "miss". Here all three resolve.

POST /v1/api/resolve/batch
Content-Type: application/json
Authorization: Bearer <token>

{ "queries": ["Eraster", "Lord Tussal", "Opat Perrin"] }
await fetch("https://evocation.nerthus.pl/v1/api/resolve/batch", {
  method: "POST",
  headers: { "Content-Type": "application/json", "Authorization": `Bearer ${token}` },
  body: JSON.stringify({ queries: ["Eraster", "Lord Tussal", "Opat Perrin"] }),
}).then((r) => r.json());

Response 200:

{ "count": 3, "items": [
  { "token": "Eraster", "resolved": "Eraster", "typ": "Postać", "stage": "exact" },
  { "token": "Lord Tussal", "resolved": "Lord Tussal", "typ": "Postać", "stage": "exact" },
  { "token": "Opat Perrin", "resolved": "Opat Perrin", "typ": "NPC", "stage": "exact" }
] }

Inspect an index token

GET /v1/api/name-index/lookup/{token} reads one raw token straight out of the index, without running the pipeline — it returns { source, ambiguous, owners }. The index is case-insensitive, so the lowercase eraster finds the Eraster token.

GET /v1/api/name-index/lookup/eraster
Authorization: Bearer <token>
await fetch("https://evocation.nerthus.pl/v1/api/name-index/lookup/eraster", {
  headers: { "Authorization": `Bearer ${token}` },
}).then((r) => r.json());

Response 200:

{ "source": "Eraster", "ambiguous": false,
  "owners": [{ "name": "Eraster", "type": "Postać", "priority": 1 }] }

Rejected: token not indexed

A token that is not a surface form of any entity is 404 — the lookup is a direct index probe, not a fuzzy search.

GET /v1/api/name-index/lookup/nieistniejacy
Authorization: Bearer <token>
await fetch("https://evocation.nerthus.pl/v1/api/name-index/lookup/nieistniejacy", {
  headers: { "Authorization": `Bearer ${token}` },
}).then((r) => r.json());

Response 404:

{ "error": "TokenNotIndexed", "detail": "token not indexed" }

What the lookup is for, and what it is not for

It answers which blocks own this literal token, and nothing else. It never runs the resolver, so every inflected form of an indexed name is a 404 — including forms POST /resolve finds without difficulty. Driven against one entity that the index holds unambiguously enough to be a fair test:

request answer
GET /name-index/lookup/vanda 200Vanda, ambiguous, owned by a Postać and a Gracz
GET /name-index/lookup/vandy 404 — the genitive
GET /name-index/lookup/vando 404 — the vocative
GET /name-index/lookup/vandzia 404 — the diminutive

POST /resolve finds all three: two by declension and one — wrongly — by fuzzy, onto a place. So a 404 here is not evidence that a form is unresolvable, and a caller that reads it that way will conclude the opposite of the truth.

Use it to debug the index: to see whether a token was written down at all, which blocks claim it, and whether they collide. Use POST /resolve for every question about a form a person actually wrote, because that is the route that runs the pipeline. Widening the lookup to run the pipeline would destroy the one question it answers cleanly, and would duplicate /resolve.

Rebuild the index

POST /v1/api/name-index/rebuild re-parses the entity model and rebuilds the index in place, returning { rebuiltAt, buildMs, indexStats } — where indexStats carries the token and stem counts. It needs admin.index.

POST /v1/api/name-index/rebuild
Authorization: Bearer <token>
await fetch("https://evocation.nerthus.pl/v1/api/name-index/rebuild", {
  method: "POST",
  headers: { "Authorization": `Bearer ${token}` },
}).then((r) => r.json());

Response 200:

{ "rebuiltAt": "2026-07-01T12:00:00.0000000Z", "buildMs": 8,
  "indexStats": { "tokenCount": 61, "stemCount": 54 } }