04 — Atlas & regions
The map graph (/atlas: nodes, edges, layers, visibility) and fog-of-war regions (/worlds/:id/regions).
Source is identity: atlas rows are canonical (ADR-0008 D4) — the
json.atlas block inside the world document is gone from every read
path; never write it back.
Atlas nodes
| Method | Path | Notes |
|---|---|---|
GET | /atlas | Layer-plugin manifest (public, no auth) |
GET | /atlas/nodes?worldId= | List nodes in a world |
POST | /atlas/nodes | Create a node |
GET | /atlas/nodes/:id | Fetch one |
GET | /atlas/nodes/:id/children | Child nodes (literal segment, matched first) |
PATCH | /atlas/nodes/:id | Update |
DELETE | /atlas/nodes/:id?cascade= | Delete |
POST | /atlas/nodes/:id/regenerate | Regenerate procgen children |
POST | /atlas/nodes/:id/design-generate | AI interior design for a level-4 node (see below) |
POST | /atlas/nodes/:id/visibility | Set node visibility |
DELETE | /atlas/nodes/:id/visibility | Clear it |
GET | /atlas/usage | Where an asset is used |
POST /atlas/nodes/:id/design-generate
Body { prompt } (string, 1–2000 chars). World owner only (a GM or member
gets 403 { error: 'forbidden', reason: 'owner_only' }). It reads the node's
stored interior layout, asks an LLM (the owner's BYOK key first, the platform
chain as fallback, metered as purpose interior) and returns a proposed
layout; nothing is written to the atlas — the client previews it and saves
through the normal PATCH /atlas/nodes/:id.
200:
{ "intent": …, "layout": …, "notices": […], "applied": …, "skipped": …,
"budget": { "planTierId": "…", "remaining": … } }
(The shapes of intent, notices, applied and skipped come from
agent/ai-interior-design.ts; not spelled out here.) The daily AI budget is
checked before the call and only counted after a successful generation.
| Status | error | Meaning |
|---|---|---|
400 | invalid_generate_body (+ issues) | Body failed validation |
400 | not_interior (+ level) | The node is not level 4 |
400 | no_layout / invalid_layout (+ issues) | Node has no stored layout, or it no longer parses |
400 | ai_not_available (+ reason) | No BYOK key and no platform credential |
400 | llm_generation_failed (+ message) | The model call or its result failed |
403 | forbidden / unresolved_caller, owner_only | No user behind the caller / not the owner |
403 | plan_blocked (+ planTierId) | The plan does not allow it |
404 | not_found | Node or its world missing |
429 | budget_exhausted (+ planTierId) | Shared daily AI cap used up |
worldId scoping: list endpoints return 404 for a world you don't
own — existence is never leaked. Scopes: world:read / world:write.
Atlas edges
| Method | Path | Notes |
|---|---|---|
GET | /atlas/edges?nodeId= | Edges touching a node |
GET | /atlas/edges/by-world?worldId= | All edges in a world |
POST | /atlas/edges | Create — idempotent on (from, to, kind); refuses self-loops and cross-world edges |
DELETE | /atlas/edges/:id | Delete |
Layers
| Method | Path | Notes |
|---|---|---|
GET | /atlas/layers?worldId= | List layers |
POST | /atlas/layers | Create |
GET | /atlas/layers/:id | Fetch |
PATCH | /atlas/layers/:id | Update |
DELETE | /atlas/layers/:id | Delete |
POST | /atlas/layers/:id/reorder | Reorder within the stack |
POST | /atlas/layers/:id/visibility | Show/hide |
Regions (fog of war)
| Method | Path | Notes |
|---|---|---|
GET | /worlds/:id/regions | List (with reveal state) |
POST | /worlds/:id/regions | Create |
PATCH | /worlds/:id/regions/:regionId | Update — layout_json must carry explicit width/height |
DELETE | /worlds/:id/regions/:regionId | Delete |
POST | /worlds/:id/regions/:regionId/reveal | Reveal to an audience (GM/owner write-side) |
POST | /worlds/:id/regions/:regionId/hide | Hide again |
Reveal writes create audience-scoped rows (party, player:<userId>);
reads go through readWorldFor, which evaluates audiences for the
requesting role (Spec #11 Phase 4).