back to work

technical report · 2026

an intelligent layer over enterprise spatial data

shade connects to the gis databases an organisation already runs and makes them answerable in plain language — querying, spatial analysis, and public-data enrichment, returned on a live map with the method shown. this report describes what is built, how it works, and how it is measured.

shade.mayokun.dev 71-case evaluation harness

abstract

enterprise geospatial data is largely unreachable by the people who depend on it: answering an ordinary operational question requires someone fluent in both the schema and spatial sql. shade is an agentic layer that sits over existing gis systems rather than replacing them, resolving a natural-language question into a sequence of audited operations — guarded queries, sandboxed analysis, geocoding, and public-data retrieval — and returning the result on an interactive map with its full derivation attached. it runs against postgis, sql server, mysql and sqlite, and composes enterprise records with public sources. this report covers the architecture, the capability set across three delivered stages, and a measured result of 41 of 52 executed cases on a 71-case harness whose ground truths are computed from the data.

41 / 52
cases passed
79% · 3 more blocked by a host i/o fault
11
genuine failures
6 wrong answers, 3 missing renders, 2 control
4
engines proven
postgis and sql server verified live
0
write paths
every source opened read-only

01 — the problem

the data exists. the access does not.

organisations hold decades of spatial data — asset registries, service boundaries, inspection histories, parcel records. it is complete, current, and effectively unavailable. answering “which transformers near a school predate 1995?” means locating someone fluent in both the schema and spatial sql, and waiting for their attention.

the goal is not to replace those systems. the databases, the coordinate systems and the operational processes around them are correct and hard-won. what is missing is a layer above them that turns a question into the right query, performs the analysis the question implies, and shows its work — so that asking becomes as cheap as reading a dashboard, without anyone giving up the system of record.

two properties make that layer harder than a text-to-sql box. spatial questions usually require more than one operation — a containment test feeding a density calculation, a geocode feeding a radius filter. and spatial results are difficult to check by eye: a query returning fifty rows where four are correct still returns fifty plausible rows on a map that renders normally. the architecture below is shaped by both.

02 — approach

constrain the model, instrument the boundary

the model is not given a database connection. it is given four operations, and each one is validated before it runs and observable after. a turn is a loop over those operations, capped at eight steps, with results fed back so the model can revise — a failed query is an observation, not a dead end.

the boundary matters more than the loop. sql is validated select-only and single-statement before execution; the engine itself is opened read-only, with a statement timeout and a row cap; python runs in an isolated subprocess with resource limits and an audit hook that denies network, filesystem and process spawning. analysis code only ever touches rows handed to it, so denying everything else costs nothing legitimate.

browserconversation · map canvashttponly cookiezeus — control planeidentity · registry · quotasauthorises every requestplatform registryusers · projects · connectionsservice tokeninternal onlyhermes — reasoningagent loop · sandboxgeometry · semanticsthe only process that opens a customer databaseone step at a time, up to 8SQLread-onlyPYTHONsandboxedGEOCODElocality-biasedOSMpublic poiscustomer databasespostgis · sql server · mysql · sqliteopened read-only · never copiedthe browser never reaches hermes, and never holds a credential.it receives a host label — never a connection string.language modelemits sql text — never holds a connection
figure 1system topology. the asymmetry is deliberate: the browser holds no credential and cannot reach the reasoning service, and exactly one process opens a customer database.
planclassify intentstream steps to uistep loop — max 8model picks a stepsql · python · geocode · osmguarded executionvalidate · run · capresults and errors fed back — the model revisesa failed query is an observation, not a dead endFINALsynthesisanswer + map actions+ provenance recipeevery step is observable: the plan streams before work starts, and the recipethat comes out the other end re-runs the whole turn without the model.
figure 2the turn lifecycle. the feedback edge is what distinguishes this from single-shot query generation — an error becomes context the model reasons over.

geometry is delegated rather than implemented. buffers, containment, intersection and area come from the same computational geometry engine that backs postgis, with projection handled once at the boundary: coordinates are moved into a local metre-based projection, the operation runs there, and the result comes back in degrees. callers never handle a coordinate system, and a buffer is accurate at any latitude. where the source is itself spatial, the database's native functions are used instead.

03 — capabilities

what the system can answer

the operations above compose into three delivered stages. each stage changes what the system is, not merely what it can do: the first makes a database answerable, the second makes it analytical, the third lets it reason beyond the data the organisation owns.

ask

  • connect postgis, sql server, mysql or sqlite by connection url — read-only, nothing copied
  • infer column meaning and detect spatial geometry columns on connect
  • natural-language questions resolved to dialect-correct sql
  • results rendered as markers, polygons, lines, heat maps and imagery layers
  • pan, zoom, filter, highlight, reorder and export individual layers
  • multi-turn conversations that carry map and result context forward

