WorldForge Studio

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

MethodPathNotes
GET/atlasLayer-plugin manifest (public, no auth)
GET/atlas/nodes?worldId=List nodes in a world
POST/atlas/nodesCreate a node
GET/atlas/nodes/:idFetch one
GET/atlas/nodes/:id/childrenChild nodes (literal segment, matched first)
PATCH/atlas/nodes/:idUpdate
DELETE/atlas/nodes/:id?cascade=Delete
POST/atlas/nodes/:id/regenerateRegenerate procgen children
POST/atlas/nodes/:id/design-generateAI interior design for a level-4 node (see below)
POST/atlas/nodes/:id/visibilitySet node visibility
DELETE/atlas/nodes/:id/visibilityClear it
GET/atlas/usageWhere 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.

StatuserrorMeaning
400invalid_generate_body (+ issues)Body failed validation
400not_interior (+ level)The node is not level 4
400no_layout / invalid_layout (+ issues)Node has no stored layout, or it no longer parses
400ai_not_available (+ reason)No BYOK key and no platform credential
400llm_generation_failed (+ message)The model call or its result failed
403forbidden / unresolved_caller, owner_onlyNo user behind the caller / not the owner
403plan_blocked (+ planTierId)The plan does not allow it
404not_foundNode or its world missing
429budget_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

MethodPathNotes
GET/atlas/edges?nodeId=Edges touching a node
GET/atlas/edges/by-world?worldId=All edges in a world
POST/atlas/edgesCreate — idempotent on (from, to, kind); refuses self-loops and cross-world edges
DELETE/atlas/edges/:idDelete

Layers

MethodPathNotes
GET/atlas/layers?worldId=List layers
POST/atlas/layersCreate
GET/atlas/layers/:idFetch
PATCH/atlas/layers/:idUpdate
DELETE/atlas/layers/:idDelete
POST/atlas/layers/:id/reorderReorder within the stack
POST/atlas/layers/:id/visibilityShow/hide

Regions (fog of war)

MethodPathNotes
GET/worlds/:id/regionsList (with reveal state)
POST/worlds/:id/regionsCreate
PATCH/worlds/:id/regions/:regionIdUpdate — layout_json must carry explicit width/height
DELETE/worlds/:id/regions/:regionIdDelete
POST/worlds/:id/regions/:regionId/revealReveal to an audience (GM/owner write-side)
POST/worlds/:id/regions/:regionId/hideHide 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).