mapsmith 0.3.0__tar.gz → 0.4.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {mapsmith-0.3.0 → mapsmith-0.4.0}/.gitignore +5 -0
- mapsmith-0.4.0/.zenodo.json +19 -0
- {mapsmith-0.3.0 → mapsmith-0.4.0}/CHANGELOG.md +385 -3
- mapsmith-0.4.0/CITATION.cff +31 -0
- {mapsmith-0.3.0 → mapsmith-0.4.0}/PKG-INFO +349 -74
- {mapsmith-0.3.0 → mapsmith-0.4.0}/README.md +348 -73
- {mapsmith-0.3.0 → mapsmith-0.4.0}/SECURITY.md +71 -8
- {mapsmith-0.3.0 → mapsmith-0.4.0}/docs/argleton-run.json +6 -6
- {mapsmith-0.3.0 → mapsmith-0.4.0}/docs/benchmarks.md +32 -4
- {mapsmith-0.3.0 → mapsmith-0.4.0}/docs/catalog-entry-spec.md +43 -7
- {mapsmith-0.3.0 → mapsmith-0.4.0}/examples/03_validated_plans.ipynb +23 -23
- {mapsmith-0.3.0 → mapsmith-0.4.0}/pyproject.toml +1 -1
- {mapsmith-0.3.0 → mapsmith-0.4.0}/schema/operation.schema.json +11 -2
- {mapsmith-0.3.0 → mapsmith-0.4.0}/server.json +3 -3
- {mapsmith-0.3.0 → mapsmith-0.4.0}/site/build.py +4 -1
- {mapsmith-0.3.0 → mapsmith-0.4.0}/site/index.template.html +107 -24
- {mapsmith-0.3.0 → mapsmith-0.4.0}/src/mapsmith/__init__.py +1 -1
- mapsmith-0.4.0/src/mapsmith/antimeridian.py +205 -0
- {mapsmith-0.3.0 → mapsmith-0.4.0}/src/mapsmith/catalog.py +1506 -66
- mapsmith-0.4.0/src/mapsmith/discovery_log.py +182 -0
- {mapsmith-0.3.0 → mapsmith-0.4.0}/src/mapsmith/engines/dispatch.py +15 -2
- {mapsmith-0.3.0 → mapsmith-0.4.0}/src/mapsmith/engines/duckdb_engine.py +13 -4
- mapsmith-0.4.0/src/mapsmith/engines/esri.py +239 -0
- mapsmith-0.4.0/src/mapsmith/engines/linework.py +845 -0
- mapsmith-0.4.0/src/mapsmith/engines/network.py +1085 -0
- {mapsmith-0.3.0 → mapsmith-0.4.0}/src/mapsmith/engines/raster.py +171 -2
- mapsmith-0.4.0/src/mapsmith/engines/sampling.py +610 -0
- mapsmith-0.4.0/src/mapsmith/engines/spatial_stats.py +794 -0
- mapsmith-0.4.0/src/mapsmith/engines/summaries.py +582 -0
- {mapsmith-0.3.0 → mapsmith-0.4.0}/src/mapsmith/engines/vector.py +618 -15
- {mapsmith-0.3.0 → mapsmith-0.4.0}/src/mapsmith/engines/whitebox_engine.py +479 -1
- mapsmith-0.4.0/src/mapsmith/grid.py +435 -0
- {mapsmith-0.3.0 → mapsmith-0.4.0}/src/mapsmith/plans/registry.py +290 -0
- {mapsmith-0.3.0 → mapsmith-0.4.0}/src/mapsmith/provenance.py +80 -1
- {mapsmith-0.3.0 → mapsmith-0.4.0}/src/mapsmith/readers.py +397 -354
- {mapsmith-0.3.0 → mapsmith-0.4.0}/src/mapsmith/server.py +66 -2
- mapsmith-0.4.0/src/mapsmith/sql_policy.py +255 -0
- mapsmith-0.4.0/src/mapsmith/stacks.py +290 -0
- {mapsmith-0.3.0 → mapsmith-0.4.0}/src/mapsmith/verify.py +50 -4
- mapsmith-0.4.0/tests/conftest.py +37 -0
- {mapsmith-0.3.0 → mapsmith-0.4.0}/tests/data/manifest_spec_validator.py +21 -1
- mapsmith-0.4.0/tests/test_antimeridian.py +302 -0
- {mapsmith-0.3.0 → mapsmith-0.4.0}/tests/test_catalog.py +1 -1
- {mapsmith-0.3.0 → mapsmith-0.4.0}/tests/test_catalog_applicability.py +32 -3
- mapsmith-0.4.0/tests/test_contours_and_cost.py +287 -0
- mapsmith-0.4.0/tests/test_dashboard.py +319 -0
- {mapsmith-0.3.0 → mapsmith-0.4.0}/tests/test_discovery_contract.py +95 -9
- mapsmith-0.4.0/tests/test_discovery_log.py +268 -0
- mapsmith-0.4.0/tests/test_discovery_report.py +217 -0
- {mapsmith-0.3.0 → mapsmith-0.4.0}/tests/test_duckdb_sandbox.py +69 -0
- mapsmith-0.4.0/tests/test_environment.py +505 -0
- mapsmith-0.4.0/tests/test_grid_registration.py +436 -0
- mapsmith-0.4.0/tests/test_linework.py +443 -0
- mapsmith-0.4.0/tests/test_network.py +472 -0
- {mapsmith-0.3.0 → mapsmith-0.4.0}/tests/test_path_containment.py +357 -241
- {mapsmith-0.3.0 → mapsmith-0.4.0}/tests/test_plans.py +55 -0
- mapsmith-0.4.0/tests/test_published_figures.py +275 -0
- {mapsmith-0.3.0 → mapsmith-0.4.0}/tests/test_retrieval_at_scale.py +221 -219
- {mapsmith-0.3.0 → mapsmith-0.4.0}/tests/test_retrieval_degradation.py +99 -1
- mapsmith-0.4.0/tests/test_sampling.py +491 -0
- mapsmith-0.4.0/tests/test_select_and_extract.py +720 -0
- {mapsmith-0.3.0 → mapsmith-0.4.0}/tests/test_showcase.py +320 -17
- mapsmith-0.4.0/tests/test_south_up_and_mixed.py +257 -0
- mapsmith-0.4.0/tests/test_spatial_stats.py +485 -0
- mapsmith-0.4.0/tests/test_sql_credentials.py +289 -0
- mapsmith-0.4.0/tests/test_stacks.py +289 -0
- mapsmith-0.4.0/tests/test_summaries.py +468 -0
- {mapsmith-0.3.0 → mapsmith-0.4.0}/tests/test_verify.py +279 -2
- {mapsmith-0.3.0 → mapsmith-0.4.0}/tests/test_verify_repair.py +9 -1
- {mapsmith-0.3.0 → mapsmith-0.4.0}/tests/test_whitebox_derivatives.py +351 -245
- {mapsmith-0.3.0 → mapsmith-0.4.0}/tests/test_worked_example.py +17 -83
- mapsmith-0.3.0/src/mapsmith/sql_policy.py +0 -105
- mapsmith-0.3.0/tests/test_discovery_report.py +0 -102
- mapsmith-0.3.0/tests/test_sql_credentials.py +0 -117
- {mapsmith-0.3.0 → mapsmith-0.4.0}/.dockerignore +0 -0
- {mapsmith-0.3.0 → mapsmith-0.4.0}/CLAUDE.md +0 -0
- {mapsmith-0.3.0 → mapsmith-0.4.0}/CONTRIBUTING.md +0 -0
- {mapsmith-0.3.0 → mapsmith-0.4.0}/Dockerfile +0 -0
- {mapsmith-0.3.0 → mapsmith-0.4.0}/LICENSE +0 -0
- {mapsmith-0.3.0 → mapsmith-0.4.0}/MANIFESTO.md +0 -0
- {mapsmith-0.3.0 → mapsmith-0.4.0}/TRADEMARKS.md +0 -0
- {mapsmith-0.3.0 → mapsmith-0.4.0}/docker-compose.yml +0 -0
- {mapsmith-0.3.0 → mapsmith-0.4.0}/examples/01_verified_geoprocessing.ipynb +0 -0
- {mapsmith-0.3.0 → mapsmith-0.4.0}/examples/02_terrain_hydrology.ipynb +0 -0
- {mapsmith-0.3.0 → mapsmith-0.4.0}/examples/README.md +0 -0
- {mapsmith-0.3.0 → mapsmith-0.4.0}/examples/fixtures/mount_st_helens_dem.tif +0 -0
- {mapsmith-0.3.0 → mapsmith-0.4.0}/funding.json +0 -0
- {mapsmith-0.3.0 → mapsmith-0.4.0}/src/mapsmith/engines/__init__.py +0 -0
- {mapsmith-0.3.0 → mapsmith-0.4.0}/src/mapsmith/engines/geodesy.py +0 -0
- {mapsmith-0.3.0 → mapsmith-0.4.0}/src/mapsmith/engines/sedona_engine.py +0 -0
- {mapsmith-0.3.0 → mapsmith-0.4.0}/src/mapsmith/gdal_policy.py +0 -0
- {mapsmith-0.3.0 → mapsmith-0.4.0}/src/mapsmith/jobs.py +0 -0
- {mapsmith-0.3.0 → mapsmith-0.4.0}/src/mapsmith/plans/__init__.py +0 -0
- {mapsmith-0.3.0 → mapsmith-0.4.0}/src/mapsmith/plans/executor.py +0 -0
- {mapsmith-0.3.0 → mapsmith-0.4.0}/src/mapsmith/plans/models.py +0 -0
- {mapsmith-0.3.0 → mapsmith-0.4.0}/src/mapsmith/plans/validator.py +0 -0
- {mapsmith-0.3.0 → mapsmith-0.4.0}/src/mapsmith/preview.py +0 -0
- {mapsmith-0.3.0 → mapsmith-0.4.0}/src/mapsmith/retrieval.py +0 -0
- {mapsmith-0.3.0 → mapsmith-0.4.0}/src/mapsmith/ui.py +0 -0
- {mapsmith-0.3.0 → mapsmith-0.4.0}/src/mapsmith/workspace.py +0 -0
- {mapsmith-0.3.0 → mapsmith-0.4.0}/tests/data/discovery_queries.json +0 -0
- {mapsmith-0.3.0 → mapsmith-0.4.0}/tests/data/manifest-v1.schema.json +0 -0
- {mapsmith-0.3.0 → mapsmith-0.4.0}/tests/test_ambiguous_layer.py +0 -0
- {mapsmith-0.3.0 → mapsmith-0.4.0}/tests/test_band_math.py +0 -0
- {mapsmith-0.3.0 → mapsmith-0.4.0}/tests/test_catalog_entry_spec.py +0 -0
- {mapsmith-0.3.0 → mapsmith-0.4.0}/tests/test_catalog_retrieval.py +0 -0
- {mapsmith-0.3.0 → mapsmith-0.4.0}/tests/test_centroid_convert.py +0 -0
- {mapsmith-0.3.0 → mapsmith-0.4.0}/tests/test_clip_reclassify_raster.py +0 -0
- {mapsmith-0.3.0 → mapsmith-0.4.0}/tests/test_describe_raster.py +0 -0
- {mapsmith-0.3.0 → mapsmith-0.4.0}/tests/test_engines.py +0 -0
- {mapsmith-0.3.0 → mapsmith-0.4.0}/tests/test_gdal_policy.py +0 -0
- {mapsmith-0.3.0 → mapsmith-0.4.0}/tests/test_geodesy.py +0 -0
- {mapsmith-0.3.0 → mapsmith-0.4.0}/tests/test_geoparquet2.py +0 -0
- {mapsmith-0.3.0 → mapsmith-0.4.0}/tests/test_measure_area.py +0 -0
- {mapsmith-0.3.0 → mapsmith-0.4.0}/tests/test_merge_simplify.py +0 -0
- {mapsmith-0.3.0 → mapsmith-0.4.0}/tests/test_nearest_explode.py +0 -0
- {mapsmith-0.3.0 → mapsmith-0.4.0}/tests/test_overlay_dissolve.py +0 -0
- {mapsmith-0.3.0 → mapsmith-0.4.0}/tests/test_preview.py +0 -0
- {mapsmith-0.3.0 → mapsmith-0.4.0}/tests/test_raster.py +0 -0
- {mapsmith-0.3.0 → mapsmith-0.4.0}/tests/test_raster_reproject_bands.py +0 -0
- {mapsmith-0.3.0 → mapsmith-0.4.0}/tests/test_redaction.py +0 -0
- {mapsmith-0.3.0 → mapsmith-0.4.0}/tests/test_resample_raster.py +0 -0
- {mapsmith-0.3.0 → mapsmith-0.4.0}/tests/test_retrieval.py +0 -0
- {mapsmith-0.3.0 → mapsmith-0.4.0}/tests/test_smoke.py +0 -0
- {mapsmith-0.3.0 → mapsmith-0.4.0}/tests/test_sql_axis_order.py +0 -0
- {mapsmith-0.3.0 → mapsmith-0.4.0}/tests/test_terrain_derivatives.py +0 -0
- {mapsmith-0.3.0 → mapsmith-0.4.0}/tests/test_tier_a_operations.py +0 -0
- {mapsmith-0.3.0 → mapsmith-0.4.0}/tests/test_ui.py +0 -0
- {mapsmith-0.3.0 → mapsmith-0.4.0}/tests/test_verification_status.py +0 -0
- {mapsmith-0.3.0 → mapsmith-0.4.0}/tests/test_voronoi.py +0 -0
- {mapsmith-0.3.0 → mapsmith-0.4.0}/tests/test_whitebox.py +0 -0
- {mapsmith-0.3.0 → mapsmith-0.4.0}/tests/test_whitebox_encoding.py +0 -0
- {mapsmith-0.3.0 → mapsmith-0.4.0}/tests/test_whitebox_geographic_refusal.py +0 -0
- {mapsmith-0.3.0 → mapsmith-0.4.0}/tests/test_workspace.py +0 -0
|
@@ -61,3 +61,8 @@ uv.lock
|
|
|
61
61
|
|
|
62
62
|
# The site is generated: `python site/build.py dist` rebuilds it from a real run.
|
|
63
63
|
site/generated/
|
|
64
|
+
|
|
65
|
+
# Written by `benchmarks/dashboard.py` and by MAPSMITH_DISCOVERY_LOG:
|
|
66
|
+
# both hold the queries somebody typed, so they stay out of the repository.
|
|
67
|
+
dashboard.html
|
|
68
|
+
*.jsonl
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"upload_type": "software",
|
|
3
|
+
"title": "MapSmith: geospatial processing for AI agents, with verifiable provenance",
|
|
4
|
+
"version": "0.4.0",
|
|
5
|
+
"creators": [
|
|
6
|
+
{"name": "The MapSmith project"}
|
|
7
|
+
],
|
|
8
|
+
"license": "agpl-3.0-or-later",
|
|
9
|
+
"access_right": "open",
|
|
10
|
+
"keywords": [
|
|
11
|
+
"GIS",
|
|
12
|
+
"geospatial",
|
|
13
|
+
"provenance",
|
|
14
|
+
"reproducibility",
|
|
15
|
+
"Model Context Protocol",
|
|
16
|
+
"MCP"
|
|
17
|
+
],
|
|
18
|
+
"description": "<p>An MCP server that gives AI agents professional geoprocessing under a rule the agent cannot bend: every geometry and every number comes from a real engine run, never from the model.</p><p>Each operation that writes a dataset also writes a manifest beside it — the inputs and their hashes, the exact parameters, the coordinate-system decisions and the reason for each, the engine and its version, the environment that changed the answer, and the deterministic checks that ran. The manifest is written <em>before</em> a failed check raises, so the audit trail survives the error rather than disappearing with it.</p><p>Capabilities are reached through a searchable catalogue rather than a wall of tools, because a caller's ability to pick the right tool degrades long before a catalogue does. The catalogue answers with a set of candidates and the text that distinguishes them, not with a verdict.</p><p>A manifest records what was done; it does <em>not</em> certify that what was done was right. MapSmith says so in its own documentation, and a separate correctness suite exists to measure the difference.</p>"
|
|
19
|
+
}
|
|
@@ -4,9 +4,391 @@ All notable changes to MapSmith are documented here, in the format of
|
|
|
4
4
|
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/). This project follows
|
|
5
5
|
[semantic versioning](https://semver.org/).
|
|
6
6
|
|
|
7
|
-
## [
|
|
7
|
+
## [0.4.0] — 2026-08-31
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- **The caller chooses the geoprocessing stack once, at the start, and it is
|
|
12
|
+
never swapped underneath them.** `MAPSMITH_STACK` takes `opensource` (the
|
|
13
|
+
default: GDAL, GeoPandas, DuckDB, Whitebox) or `esri`, which routes to ArcPy
|
|
14
|
+
on a machine that has a licence for it. The rule that makes the choice worth
|
|
15
|
+
making is what happens at the edges: when the chosen stack cannot do
|
|
16
|
+
something, MapSmith says which of three things is true — the tool does not
|
|
17
|
+
exist, this licence tier does not include it, or it would need an online
|
|
18
|
+
service — and falls back only where the caller asked for a fallback, naming
|
|
19
|
+
the substitution in the manifest. An engine quietly replaced by another is a
|
|
20
|
+
result whose provenance record is true and whose number came from software
|
|
21
|
+
nobody chose.
|
|
22
|
+
- **Citation metadata** (`CITATION.cff`). Every release from here on is
|
|
23
|
+
archived and assigned a DOI, so the file is a release artifact: a test fails
|
|
24
|
+
if its version is not the version being released, or if its release date is
|
|
25
|
+
in the future.
|
|
26
|
+
- **A manifest can now say which configuration produced the numbers**
|
|
27
|
+
(`environment`, section 3.8 of the manifest specification). The field existed
|
|
28
|
+
in the published specification and in the schema, and nothing in MapSmith
|
|
29
|
+
could produce it — a whole section describing something the reference
|
|
30
|
+
implementation could not do.
|
|
31
|
+
|
|
32
|
+
What made it concrete: a GeoTIFF and the `.aux.xml` beside it can declare
|
|
33
|
+
different georeferencing, and GDAL prefers the sidecar by documented design,
|
|
34
|
+
because that is how somebody overrides georeferencing they know to be wrong.
|
|
35
|
+
Both readings are the library behaving exactly as written. On one fixture the
|
|
36
|
+
same file gives 40,000 m² or 160,000 m² and an origin a hundred kilometres
|
|
37
|
+
apart, and until now nothing MapSmith wrote could say which of the two it
|
|
38
|
+
read. There is nothing upstream to fix and everything to state.
|
|
39
|
+
|
|
40
|
+
So: `describe_dataset` reports both sources when a raster has two, and the
|
|
41
|
+
nine operations that read a raster's grid directly refuse instead, naming
|
|
42
|
+
both and saying how to choose. The terrain and sampling operations are not
|
|
43
|
+
covered yet and the README says so rather than implying otherwise; the
|
|
44
|
+
terrain engine stops on the same file for a different reason, because its own
|
|
45
|
+
reading of the grid disagrees with GDAL's, and its message names neither the
|
|
46
|
+
sidecar nor the way out. That split is the multi-layer refusal (#29)
|
|
47
|
+
applied to a second axis — the format's default answers a question the caller
|
|
48
|
+
never asked, and a manifest could not honestly say which data produced the
|
|
49
|
+
numbers. Describing is different from computing: a file with two
|
|
50
|
+
georeferencings is a thing to be told about.
|
|
51
|
+
|
|
52
|
+
The field is filled where a manifest becomes a file rather than in
|
|
53
|
+
`verify.audited`, and the difference matters: seventeen writers of fifty-seven
|
|
54
|
+
build their record by hand and never reach `audited`, and they are
|
|
55
|
+
concentrated in raster — exactly where georeferencing decides the numbers.
|
|
56
|
+
Empty when there is nothing to say, because a field that appears on every
|
|
57
|
+
operation is a field its reader learns to skip.
|
|
58
|
+
- **Redaction now reads dictionary keys, not only values.** Adding a field that
|
|
59
|
+
holds environment variables exposed a gap that was never about that field:
|
|
60
|
+
`{"AWS_SECRET_ACCESS_KEY": "AKIA…"}` passed through untouched, because the
|
|
61
|
+
scanner looks for `name=value` *inside a string* and in a dictionary the name
|
|
62
|
+
is the key. That was true of every dictionary a manifest carries. Bare `key`
|
|
63
|
+
stays off the list on purpose — `sort_key` and `primary_key` are ordinary
|
|
64
|
+
field names, and a mask that fires on those teaches its reader to distrust it.
|
|
65
|
+
- **`select_features` and `extract_layer`: two remedies MapSmith was already
|
|
66
|
+
recommending without offering.** The mixed-geometry warning said *select the
|
|
67
|
+
features the question is about*; the multi-layer refusal said *extract the
|
|
68
|
+
layer you mean into its own dataset*. Both then handed over a `run_sql`
|
|
69
|
+
incantation, which is a long way round for "keep the lines". Now they name an
|
|
70
|
+
operation, and the hints were rewritten to do so — an error message that
|
|
71
|
+
points at a longer route than the one that exists is a defect of its own,
|
|
72
|
+
because it is the only thing a blocked caller reads.
|
|
73
|
+
- `select_features` keeps features by geometry family (`line` keeps
|
|
74
|
+
MultiLineString too), by exact geometry type, or by an attribute condition
|
|
75
|
+
(`field_equals`, `field_in`, `field_between`, inclusive at both ends).
|
|
76
|
+
Deliberately no expression language: that is `run_sql` with extra steps and
|
|
77
|
+
its own way of going wrong. Filter values arriving as text against a numeric
|
|
78
|
+
column are **converted and recorded**, and the conversion tries `int` before
|
|
79
|
+
`float` because it has to: `float("9007199254740993")` is 9007199254740992,
|
|
80
|
+
so going through float would return **the row next to** the one asked for —
|
|
81
|
+
one feature kept, every check green, and a manifest naming a number nobody
|
|
82
|
+
typed. OSM ids, BIGINT keys and cadastral references all live past 2^53.
|
|
83
|
+
Five more inputs are refused rather than answered with an empty dataset,
|
|
84
|
+
because an empty dataset with a complete manifest reads as a finding:
|
|
85
|
+
`"high"` against an integer column, `"nan"` (which `float()` accepts and
|
|
86
|
+
which equals nothing, including itself), `"inf"`, `field_equals` with no
|
|
87
|
+
value, and a range whose minimum is above its maximum. Matching nothing for
|
|
88
|
+
a reason that is not one of those is reported as a warning, with the hint
|
|
89
|
+
that a typo and a genuinely empty result look identical.
|
|
90
|
+
- A **family selection says what it could not keep**. `line` keeps
|
|
91
|
+
MultiLineString because a MultiLineString is line-shaped — and by the same
|
|
92
|
+
argument a GeometryCollection holding a polygon is a polygon to everybody
|
|
93
|
+
except its type name, so dropping it without a word would be that care
|
|
94
|
+
applied to half the problem. Types no family covers, and null geometries,
|
|
95
|
+
are counted in a non-critical check that names them.
|
|
96
|
+
- `extract_layer` copies one named layer out of a multi-layer container. The
|
|
97
|
+
refusal it resolves (issue #29) stays exactly as strict; what changes is
|
|
98
|
+
that the way out is now one call whose manifest names the layer, the
|
|
99
|
+
container and the layers left behind.
|
|
100
|
+
- **A bounding box that spans the planet for data two degrees wide** is now
|
|
101
|
+
reported with the sentence that makes it readable (`antimeridian.py`).
|
|
102
|
+
RFC 7946 §3.1.9 splits a geometry at the antimeridian, so the bounds computed
|
|
103
|
+
from its coordinates come out as the ordinary west-to-east form spanning the
|
|
104
|
+
world; §5.2 defines the crossing form as the one with west greater than east.
|
|
105
|
+
Nothing in a planar geometry library can produce the second from the first,
|
|
106
|
+
so `describe_dataset` reported `(-180, -17.5, 180, -16.5)` for a Fijian survey
|
|
107
|
+
zone — arithmetically correct, and the wrong answer to the question anybody
|
|
108
|
+
asked. The plain box is still returned unchanged; when the data crosses, a
|
|
109
|
+
`true_extent`, a width and a note saying what a filter on the plain box would
|
|
110
|
+
select come with it. Distinguishing a real crossing from a dataset that
|
|
111
|
+
genuinely covers the world needs a geometry probe, not arithmetic: a single
|
|
112
|
+
global rectangle has only ±180 as longitudes and they wrap onto one value.
|
|
113
|
+
Detection deliberately carries **no threshold on the plain span**: the first
|
|
114
|
+
version required 350° before looking further, which only ever fires on data
|
|
115
|
+
split exactly at ±180 — the well-formed case. Buoys at 170°E and 140°W span
|
|
116
|
+
310° and were reported in silence. What replaces it is a rule with a meaning:
|
|
117
|
+
a crossing layer reads narrower when the seam is treated as continuous *and*
|
|
118
|
+
occupies less than half the world going the short way, so points scattered at
|
|
119
|
+
-179, -90, 0, 90 and 179 are global data rather than a crossing. Only
|
|
120
|
+
degree-based geographic systems are considered — EPSG:4807 is geographic in
|
|
121
|
+
grads, where wrapping at 360 means nothing.
|
|
122
|
+
|
|
123
|
+
- **`locate_extreme_cell`** answers where the lowest or highest value of a
|
|
124
|
+
raster is, as a coordinate. No operation could be asked that before, which is
|
|
125
|
+
why MapSmith reported `unsupported` twice on Argleton's `grid-registration`
|
|
126
|
+
family — the one whose whole question is whether a system knows where its own
|
|
127
|
+
cells are. Nodata is excluded rather than competing for the minimum, and a tie
|
|
128
|
+
is reported rather than broken in silence: two cells at the same extreme is a
|
|
129
|
+
plateau, a flat pond or a saturated sensor, and naming one of them is a
|
|
130
|
+
confident answer to a question about a region.
|
|
131
|
+
|
|
132
|
+
- **Ten more operations, and this time the gap they fill is a shape rather
|
|
133
|
+
than a subject.** Of sixty-one catalogue entries, three produced an `answer`
|
|
134
|
+
and none of them took a vector layer — so the commonest question in GIS, *how
|
|
135
|
+
much land is there*, had no operation for anybody holding parcels, and a
|
|
136
|
+
search declaring it came back empty. Four operations close that:
|
|
137
|
+
`summarize_field` (totals, averages and extremes of an attribute, per group if
|
|
138
|
+
asked), `spatial_autocorrelation` (global Moran's I — whether the map holds a
|
|
139
|
+
pattern at all, which is the question to ask before `hot_spots` shows where),
|
|
140
|
+
`nearest_neighbour_index` (Clark-Evans R, with the study area treated as the
|
|
141
|
+
decision it is rather than taken silently from the points' own extent), and
|
|
142
|
+
`compare_layers` (what actually changed between two versions of a delivery).
|
|
143
|
+
None of them writes a file, so none carries a manifest: there is no artefact
|
|
144
|
+
to attach one to, and inventing a file to have something to sign would be
|
|
145
|
+
worse than the gap.
|
|
146
|
+
|
|
147
|
+
Three answer requests from the discovery benchmark that neither labeller could
|
|
148
|
+
place. `contour_lines` turns a surface into isolines. `least_cost_path` routes
|
|
149
|
+
across a cost raster where `network_shortest_path` needs roads — charging per
|
|
150
|
+
unit of DISTANCE, so a diagonal step costs sqrt(2) and not 1, which is the
|
|
151
|
+
difference between a route a person would walk and a staircase.
|
|
152
|
+
`transform_by_control_points` puts a survey on real ground by fitting a
|
|
153
|
+
transform from points known in both systems: *"my traverse is sitting in the
|
|
154
|
+
middle of the river"* has an answer, and the answer that matters is the
|
|
155
|
+
residual, which is in the manifest per control point with the worst one named.
|
|
156
|
+
An exactly determined fit says in words that its zero residual is evidence of
|
|
157
|
+
nothing.
|
|
158
|
+
|
|
159
|
+
And three that existed only inside other operations, which means they existed
|
|
160
|
+
only for whoever already knew where to look: `snap_layer`, `points_along_lines`
|
|
161
|
+
(chainage, what `elevation_profile` does before it reads a surface) and
|
|
162
|
+
`line_intersections`.
|
|
163
|
+
|
|
164
|
+
All ten are catalogue-only. The exposed tool list stays at 28, because that is
|
|
165
|
+
the count with a ceiling and capability is the count without one.
|
|
166
|
+
|
|
167
|
+
- **MapSmith can record its own discovery cases, and deliberately does not
|
|
168
|
+
learn from them.** Set `MAPSMITH_DISCOVERY_LOG=<path>` and every catalogue
|
|
169
|
+
search is written as one JSON line together with the operation run after it:
|
|
170
|
+
the query, the facets declared, which engine ranked it, every candidate
|
|
171
|
+
delivered, and where in that list the chosen one sat.
|
|
172
|
+
`benchmarks/log_to_cases.py` turns those lines into rows shaped like the
|
|
173
|
+
discovery benchmark and flags the two worth reading — a choice the ranking did
|
|
174
|
+
not put first, and a search nothing followed.
|
|
175
|
+
|
|
176
|
+
The figures MapSmith publishes rest on 155 requests written by two language
|
|
177
|
+
models, which is the best set obtainable without users and is not what users
|
|
178
|
+
ask. This closes that gap without closing a feedback loop: a ranker trained on
|
|
179
|
+
what callers pick learns from an ordering it produced, and the pinned model
|
|
180
|
+
revision is what makes a search reproducible a year later. So the log produces
|
|
181
|
+
benchmark rows for a person to accept or discard, and the improvement lands in
|
|
182
|
+
the catalogue text as a reviewable diff. Off unless the variable is set; holds
|
|
183
|
+
queries and operation names but never dataset paths or arguments; confined by
|
|
184
|
+
`MAPSMITH_WORKSPACE` like any other path MapSmith writes; never leaves the
|
|
185
|
+
machine.
|
|
186
|
+
|
|
187
|
+
- **Ten operations, chosen by measurement rather than by taste.** The discovery
|
|
188
|
+
benchmark holds 155 requests written by other model families; twenty-one of
|
|
189
|
+
them were marked `none` by BOTH independent labellers, meaning the catalogue
|
|
190
|
+
could not serve them at all. Grouped, those twenty-one are six families, and
|
|
191
|
+
these ten cover four of them:
|
|
192
|
+
|
|
193
|
+
`sample_raster_at_points`, `elevation_profile` and `line_of_sight` read a
|
|
194
|
+
surface at a place, along a line, and between two positions. Every one counts
|
|
195
|
+
the positions it could not read and puts that count in the manifest, because a
|
|
196
|
+
null that reads as a value is how a nodata of -9999 ends up in an average.
|
|
197
|
+
`line_of_sight` requires an explicit answer on earth curvature: over 30 km the
|
|
198
|
+
planet drops 62 m net of refraction, and there is no way to guess whether the
|
|
199
|
+
caller has a rooftop survey or a radio link.
|
|
200
|
+
|
|
201
|
+
`network_shortest_path` and `service_area` route over a line network the caller
|
|
202
|
+
supplies. The failure they are built around is not a wrong number, it is a
|
|
203
|
+
disconnected graph: two street segments a millimetre apart are one junction on
|
|
204
|
+
the ground and two nodes in a naive build, so the route detours or is reported
|
|
205
|
+
impossible and nothing raises. `tolerance` therefore has no default, and every
|
|
206
|
+
manifest carries the component count, the merged-endpoint count and how far
|
|
207
|
+
each end snapped to the network. `service_area` cuts the last segment where the
|
|
208
|
+
budget runs out — a ten-minute walk ends where the walk ends, not at the next
|
|
209
|
+
junction — and it is a network operation rather than a buffer because a circle
|
|
210
|
+
includes the far bank of a bridgeless river and excludes the house 900 m away
|
|
211
|
+
along a straight road.
|
|
212
|
+
|
|
213
|
+
`viewshed` maps what a set of stations can see. **Its output is a COUNT of
|
|
214
|
+
stations, not a 0/1 flag**, which contradicts Whitebox's own documentation:
|
|
215
|
+
the help says "a Boolean raster, containing 1's and 0's", and measured on 2.0.6
|
|
216
|
+
with two stations on flat ground every cell comes back 2.0. Second time this
|
|
217
|
+
library's prose has described the reverse of what its code does, after the D8
|
|
218
|
+
pointer table.
|
|
219
|
+
|
|
220
|
+
`hot_spots`, `smooth_rates`, `aggregate_to_threshold` and `thin_points` are for
|
|
221
|
+
the moment before somebody publishes a map of noise. Getis-Ord Gi* with a
|
|
222
|
+
Benjamini-Hochberg correction, because at 0.05 over 300 districts about fifteen
|
|
223
|
+
hot spots appear from nothing and they will be somewhere. Empirical-Bayes rates,
|
|
224
|
+
because a choropleth of raw rates over small populations is a map of population
|
|
225
|
+
size. Disclosure-control aggregation that refuses rather than publishing the one
|
|
226
|
+
island it could not merge. And deterministic point thinning that says in the
|
|
227
|
+
manifest that it removed data.
|
|
228
|
+
|
|
229
|
+
- **A dashboard for the whole project** (`benchmarks/dashboard.py`), written
|
|
230
|
+
as one self-contained HTML file: no CDN, no fonts, no analytics, works with
|
|
231
|
+
the network off. Six panels — what exists, whether every operation can
|
|
232
|
+
actually be found, the retrieval figures and the degradation curve, Argleton's
|
|
233
|
+
traps and what each engine does with them, the open questions answered by
|
|
234
|
+
clicking, and a trend appended on each generation.
|
|
235
|
+
|
|
236
|
+
It exists because the numbers were spread across a report nobody reruns, a
|
|
237
|
+
test that prints a curve nobody reads, and a suite in another repository. The
|
|
238
|
+
tuning panel is the point: an operation nothing reaches does not exist, and
|
|
239
|
+
this says which ones, at what rank, with words alone and with the facets a
|
|
240
|
+
caller would declare.
|
|
241
|
+
|
|
242
|
+
Two things it caught immediately, both about itself. It drew ten working
|
|
243
|
+
operations as unreachable by collapsing "the search declined because the two
|
|
244
|
+
rankers shared nothing" into "not found" — three outcomes, one label. And the
|
|
245
|
+
MapSmith adapter appeared as `adapters.mapsmith:Adapter` because the label was
|
|
246
|
+
read from an import path. With those apart: 51 of 61 operations found by words
|
|
247
|
+
alone, 55 in the top three once facets are declared, none unreachable.
|
|
248
|
+
|
|
249
|
+
### Fixed
|
|
250
|
+
|
|
251
|
+
- **A DEM whose rows run south to north no longer produces a slope eight times
|
|
252
|
+
too steep.** A GeoTIFF's geotransform may have a positive fifth element,
|
|
253
|
+
meaning row 0 is the southern edge — NetCDF, GRIB and HDF index latitude
|
|
254
|
+
upwards, so a straight conversion produces one. whitebox-workflows cannot
|
|
255
|
+
express that and does not say so: it discards the georeferencing and reads the
|
|
256
|
+
grid as unit cells at the origin. MapSmith answered **45 degrees where the
|
|
257
|
+
ground is 5.71**, wrote the output raster at the origin at a tenth of the
|
|
258
|
+
site's size, and **passed all five of its own checks** — `crs_matches` among
|
|
259
|
+
them, because the coordinate system survived and only the geotransform did
|
|
260
|
+
not.
|
|
261
|
+
|
|
262
|
+
Such a file is now rewritten north-up before the engine sees it, the same
|
|
263
|
+
mechanism already used for the TIFF predictor, and the rewrite is disclosed in
|
|
264
|
+
the manifest. Behind that is the general guard: MapSmith now compares the
|
|
265
|
+
engine's idea of the grid with GDAL's and **refuses** if they differ for any
|
|
266
|
+
other reason, because a number computed on a grid nobody can reconcile carries
|
|
267
|
+
a correct-looking CRS on top of it and nothing downstream can tell.
|
|
268
|
+
|
|
269
|
+
Found by [Argleton](https://argleton.org) trap 026.
|
|
270
|
+
|
|
271
|
+
- **Measuring a layer that holds more than one kind of geometry now says so.**
|
|
272
|
+
A GeoPackage layer may declare its type as GEOMETRY and hold whatever it likes;
|
|
273
|
+
only the shapefile enforced one type per file, which is why mixed layers
|
|
274
|
+
arrive exactly when data is converted out of shapefiles. `length` on a polygon
|
|
275
|
+
is its perimeter — true, and the right answer to a question nobody asked — so
|
|
276
|
+
a pipe network with a treatment plant in the same layer totalled **3000 m
|
|
277
|
+
where the pipe is 2000**.
|
|
278
|
+
|
|
279
|
+
`measure_length` and `measure_area` now carry a non-critical check naming the
|
|
280
|
+
geometry types present, with the remedy. It is not an error: measuring a mixed
|
|
281
|
+
layer is a legitimate request and MapSmith cannot know which features the
|
|
282
|
+
question was about. What it can do is refuse to let the total arrive without
|
|
283
|
+
the sentence — especially here, where **every individual row is still
|
|
284
|
+
correct**, so a spot check of the data confirms the data.
|
|
285
|
+
|
|
286
|
+
Found by Argleton trap 027.
|
|
287
|
+
|
|
288
|
+
- **MapSmith now reads `AREA_OR_POINT`, everywhere at once.** GeoTIFF records
|
|
289
|
+
which of two conventions a grid uses — `RasterPixelIsArea`, where a value
|
|
290
|
+
describes the cell it fills, and `RasterPixelIsPoint`, where it is a sample at
|
|
291
|
+
a grid node — and they differ by half a pixel. Every USGS elevation product is
|
|
292
|
+
point-registered. **No line of MapSmith read the tag.** Every place that
|
|
293
|
+
turned a cell index into a coordinate did what `rasterio.xy` does, which is to
|
|
294
|
+
answer as if the file were area-registered: half a cell, 15 m on a 30 m DEM,
|
|
295
|
+
systematic, and with nothing in any output to say so.
|
|
296
|
+
|
|
297
|
+
That is the shape of #28 again — "open a vector file" as six copies of one
|
|
298
|
+
decision, four of them missing a branch — so the fix has the same shape as its
|
|
299
|
+
fix. `mapsmith/grid.py` is now the only place that decides where a value sits,
|
|
300
|
+
a test fails if a second copy appears, and every operation that converts
|
|
301
|
+
between cells and coordinates records the registration in its manifest,
|
|
302
|
+
including the ordinary case: a manifest that mentions the convention only when
|
|
303
|
+
it is unusual leaves a reader unable to tell *area* from *nobody looked*.
|
|
304
|
+
|
|
305
|
+
What changed behind that: sampling reads at the nodes on a point-registered
|
|
306
|
+
grid, so `sample_raster_at_points`, `elevation_profile` and `line_of_sight`
|
|
307
|
+
stop being half a cell off; `least_cost_path` starts, routes and ends on the
|
|
308
|
+
right cells; `contour_lines` applies its half-cell correction in the direction
|
|
309
|
+
the registration calls for, where an unconditional one put a USGS DEM's
|
|
310
|
+
contours a **whole** cell out; `zonal_statistics` offsets the zones for the
|
|
311
|
+
coverage computation so each cell is weighted around its own sample, and hands
|
|
312
|
+
back the caller's own geometry; and every raster MapSmith writes carries the
|
|
313
|
+
input's registration forward, where `profile.copy()` had been silently
|
|
314
|
+
converting point to area on the way out.
|
|
315
|
+
|
|
316
|
+
Found by building [Argleton](https://argleton.org) trap 024, which measures
|
|
317
|
+
exactly this and which MapSmith could not attempt. It can now, and it answers
|
|
318
|
+
both halves of the pair — the trap at 412090 and its clean twin at 412105,
|
|
319
|
+
fifteen metres apart, from files that differ in one metadata tag.
|
|
320
|
+
|
|
321
|
+
- **Whitebox places contour vertices half a cell from where they belong, and
|
|
322
|
+
MapSmith now corrects it.** Measured on the installed 2.0.6 with a planar ramp:
|
|
323
|
+
the contour for height 3 came back on the WEST EDGE of the column holding 3
|
|
324
|
+
rather than at its centre. On a 30 m DEM that is 15 m of horizontal error on
|
|
325
|
+
every contour — plausible, well-formed and wrong, which is the third time this
|
|
326
|
+
library's behaviour has diverged from its description here after the TIFF
|
|
327
|
+
predictor and the D8 pointer table.
|
|
328
|
+
|
|
329
|
+
The correction is verified rather than trusted: the DEM is read back at the
|
|
330
|
+
finished vertices and the elevation must equal the height each line claims, so
|
|
331
|
+
a future library version that fixes its own registration fails a critical
|
|
332
|
+
check instead of being silently double-corrected. `contour_lines` also ships
|
|
333
|
+
with Whitebox's default smoothing filter OFF, because a contour whose vertices
|
|
334
|
+
have been moved to look better no longer passes through the elevation it
|
|
335
|
+
names — smoothing is available, recorded in the manifest, and demotes that
|
|
336
|
+
check to non-critical, because then the output is a drawing rather than a
|
|
337
|
+
measurement.
|
|
338
|
+
|
|
339
|
+
- **A search whose facets leave nothing now says which declaration did it.**
|
|
340
|
+
Zero surviving candidates fell into the hand-over branch and came back as
|
|
341
|
+
`status: "choose"` with an empty list and the sentence "0 operations survive
|
|
342
|
+
what you declared, which is few enough to read" — nonsense as prose, and read
|
|
343
|
+
by an agent as *MapSmith cannot do this*. It was false in the case that found
|
|
344
|
+
it: "how much land is in each of these parcels" with `produces="answer"` left
|
|
345
|
+
nothing, while `measure_area` computes exactly that and declares
|
|
346
|
+
`dataset:vector` because it writes the areas into a column. That case is now
|
|
347
|
+
`status: "none_apply"`, listing each declaration with the number of operations
|
|
348
|
+
that would survive without it, smallest first. Arithmetic, not ranking. Found
|
|
349
|
+
by the discovery log above on its first recorded session.
|
|
350
|
+
|
|
351
|
+
### Changed
|
|
352
|
+
|
|
353
|
+
- **`dataset_inputs` is a new facet, and a new REQUIRED field of the published
|
|
354
|
+
catalogue-entry specification.** How many datasets the caller is holding: 0, 1
|
|
355
|
+
or 2. It exists because the ten operations above broke discovery and it is the
|
|
356
|
+
thing that fixed it — measured, on the 118 independent requests: the surviving
|
|
357
|
+
set for the commonest facet combination went from 26 candidates to 34, over the
|
|
358
|
+
threshold at which the whole set is handed over, and `delivered` fell from 100%
|
|
359
|
+
to 45% with found@3 from 48% to 36%. With arity declared the median set is 9,
|
|
360
|
+
found@3 is 60% and delivered is back to 100%.
|
|
361
|
+
|
|
362
|
+
It is the right *kind* of facet, which is the part that generalises: a fact
|
|
363
|
+
about the caller's own situation rather than a guess about our taxonomy, and
|
|
364
|
+
derivable from each operation's signature so
|
|
365
|
+
`test_the_declared_arity_matches_the_binding` checks the declaration against
|
|
366
|
+
the code instead of trusting it.
|
|
367
|
+
|
|
368
|
+
### Security
|
|
369
|
+
|
|
370
|
+
- **A statement that says `INSTALL` or `LOAD` is now refused in both modes, and
|
|
371
|
+
this is a breaking change for SQL that names an extension.** Until 0.4.0 only
|
|
372
|
+
the *implicit* forms were off, and an audit before this tag used `run_sql` in
|
|
373
|
+
the default mode to install DuckDB's `aws` extension and read the host's real
|
|
374
|
+
cloud credentials back through a tool result. An `INSTALL` is an HTTPS fetch of
|
|
375
|
+
a native binary executed in the server's process, on a statement written by a
|
|
376
|
+
model, so it is not a thing to do by default in either mode.
|
|
377
|
+
|
|
378
|
+
Extensions already loaded keep working, `spatial` included: MapSmith loads it
|
|
379
|
+
through the Python API before the configuration is locked, so no statement has
|
|
380
|
+
to ask for it. **What breaks** is SQL that says `LOAD spatial` or acquires
|
|
381
|
+
another extension. To allow specific ones, name them where the agent cannot
|
|
382
|
+
reach: `MAPSMITH_ALLOW_EXTENSIONS=postgres,azure` in the environment of the
|
|
383
|
+
process that starts the server — named extensions rather than a switch,
|
|
384
|
+
because "allow everything" is how a geoprocessing server ends up holding a
|
|
385
|
+
credential reader.
|
|
386
|
+
|
|
387
|
+
[SECURITY.md](SECURITY.md) carries the account, including why none of the four
|
|
388
|
+
layers that already existed saw it, and the fact that this is the second
|
|
389
|
+
promise in that file the code contradicted. Both were found by auditing before
|
|
390
|
+
a release rather than reported from outside, and both are recorded rather than
|
|
391
|
+
quietly corrected.
|
|
10
392
|
|
|
11
393
|
## [0.3.0] — 2026-08-29
|
|
12
394
|
|
|
@@ -75,7 +457,7 @@ are in `Changed` and one of them affects anybody passing `category=`.
|
|
|
75
457
|
with a tool of its own: `measure_length`, `join_table`, `aggregate_weighted`,
|
|
76
458
|
`parse_coordinates`, `point_on_surface`, `hull_layer`, `validate_geometry`,
|
|
77
459
|
`count_in_polygons`, `focal_statistics` and `extract_streams`. The catalogue is
|
|
78
|
-
at
|
|
460
|
+
at 74 operations (72 available, 2 planned) behind 28 tools.
|
|
79
461
|
- **Overlay and dissolve declare their semantics in the manifest.** Dropped
|
|
80
462
|
lower-dimension pieces from an overlay are named rather than silently absent,
|
|
81
463
|
and a dissolve's aggregation is recorded with the group count verified in
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
cff-version: 1.2.0
|
|
2
|
+
message: "If you use MapSmith in your work, please cite it as below."
|
|
3
|
+
title: "MapSmith: geospatial processing for AI agents, with verifiable provenance"
|
|
4
|
+
type: software
|
|
5
|
+
authors:
|
|
6
|
+
- name: "The MapSmith project"
|
|
7
|
+
repository-code: "https://github.com/mapsmith-ai/MapSmith"
|
|
8
|
+
url: "https://mapsmith.dev"
|
|
9
|
+
license: AGPL-3.0-or-later
|
|
10
|
+
version: "0.4.0"
|
|
11
|
+
date-released: "2026-08-31"
|
|
12
|
+
keywords:
|
|
13
|
+
- GIS
|
|
14
|
+
- geospatial
|
|
15
|
+
- provenance
|
|
16
|
+
- reproducibility
|
|
17
|
+
- Model Context Protocol
|
|
18
|
+
- MCP
|
|
19
|
+
abstract: >-
|
|
20
|
+
An MCP server that gives AI agents professional geoprocessing under a rule
|
|
21
|
+
the agent cannot bend: every geometry and every number comes from a real
|
|
22
|
+
engine run, never from the model. Each operation that writes a dataset also
|
|
23
|
+
writes a manifest beside it — the inputs and their hashes, the exact
|
|
24
|
+
parameters, the coordinate-system decisions and the reason for each, the
|
|
25
|
+
engine and its version, and the deterministic checks that ran — and the
|
|
26
|
+
manifest is written before a failed check raises, so the audit trail survives
|
|
27
|
+
the error. Capabilities are reached through a searchable catalogue rather
|
|
28
|
+
than a wall of tools, because a caller's ability to pick the right tool
|
|
29
|
+
degrades long before a catalogue does. A manifest records what was done; it
|
|
30
|
+
does not certify that what was done was right, and MapSmith says so in its
|
|
31
|
+
own documentation rather than leaving a reader to find out.
|