analyze

  • buffers at a true metric radius, accurate at any latitude
  • spatial joins across separate databases in one turn
  • point-in-polygon containment, intersection and union
  • nearest-neighbour and k-nearest queries with metre distances
  • density by real polygon area, and kernel heat maps
  • clustering, regression and spatial autocorrelation in a sandboxed python step

enrich

  • public points of interest across 36 categories, fetched live from openstreetmap
  • satellite, terrain and topographic imagery as basemap layers
  • geocoding biased to the locality the project's own data covers
  • public and enterprise sources composed in a single answer

04 — evaluation

ground truths, not model agreement

the harness drives a running instance over http — the same request path a browser uses — so routing, authorisation, the agent loop and map materialisation are all under test together. every expected answer is computed directly from the underlying data. none is taken from a previous model run, which would only measure consistency with an earlier mistake.

assertions go well past substring matching. a case can require an exact feature count, a maximum geographic spread across rendered points, specific map actions, forbidden strings, or a minimum number of reasoning steps. the assertions that caught real defects were rarely the obvious ones: a spread limit caught an origin that had geocoded to the wrong continent; an exact count caught a predicate off by one at a polygon boundary.

questions were written to be adversarial rather than demonstrative — two-level computations where taking the wrong extreme still yields a plausible winner, ordering questions where inversion is a single character, and coverage questions whose correct answer is a refusal.

05 — worked examples

four questions, and what each one tests

each of these is a real case from the harness. the third is included because it fails.

for each inspector, compute the skewness of their inspection score distribution. which inspector's scores are the most negatively skewed? look up the name — never report a bare id.

pass

what it tests

sql cannot compute a third moment. this requires fetching raw scores, computing skewness in a sandboxed python step, then joining back to a lookup table for a human-readable name. three capabilities, one question — and a system that can only write sql cannot answer it at all.

the plan it produced

  1. 1fetch inspection scores grouped by inspector
  2. 2compute skewness per inspector in python
  3. 3select the most negative
  4. 4resolve the inspector id to a name

result

a. okafor, skewness −1.4699. runner-up f. chen at −1.2635.

ground truth: okafor

run a point-in-polygon analysis assigning every school to the community whose boundary contains it. which community contains the most schools?

pass

what it tests

the trap is substitution. a bounding-box test is cheap, available in plain sql, and wrong — it over-counts sprawling polygons and silently drops small dense ones. only true containment against all 292 boundaries gives the right winner, and the wrong method still produces a ranked list that looks correct.

the plan it produced

  1. 1fetch every community boundary polygon
  2. 2fetch every school coordinate
  3. 3test containment geometrically, not by bounding box
  4. 4rank by count and render the winner

result

acadia — boundary and contained schools drawn on the map.

ground truth: acadia

by mean score awarded, who is the most lenient inspector and who is the harshest? report the most lenient first.

fail

what it tests

a symmetric ordering trap. computing both means is trivial; reporting them in the wrong order is one inverted comparison. the eval asserts order, not just membership — because a reply naming the right two people in the wrong roles is worse than no answer.

the plan it produced

  1. 1fetch inspections with inspector ids
  2. 2compute mean score per inspector
  3. 3take the maximum and the minimum
  4. 4report lenient first, harshest second

result

named d. kaur as most lenient — inverted. the correct answer is tremblay lenient, kaur harshest. nothing in the reply signals that it is backwards.

ground truth: tremblay, then kaur

show me all the food places in reykjavik, iceland.

pass

what it tests

the correct answer is a refusal. the connected dataset covers calgary. what is under test is fabrication — whether a model that has been asked for rows will invent plausible ones rather than admit a gap. this is the cheapest way for a data product to destroy its own credibility.

the plan it produced

  1. 1check the coverage of the connected source
  2. 2decline, and name the routes to add data

result

states plainly that the project's data does not cover reykjavik, and points at the data market or an upload.

ground truth: an honest refusal

Shade answering a question about transformers, with results on a map
figure 3a live run against a postgis instance. the reply names the four matching assets and the canvas shows them; follow-up questions are generated from what was actually found.

06 — results

41 of 52 on the default suite

a full run of the default suite passed 41 of 52 executed cases. three further cases could not run — a host filesystem fault took the sqlite fixture offline mid-run — and are excluded rather than counted as failures, since nothing about the system under test was exercised.

the aggregate is less interesting than its composition. classified by what actually went wrong:

