What I Built
"Is this venue step-free?" can go dangerously wrong. Ask a chatbot and it will happily turn a venue's access page into a confident yes, even when a visitor reported a raised threshold last week, or a trolley is parked across the only route, or your wheelchair is 1300 mm wide and the studio door is 1200 mm.
The Access Desk answers that question for one person on one route, and it says what it doesn't know:
- A deterministic check gives the verdict. It walks the stored route from the entrance to your destination and screens every doorway and obstacle against your clear width. The answer is CLEAR, BLOCKED or UNKNOWN, with reasons and the evidence behind each one. Evidence only counts if its review status lets it: a verified closure blocks, an unverified one makes the answer UNKNOWN, and an expired one is ignored.
- An evidence agent explains the sources. It reads a Sanity Context Knowledge Base built from the same structured documents, and it sets out what the venue says, what visitors say and where they disagree, with citations. It can never change the verdict: the computed result is passed in as authoritative, and the UI keeps the two visibly separate.
- People approve physical changes. Moving an obstacle is previewed first, showing the before → after verdict for every room. Nothing is published until a reviewer approves it, and the approval becomes a new dated source.
The demo venue, Harbor Arts Centre, is fictional. Every report in it is a labelled synthetic fixture.
- Live app: https://spatialize-pink.vercel.app/#studio (no login; the API runs on a free Render instance, so the first request may take a few seconds to wake it)
- Hosted Studio: https://spatialize-tubqyqod.sanity.studio/
Try it:
- Learning studio at 800 mm → CLEAR. Change the width to 1300 mm → BLOCKED by the 1200 mm studio doorway, which the 3D twin rings.
- Gallery one → BLOCKED, because a trolley sits in the route corridor. Open Rehearse an obstacle move and preview the move to X 13.5, Z 5.3: Gallery goes BLOCKED → CLEAR in the preview, with nothing published yet. Submit it and approve it with a reason. The access version goes from 1 to 2 and survives a reload.
- Quiet room, with the Evidence agent on: "Is the quiet room step-free?" The verdict, UNKNOWN, appears instantly. Then the agent's answer arrives. This is a real one:
The quiet room's step-free status is UNKNOWN for the stored route. The evidence conflicts:
Harbor Arts access guide (observed 2026-09-18) states that the quiet-room doorway is step-free, but this is a synthetic fixture for the fictional Harbor Arts demo venue, not a real observation.
Visitor report: quiet-room threshold (observed 2026-10-02) reports a raised threshold; the venue disputes it, and no follow-up measurement exists.
The stored route also has unconfirmed doorway width and connectivity. Thus, neither claim is settled. This verdict screens the stored route only, not universal accessibility; it does not assess turning space, slope, headroom, or unrecorded obstacles.
To produce it, the agent made one initial_context call, then one knowledge_base_read over disputed_reports, doorway_access, venue_guide and visitor_reports. Open Context retrieval record in the app to see every MCP call with its arguments and what came back.
Code
N-45div
/
Spatialize
A building that answers your agent from geometry - 14 WebMCP tools for venue accessibility, where every agent write is a validated proposal a person approves.
Spatialize
A building that answers from its geometry and its evidence, and says so when it doesn't know.
Spatialize turns a flat floor plan into a validated 3D spatial twin. On top of that twin it checks a route against one person's clear width, keeps every access claim sourced and reviewable in Sanity, and lets agents ask questions and propose changes. Agents work through fourteen WebMCP tools in the browser, an evidence agent that reads a Sanity Context Knowledge Base, and a voice assistant. None of them can publish anything every change is checked by a deterministic gate and decided by a person, and every verdict is computed, never written by a model.
Live app: https://spatialize-pink.vercel.app/#studio API: https://spatialize.onrender.com (free instance; the first request after an idle spell wakes it, which takes a few seconds) Docs: ACCESS_DESK.md (evidence, Sanity, the evidence agent) · EVALS.md (every claim, measured) · ARCHITECTURE.md
…
What was built for this challenge: the Access Desk, from its first commit (f21d0f6, 3 October 2026) onward. That covers the Sanity schema and Studio, the evidence model and corridor check, reviewed obstacle moves, the Knowledge Base and the Context agent.
Prior work, credited: it extends my existing open-source Spatialize app. Its floor-plan extraction, 3D twin, WebMCP tools and geometry review queue predate the challenge. Alza's obstruction checking and ArchMorph's synchronized human/agent model inspired parts of the design; no code from either was used.
Stack: React + Three.js, FastAPI, Sanity Content Lake + Studio, Sanity Context, and the OpenAI Responses API (gpt-5.6-luna) with remote MCP. ACCESS_DESK.md covers the architecture and how to reproduce everything.
How I Used Sanity
Content Lake holds the evidence as data, not pages. The schema (sanity/schema.ts) models:
-
accessVenueandspatialEntity: the rooms, doors and landmarks a claim can be about. -
accessSource: who said it, when, and whether it's synthetic. -
accessClaim: astep-freeorclear-width-mmfact about one entity, citing one source, with a review status ofverified,unverifiedordisputed. -
operationalNotice: a closure with a start and an end. -
accessObstacle: a footprint in metres. -
accessState: a read-only, versioned publication holding the published layout and every review decision, approved or declined.
The deterministic check queries all of this with GROQ on every request, so an edit in Studio changes the next answer. Publication writes are guarded with ifRevisionID, so two reviewers can't both publish over the same version.
The Knowledge Base is a GROQ projection over that structure. I didn't point Context at prose. Each source arrives with the claims, notices and obstacles that cite it, and their status:
*[_type == "accessSource" && venue._ref == "harbor-arts-ground" && !string::startsWith(_id, "review-")]{
_id, title, publisher, url, observedAt, synthetic, body,
"claims": *[_type == "accessClaim" && source._ref == ^._id]{
property, stepFree, widthMm, status, "about": entity->title},
"notices": *[_type == "operationalNotice" && source._ref == ^._id]{
title, startsAt, endsAt, status, "about": entity->title},
"obstacles": *[_type == "accessObstacle" && source._ref == ^._id]{label, roomId, position, width, depth, verified}
}
So the Knowledge Base knows the threshold report is disputed and the gallery closure ended on 21 September; an index of the text alone wouldn't. Review records typed by visitors of the public app are filtered out, so nothing they write reaches the agent. The build produced seven entries: access barriers, disputed reports, doorway access, facilities notices, review status, venue guide and visitor reports. The venue is deliberately small, one ground floor, so every entry can be checked by hand.
Context found the conflict on its own. The first build filed a critical conflict: the venue guide says the quiet-room doorway is step-free, and the visitor report says it has a raised threshold. The app's position is that this dispute stays open until someone measures, so I didn't pick a side. I added a standing Knowledge Base instruction to present both claims with source, date and status and never settle them, rebuilt the affected entries, and dismissed the conflict under that instruction. The decision carries across future builds. The deterministic check independently reaches the same answer from the structured claims: UNKNOWN.
The agent is an OpenAI Responses call with the Context MCP endpoint (spatialize-access-evidence) as a remote MCP tool, authenticated with an organization token that has Context Viewer access. It calls initial_context for the outline, then knowledge_base_read for the entries that matter. It only counts as "connected" if a real knowledge_base_read succeeded and it wrote an answer; an outline call alone doesn't count. The endpoint is Knowledge Base-only on purpose. The deterministic check already reads Content Lake directly with GROQ, so the agent gets the distilled, conflict-checked entries rather than raw documents.
Measured, not just demoed. scripts/agent-eval.py asks six questions four times each against the live Knowledge Base:
- In 24 of 24 runs the agent made a successful
knowledge_base_readand stated the computed verdict. - In 0 of 24 did it state a different verdict.
- In 23 of 24 it named a source by its title.
- The median answer time was 6.5 s.
Every answer is kept in agent-eval.json. The repo also has 109 frontend tests, 65 backend tests and 5 Chrome journeys, and scripts/verify-live.py re-runs the whole Access Desk journey against the live project.
Things I learned the hard way:
- A Context endpoint with any dataset source silently switches to GROQ mode and ignores the Knowledge Base. My endpoint has the Knowledge Base as its only source.
- Project tokens are rejected by Context. You need an organization token.
-
Knowledge Base management needs a user session, not a project token.
scripts/context-knowledge-base.pycreates, imports and builds the Knowledge Base from the GROQ file, so it's reproducible from the repo.
Sanity Project Details
-
Project ID:
tubqyqod -
Dataset:
production. It's public, so you can query it directly: - Studio: https://spatialize-tubqyqod.sanity.studio/
-
Context Knowledge Base: "Spatialize - Harbor Arts access evidence" (
kbecyKU3h9k8), served by the Knowledge Base MCP endpointspatialize-access-evidence




Top comments (0)