Skip to content

Settlement

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

The two workflows that catch the repository up to itself. Settle derives every pending routine operation from committed state — drifted distributed sessions, unapplied @Transfer sessions, and the month's PU batch — and applies them in one run. Map checkup sweeps the Mapa catalogue, HEAD-probing the Margonem CDN for a newer render of each map. Both read the canonical settlement model; the examples use the fixture month 2026-07, whose only PU-bearing session is 2026-07-01, Eraster rozmawia z Tussalem, Anward.

Both capabilities — workflow.settle and workflow.map_checkup — sit in no role bundle: settle spans distribution, currency, and PU, so it resolves only through admin.all. A token holding the individual write capabilities is still refused. Every example runs against a live daemon — see how the reference is tested.

Routes

Method Path Cmdlet Cap Write
POST /workflows/settle Invoke-NerthusSettle workflow.settle
POST /workflows/map-checkup Invoke-NerthusMapCheckup workflow.map_checkup

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.

Both routes always answer 200 with a per-item report (ok flags any failure) and honor ?dryRun=true. A full map sweep runs for minutes and answers only at the end, so Invoke-NerthusMapCheckup -ShowProgress renders a done/total bar from the route's mapcheckup:progress ticks (Server-Sent Events). Neither workflow.settle nor workflow.map_checkup sits in a role bundle — each resolves only through admin.all. What counts as pending: Settlement; the CDN version sweep: Locations; the scheduled jobs: Set up pipelines.

Preview a settlement without applying

POST /v1/api/workflows/settle with ?dryRun=true returns the full report with applied: false and writes nothing — distribution, transfers, and PU are each derived and previewed. month is YYYY-MM; omit it and the previous calendar month is used.

POST /v1/api/workflows/settle?dryRun=true
Content-Type: application/json
Authorization: Bearer <token>

{ "month": "2026-07" }
await fetch("https://evocation.nerthus.pl/v1/api/workflows/settle?dryRun=true", {
  method: "POST",
  headers: { "Content-Type": "application/json", "Authorization": `Bearer ${token}` },
  body: JSON.stringify({ month: "2026-07" }),
}).then((r) => r.json());

Response 200 (trimmed — every item carries, ok turns false on any failure):

{
  "month": "2026-07", "applied": false, "ok": true,
  "distribution": { "pending": 0, "items": [] },
  "transfers": { "pending": 0, "settled": 0, "items": [] },
  "pu": { "month": "2026-07", "status": "pending", "counted": 1, "echoed": 0 }
}

Rejected: malformed month

A month that is not YYYY-MM is 400, before any work is derived.

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

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

Response 400:

{ "error": "MonthInvalid", "detail": "month must be YYYY-MM, got 'July'" }

Rejected: the piecemeal capabilities are not enough

A token holding session.distribute, currency.write, and pu.award still cannot settle — the capability gate demands workflow.settle, which only admin.all carries. Here the bearer is a token minted with exactly those three write capabilities.

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

{ "month": "2026-07" }
// `scoped` holds only session.distribute + currency.write + pu.award — no workflow.settle.
await fetch("https://evocation.nerthus.pl/v1/api/workflows/settle", {
  method: "POST",
  headers: { "Content-Type": "application/json", "Authorization": `Bearer ${scoped}` },
  body: JSON.stringify({ month: "2026-07" }),
}).then((r) => r.json());

Response 403:

{ "error": "Forbidden", "required": "workflow.settle" }

Run the map checkup

POST /v1/api/workflows/map-checkup sweeps the active Mapa entities in a deterministic order, probing the CDN for a newer version of each @url. Because a real sweep reaches the network, the batch is resumable: maxMaps bounds one batch and cursor is its resume point (0 once the catalogue is exhausted), and maxMillis bounds the wall-clock time one call may hold the daemon's thread. This call resumes past the tail of the catalogue, so the batch is empty — no probe is issued, scanned is 0, and cursor rewinds to 0 to signal the sweep is complete.

The size stage: ?dimensions=only|skip|both

A repaint changes what a map's render looks like, and it can change how big it is. The sweep moves @url and refreshes @slug; the size stage measures the render behind it, with one 34-byte Range read on the connection the version check just opened (Measuring a map).

Value Runs
both (default) version probes, then a size read of every map whose @url just moved or that carries no @wymiary at all
skip version probes only
only size reads only, no version walk

both is the default because the alternative is silent: without it, a repaint that resized a map leaves a stale W x H on the block that nothing ever re-checks — the dimensions route skips any map that already carries one — and four subsystems read that wrong number as measured. The gate exists so the halves stay separable for CI and for debugging, not so the repair can be forgotten.

both is also what measures a catalogue that has never been measured. That pass belongs here, on the fleet tick, where it already has a cursor that survives restarts and a tick_budget_ms — not in a browser, which cannot hold a sweep that runs for minutes.

The report carries the size stage's work in measured[], beside the version stage's updated[].

POST /v1/api/workflows/map-checkup?dryRun=true
Content-Type: application/json
Authorization: Bearer <token>

{ "maxMaps": 1, "cursor": 999 }
await fetch("https://evocation.nerthus.pl/v1/api/workflows/map-checkup?dryRun=true", {
  method: "POST",
  headers: { "Content-Type": "application/json", "Authorization": `Bearer ${token}` },
  body: JSON.stringify({ maxMaps: 1, cursor: 999 }),
}).then((r) => r.json());

Response 200 (updated and failed carry any repainted or unreachable maps):

{ "applied": false, "total": 12, "scanned": 0, "skipped": 0, "updated": [], "measured": [], "failed": [], "budgetExhausted": false, "cursor": 0 }