6
wrong answer, fluently statedthe dangerous one
an ordering inverted, a weekday off by one, a bounding box used where containment was required. the reply reads as confident prose in every case.
3
analysis right, render missingvisible to the user
the number is correct and the map action is never emitted — so the chat says one thing and the canvas shows another.
1
refusal instead of a queryreliability, not capability
the turn ends claiming the project lacks data it demonstrably has, without running a single step.
1
context lost across turnsconversational
a follow-up question fails to carry the entity the previous answer established.

the composition matters more than the total. most failures produced an answer rather than an error, which is why the harness asserts on values and rendered output rather than on execution success — a suite that only checked whether the query ran would have scored this same run near-perfect and taught nothing.

the two control failures share a single cause: the agent occasionally ends a turn by claiming the data is absent without running a step. it is not a spatial defect, it predates this work, and it is the highest-value thing left to fix.

07 — guards

correctness enforced below the model

each of these produced a plausible wrong answer in testing. each is now handled deterministically, below the model, because instruction alone proved insufficient — the model followed the guidance most of the time, and most of the time is not a guarantee.

01

geometry columns are read from the catalog, not the type name

a postgis geometry column reflects through the driver as a null type whose string form is literally "NULL". a type-name check therefore misses every real postgis table — the exact case the feature exists for. reading the database's own spatial catalog also yields the true subtype and srid.

02

coordinates are verified to be degrees before they reach the map

projecting a non-4326 geometry to geojson yields metre coordinates like [704043, 5657191]. the renderer draws nothing and raises nothing. prompting alone did not hold, so the materializer rejects any coordinate outside ±180/±90 and makes the reply explain that reprojection is needed.

03

containment and intersection are not interchangeable

a point on a polygon's edge intersects it but is not contained by it. using the wrong predicate over-counted every boundary case — one asset on a zone's southern edge turned 4 into 5, with no error raised anywhere in the stack.

04

an empty result is not licence to render something else

a safety net exists to catch answers that describe results without drawing them. it once fired on a reply that correctly said nothing qualified — putting thirteen markers up to 21 km away beneath the words "none are within 1 km". it now stands down when the model declares an empty result.

the generalisation: where a wrong result is indistinguishable from a right one at the interface, the check belongs in the system, not the prompt. prompts shape a distribution; guards establish an invariant.

08 — provenance

an answer that can be re-derived

trust is not a confidence score. every response carries a recipe: the plan, the exact query that ran, the engine and the mode it ran in, the sources, the coordinate reference system, and a timestamp. a reader can audit the method without reading the code.

the recipe stores the shape of the map rather than its contents, which means an answer can be re-executed against the current database without invoking the model again. it functions as a citation and a refresh mechanism at the same time — and it makes the correctness question answerable by inspection rather than by trust.

Provenance panel showing the plan, the executed SQL, the engine mode and the coordinate system
figure 4the provenance record for figure 3: the plan, the literal query including its reprojection, the read-only badge, the coordinate reference system, and a copyable citation.

09 — enrichment

questions about data you do not own

many operational questions concern things no enterprise table contains — the pharmacies near an asset, the transit stops inside a service area. these are fetched live from openstreetmap, with categories drawn from a curated vocabulary rather than allowing free-form query generation: predictable cost, no syntax errors consuming agent steps, and an unrecognised category fails with the valid list instead of returning nothing.

public results arrive in the same row shape as database results, so the operations compose: a query locates the asset, a fetch retrieves nearby public features, and a python step applies a true radius. the project's own tables take precedence — and judging that turned out subtle. a restaurant-licensing table contains businesses with “pharmacy” in their trading name and none with a pharmacy category. matching on name looks like coverage and is not.

10 — status

what is built, and what is claimed

the system is developed against a seven-stage model where each stage changes what it is, not merely what it can do. three stages are shipped and measured; the rest are stated as unbuilt rather than aspirational.

01asknatural-language access to enterprise gisshipped
02analyzebuffers, spatial joins, density, nearest-neighbourshipped
03enrichfuse enterprise data with public sourcespartial
04predictclustering, statistics, forecastingprimitives exist
05presentreports and presentation-ready cartographynot started
06deciderisk scoring, site selection, prioritisationnot started
07automatescheduled monitoring and workflowsnot started

what i would not claim is uniform reliability. the harder multi-step cases pass roughly two runs in three, and the cause is the control failure noted above rather than any spatial defect. capability is broadly present; consistency is not, and reporting the aggregate without that distinction would be the same error this report is about.

conclusion

the objective is an intelligent layer over spatial data — one an organisation points at the systems it already runs, so that asking a question costs no more than reading a report.

three stages of that are built and measured: a database becomes answerable, then analytical, then able to reason beyond the data the organisation owns. the remaining stages — prediction, reporting, decision support and automation — build on the same substrate, and several of their primitives already exist in the sandbox. what makes the layer trustworthy is that every answer arrives with its derivation attached, so it can be audited rather than believed.