mapsmith 0.8.0__tar.gz → 0.9.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.8.0 → mapsmith-0.9.0}/.zenodo.json +1 -1
- {mapsmith-0.8.0 → mapsmith-0.9.0}/CHANGELOG.md +104 -0
- {mapsmith-0.8.0 → mapsmith-0.9.0}/CITATION.cff +2 -2
- {mapsmith-0.8.0 → mapsmith-0.9.0}/PKG-INFO +90 -74
- {mapsmith-0.8.0 → mapsmith-0.9.0}/README.md +55 -39
- {mapsmith-0.8.0 → mapsmith-0.9.0}/SECURITY.md +26 -0
- {mapsmith-0.8.0 → mapsmith-0.9.0}/docs/benchmarks.md +9 -4
- {mapsmith-0.8.0 → mapsmith-0.9.0}/docs/catalog-entry-spec.md +8 -8
- {mapsmith-0.8.0 → mapsmith-0.9.0}/docs/manifest-vocabulary.md +185 -179
- {mapsmith-0.8.0 → mapsmith-0.9.0}/examples/01_verified_geoprocessing.ipynb +24 -24
- {mapsmith-0.8.0 → mapsmith-0.9.0}/examples/02_terrain_hydrology.ipynb +36 -36
- {mapsmith-0.8.0 → mapsmith-0.9.0}/examples/03_validated_plans.ipynb +23 -23
- {mapsmith-0.8.0 → mapsmith-0.9.0}/examples/04_copernicus_terrain_vegetation.ipynb +31 -31
- {mapsmith-0.8.0 → mapsmith-0.9.0}/funding.json +1 -1
- {mapsmith-0.8.0 → mapsmith-0.9.0}/pyproject.toml +4 -4
- {mapsmith-0.8.0 → mapsmith-0.9.0}/server.json +3 -3
- {mapsmith-0.8.0 → mapsmith-0.9.0}/site/index.template.html +8 -8
- {mapsmith-0.8.0 → mapsmith-0.9.0}/src/mapsmith/__init__.py +1 -1
- {mapsmith-0.8.0 → mapsmith-0.9.0}/src/mapsmith/catalog.py +68 -0
- mapsmith-0.9.0/src/mapsmith/datum.py +865 -0
- {mapsmith-0.8.0 → mapsmith-0.9.0}/src/mapsmith/engines/linework.py +3 -3
- {mapsmith-0.8.0 → mapsmith-0.9.0}/src/mapsmith/engines/network.py +6 -2
- {mapsmith-0.8.0 → mapsmith-0.9.0}/src/mapsmith/engines/raster.py +60 -17
- {mapsmith-0.8.0 → mapsmith-0.9.0}/src/mapsmith/engines/sampling.py +2 -2
- {mapsmith-0.8.0 → mapsmith-0.9.0}/src/mapsmith/engines/summaries.py +2 -2
- {mapsmith-0.8.0 → mapsmith-0.9.0}/src/mapsmith/engines/vector.py +831 -59
- {mapsmith-0.8.0 → mapsmith-0.9.0}/src/mapsmith/engines/whitebox_engine.py +2 -2
- {mapsmith-0.8.0 → mapsmith-0.9.0}/src/mapsmith/plans/registry.py +14 -0
- {mapsmith-0.8.0 → mapsmith-0.9.0}/src/mapsmith/provenance.py +142 -9
- {mapsmith-0.8.0 → mapsmith-0.9.0}/src/mapsmith/server.py +1 -1
- {mapsmith-0.8.0 → mapsmith-0.9.0}/src/mapsmith/verify.py +35 -6
- mapsmith-0.9.0/tests/test_apportion_by_area.py +861 -0
- {mapsmith-0.8.0 → mapsmith-0.9.0}/tests/test_datum.py +519 -9
- mapsmith-0.9.0/tests/test_failure_record_and_earlier_bytes.py +215 -0
- {mapsmith-0.8.0 → mapsmith-0.9.0}/tests/test_geometry_declarations.py +4 -0
- {mapsmith-0.8.0 → mapsmith-0.9.0}/tests/test_verify.py +3 -0
- mapsmith-0.8.0/src/mapsmith/datum.py +0 -308
- {mapsmith-0.8.0 → mapsmith-0.9.0}/.dockerignore +0 -0
- {mapsmith-0.8.0 → mapsmith-0.9.0}/.gitignore +0 -0
- {mapsmith-0.8.0 → mapsmith-0.9.0}/CLAUDE.md +0 -0
- {mapsmith-0.8.0 → mapsmith-0.9.0}/CONTRIBUTING.md +0 -0
- {mapsmith-0.8.0 → mapsmith-0.9.0}/Dockerfile +0 -0
- {mapsmith-0.8.0 → mapsmith-0.9.0}/LICENSE +0 -0
- {mapsmith-0.8.0 → mapsmith-0.9.0}/MANIFESTO.md +0 -0
- {mapsmith-0.8.0 → mapsmith-0.9.0}/TRADEMARKS.md +0 -0
- {mapsmith-0.8.0 → mapsmith-0.9.0}/docker-compose.yml +0 -0
- {mapsmith-0.8.0 → mapsmith-0.9.0}/docs/argleton-run.json +0 -0
- {mapsmith-0.8.0 → mapsmith-0.9.0}/examples/README.md +0 -0
- {mapsmith-0.8.0 → mapsmith-0.9.0}/examples/fixtures/COPERNICUS-NOTICE.md +0 -0
- {mapsmith-0.8.0 → mapsmith-0.9.0}/examples/fixtures/fetch_copernicus.py +0 -0
- {mapsmith-0.8.0 → mapsmith-0.9.0}/examples/fixtures/monte_baldo_dem.tif +0 -0
- {mapsmith-0.8.0 → mapsmith-0.9.0}/examples/fixtures/monte_baldo_s2.tif +0 -0
- {mapsmith-0.8.0 → mapsmith-0.9.0}/examples/fixtures/mount_st_helens_dem.tif +0 -0
- {mapsmith-0.8.0 → mapsmith-0.9.0}/glama.json +0 -0
- {mapsmith-0.8.0 → mapsmith-0.9.0}/schema/operation.schema.json +0 -0
- {mapsmith-0.8.0 → mapsmith-0.9.0}/site/build.py +0 -0
- {mapsmith-0.8.0 → mapsmith-0.9.0}/src/mapsmith/antimeridian.py +0 -0
- {mapsmith-0.8.0 → mapsmith-0.9.0}/src/mapsmith/discovery_log.py +0 -0
- {mapsmith-0.8.0 → mapsmith-0.9.0}/src/mapsmith/engines/__init__.py +0 -0
- {mapsmith-0.8.0 → mapsmith-0.9.0}/src/mapsmith/engines/dispatch.py +0 -0
- {mapsmith-0.8.0 → mapsmith-0.9.0}/src/mapsmith/engines/duckdb_engine.py +0 -0
- {mapsmith-0.8.0 → mapsmith-0.9.0}/src/mapsmith/engines/esri.py +0 -0
- {mapsmith-0.8.0 → mapsmith-0.9.0}/src/mapsmith/engines/geodesy.py +0 -0
- {mapsmith-0.8.0 → mapsmith-0.9.0}/src/mapsmith/engines/sedona_engine.py +0 -0
- {mapsmith-0.8.0 → mapsmith-0.9.0}/src/mapsmith/engines/spatial_stats.py +0 -0
- {mapsmith-0.8.0 → mapsmith-0.9.0}/src/mapsmith/gdal_policy.py +0 -0
- {mapsmith-0.8.0 → mapsmith-0.9.0}/src/mapsmith/grid.py +0 -0
- {mapsmith-0.8.0 → mapsmith-0.9.0}/src/mapsmith/jobs.py +0 -0
- {mapsmith-0.8.0 → mapsmith-0.9.0}/src/mapsmith/limits.py +0 -0
- {mapsmith-0.8.0 → mapsmith-0.9.0}/src/mapsmith/lineage.py +0 -0
- {mapsmith-0.8.0 → mapsmith-0.9.0}/src/mapsmith/plans/__init__.py +0 -0
- {mapsmith-0.8.0 → mapsmith-0.9.0}/src/mapsmith/plans/executor.py +0 -0
- {mapsmith-0.8.0 → mapsmith-0.9.0}/src/mapsmith/plans/models.py +0 -0
- {mapsmith-0.8.0 → mapsmith-0.9.0}/src/mapsmith/plans/validator.py +0 -0
- {mapsmith-0.8.0 → mapsmith-0.9.0}/src/mapsmith/preview.py +0 -0
- {mapsmith-0.8.0 → mapsmith-0.9.0}/src/mapsmith/readers.py +0 -0
- {mapsmith-0.8.0 → mapsmith-0.9.0}/src/mapsmith/retrieval.py +0 -0
- {mapsmith-0.8.0 → mapsmith-0.9.0}/src/mapsmith/sql_policy.py +0 -0
- {mapsmith-0.8.0 → mapsmith-0.9.0}/src/mapsmith/stacks.py +0 -0
- {mapsmith-0.8.0 → mapsmith-0.9.0}/src/mapsmith/ui.py +0 -0
- {mapsmith-0.8.0 → mapsmith-0.9.0}/src/mapsmith/workspace.py +0 -0
- {mapsmith-0.8.0 → mapsmith-0.9.0}/tests/conftest.py +0 -0
- {mapsmith-0.8.0 → mapsmith-0.9.0}/tests/data/discovery_queries.json +0 -0
- {mapsmith-0.8.0 → mapsmith-0.9.0}/tests/data/manifest-v1.schema.json +0 -0
- {mapsmith-0.8.0 → mapsmith-0.9.0}/tests/data/manifest_spec_multi_file_digest.py +0 -0
- {mapsmith-0.8.0 → mapsmith-0.9.0}/tests/data/manifest_spec_validator.py +0 -0
- {mapsmith-0.8.0 → mapsmith-0.9.0}/tests/test_ambiguous_layer.py +0 -0
- {mapsmith-0.8.0 → mapsmith-0.9.0}/tests/test_antimeridian.py +0 -0
- {mapsmith-0.8.0 → mapsmith-0.9.0}/tests/test_band_math.py +0 -0
- {mapsmith-0.8.0 → mapsmith-0.9.0}/tests/test_catalog.py +0 -0
- {mapsmith-0.8.0 → mapsmith-0.9.0}/tests/test_catalog_applicability.py +0 -0
- {mapsmith-0.8.0 → mapsmith-0.9.0}/tests/test_catalog_entry_spec.py +0 -0
- {mapsmith-0.8.0 → mapsmith-0.9.0}/tests/test_catalog_retrieval.py +0 -0
- {mapsmith-0.8.0 → mapsmith-0.9.0}/tests/test_centroid_convert.py +0 -0
- {mapsmith-0.8.0 → mapsmith-0.9.0}/tests/test_choice_at_scale.py +0 -0
- {mapsmith-0.8.0 → mapsmith-0.9.0}/tests/test_clip_reclassify_raster.py +0 -0
- {mapsmith-0.8.0 → mapsmith-0.9.0}/tests/test_contours_and_cost.py +0 -0
- {mapsmith-0.8.0 → mapsmith-0.9.0}/tests/test_dashboard.py +0 -0
- {mapsmith-0.8.0 → mapsmith-0.9.0}/tests/test_dataset_digest.py +0 -0
- {mapsmith-0.8.0 → mapsmith-0.9.0}/tests/test_describe_raster.py +0 -0
- {mapsmith-0.8.0 → mapsmith-0.9.0}/tests/test_discovery_contract.py +0 -0
- {mapsmith-0.8.0 → mapsmith-0.9.0}/tests/test_discovery_log.py +0 -0
- {mapsmith-0.8.0 → mapsmith-0.9.0}/tests/test_discovery_report.py +0 -0
- {mapsmith-0.8.0 → mapsmith-0.9.0}/tests/test_duckdb_sandbox.py +0 -0
- {mapsmith-0.8.0 → mapsmith-0.9.0}/tests/test_engines.py +0 -0
- {mapsmith-0.8.0 → mapsmith-0.9.0}/tests/test_environment.py +0 -0
- {mapsmith-0.8.0 → mapsmith-0.9.0}/tests/test_extract_from_gdb.py +0 -0
- {mapsmith-0.8.0 → mapsmith-0.9.0}/tests/test_failure_manifest.py +0 -0
- {mapsmith-0.8.0 → mapsmith-0.9.0}/tests/test_gdal_policy.py +0 -0
- {mapsmith-0.8.0 → mapsmith-0.9.0}/tests/test_geodesy.py +0 -0
- {mapsmith-0.8.0 → mapsmith-0.9.0}/tests/test_geoparquet2.py +0 -0
- {mapsmith-0.8.0 → mapsmith-0.9.0}/tests/test_grid_registration.py +0 -0
- {mapsmith-0.8.0 → mapsmith-0.9.0}/tests/test_limits.py +0 -0
- {mapsmith-0.8.0 → mapsmith-0.9.0}/tests/test_lineage.py +0 -0
- {mapsmith-0.8.0 → mapsmith-0.9.0}/tests/test_lineage_digest_rules.py +0 -0
- {mapsmith-0.8.0 → mapsmith-0.9.0}/tests/test_linework.py +0 -0
- {mapsmith-0.8.0 → mapsmith-0.9.0}/tests/test_measure_area.py +0 -0
- {mapsmith-0.8.0 → mapsmith-0.9.0}/tests/test_merge_simplify.py +0 -0
- {mapsmith-0.8.0 → mapsmith-0.9.0}/tests/test_nearest_explode.py +0 -0
- {mapsmith-0.8.0 → mapsmith-0.9.0}/tests/test_network.py +0 -0
- {mapsmith-0.8.0 → mapsmith-0.9.0}/tests/test_output_guard.py +0 -0
- {mapsmith-0.8.0 → mapsmith-0.9.0}/tests/test_overlay_dissolve.py +0 -0
- {mapsmith-0.8.0 → mapsmith-0.9.0}/tests/test_path_containment.py +0 -0
- {mapsmith-0.8.0 → mapsmith-0.9.0}/tests/test_plans.py +0 -0
- {mapsmith-0.8.0 → mapsmith-0.9.0}/tests/test_preview.py +0 -0
- {mapsmith-0.8.0 → mapsmith-0.9.0}/tests/test_published_figures.py +0 -0
- {mapsmith-0.8.0 → mapsmith-0.9.0}/tests/test_raster.py +0 -0
- {mapsmith-0.8.0 → mapsmith-0.9.0}/tests/test_raster_reproject_bands.py +0 -0
- {mapsmith-0.8.0 → mapsmith-0.9.0}/tests/test_redaction.py +0 -0
- {mapsmith-0.8.0 → mapsmith-0.9.0}/tests/test_resample_raster.py +0 -0
- {mapsmith-0.8.0 → mapsmith-0.9.0}/tests/test_retrieval.py +0 -0
- {mapsmith-0.8.0 → mapsmith-0.9.0}/tests/test_retrieval_at_scale.py +0 -0
- {mapsmith-0.8.0 → mapsmith-0.9.0}/tests/test_retrieval_degradation.py +0 -0
- {mapsmith-0.8.0 → mapsmith-0.9.0}/tests/test_sampling.py +0 -0
- {mapsmith-0.8.0 → mapsmith-0.9.0}/tests/test_select_and_extract.py +0 -0
- {mapsmith-0.8.0 → mapsmith-0.9.0}/tests/test_showcase.py +0 -0
- {mapsmith-0.8.0 → mapsmith-0.9.0}/tests/test_smoke.py +0 -0
- {mapsmith-0.8.0 → mapsmith-0.9.0}/tests/test_south_up_and_mixed.py +0 -0
- {mapsmith-0.8.0 → mapsmith-0.9.0}/tests/test_spatial_stats.py +0 -0
- {mapsmith-0.8.0 → mapsmith-0.9.0}/tests/test_sql_axis_order.py +0 -0
- {mapsmith-0.8.0 → mapsmith-0.9.0}/tests/test_sql_credentials.py +0 -0
- {mapsmith-0.8.0 → mapsmith-0.9.0}/tests/test_stacks.py +0 -0
- {mapsmith-0.8.0 → mapsmith-0.9.0}/tests/test_summaries.py +0 -0
- {mapsmith-0.8.0 → mapsmith-0.9.0}/tests/test_summarize_points_in_polygons.py +0 -0
- {mapsmith-0.8.0 → mapsmith-0.9.0}/tests/test_terrain_derivatives.py +0 -0
- {mapsmith-0.8.0 → mapsmith-0.9.0}/tests/test_tier_a_operations.py +0 -0
- {mapsmith-0.8.0 → mapsmith-0.9.0}/tests/test_ui.py +0 -0
- {mapsmith-0.8.0 → mapsmith-0.9.0}/tests/test_verification_status.py +0 -0
- {mapsmith-0.8.0 → mapsmith-0.9.0}/tests/test_verify_repair.py +0 -0
- {mapsmith-0.8.0 → mapsmith-0.9.0}/tests/test_voronoi.py +0 -0
- {mapsmith-0.8.0 → mapsmith-0.9.0}/tests/test_whitebox.py +0 -0
- {mapsmith-0.8.0 → mapsmith-0.9.0}/tests/test_whitebox_derivatives.py +0 -0
- {mapsmith-0.8.0 → mapsmith-0.9.0}/tests/test_whitebox_encoding.py +0 -0
- {mapsmith-0.8.0 → mapsmith-0.9.0}/tests/test_whitebox_geographic_refusal.py +0 -0
- {mapsmith-0.8.0 → mapsmith-0.9.0}/tests/test_worked_example.py +0 -0
- {mapsmith-0.8.0 → mapsmith-0.9.0}/tests/test_workspace.py +0 -0
|
@@ -6,6 +6,110 @@ All notable changes to MapSmith are documented here, in the format of
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [0.9.0] - 2026-10-08
|
|
10
|
+
|
|
11
|
+
Records still declare `spec_version` `1.0.0-draft.10`, and no key is renamed.
|
|
12
|
+
Three keys of MapSmith's own are new, each declared with the reason it is not
|
|
13
|
+
a synonym of anything the specification has: `x-mapsmith:areas_measured_on` in
|
|
14
|
+
`crs_decisions`, from `apportion_by_area`, and `x-mapsmith:ballpark_share` and
|
|
15
|
+
`x-mapsmith:outside_area_of_use` inside `crs_decisions.transformation`. Three
|
|
16
|
+
check names are new, all from `apportion_by_area`:
|
|
17
|
+
`x-mapsmith:extensive_total_preserved`, `x-mapsmith:every_source_value_placed`
|
|
18
|
+
and `x-mapsmith:no_zone_gives_more_than_it_holds`. **What
|
|
19
|
+
`reproject_layer` writes changes in two cases**, both described under *Fixed*:
|
|
20
|
+
coordinates outside every published operation's area are no longer moved by
|
|
21
|
+
an operation from elsewhere, and a geographic CRS counted from another
|
|
22
|
+
meridian whose operations are published only for its Greenwich twin
|
|
23
|
+
(EPSG:4806, Monte Mario from Rome) goes through that twin. Shapely 2.1 and
|
|
24
|
+
pyarrow 23.0.1 or later are required.
|
|
25
|
+
|
|
26
|
+
### Added
|
|
27
|
+
|
|
28
|
+
- **`apportion_by_area`: move counts and rates from the zones they are reported
|
|
29
|
+
in to zones that cross them.** Cases by municipality to health districts,
|
|
30
|
+
census-tract population to school catchments. Each column is declared
|
|
31
|
+
extensive (a count, split by share of area) or intensive (a rate, averaged by
|
|
32
|
+
area); a call declaring neither is refused, because splitting a rate or
|
|
33
|
+
averaging a count gives a plausible wrong number. Each count comes with
|
|
34
|
+
`_min` and `_max` that hold however it is distributed inside the source
|
|
35
|
+
zones; rate bounds rest on the same uniformity the estimate assumes, and the
|
|
36
|
+
record says which is which. Zones are cut in the source zones' own CRS,
|
|
37
|
+
where a boundary both layers share is the same line, and a zone counts as
|
|
38
|
+
wholly inside a target by a topological test, not by a ratio of areas.
|
|
39
|
+
Shares are ratios of areas measured on an equal-area surface -- the source
|
|
40
|
+
CRS when it is one, otherwise a plane centred on the zones on their own
|
|
41
|
+
datum, named in the record -- so the pieces of each zone add up to it.
|
|
42
|
+
Whether targets overlap is decided zone by zone, so a few square metres of
|
|
43
|
+
overlapping boundaries are said in the record where they put a zone's value
|
|
44
|
+
in two targets, and a check fails if a zone with no overlap gives out more
|
|
45
|
+
than it holds. Invalid zones are repaired and the repair recorded; a zone
|
|
46
|
+
the repair leaves with no area or with a coordinate that is not a number,
|
|
47
|
+
zones spread over more than a hemisphere, layers writing longitude in two
|
|
48
|
+
conventions, and a target that the source CRS would tear at one of its
|
|
49
|
+
seams (the 180th meridian, a pole: caught by area and by position, whatever
|
|
50
|
+
the units) are refused; targets keep their own CRS; and the counts written
|
|
51
|
+
are checked against the pieces they came from.
|
|
52
|
+
|
|
53
|
+
### Fixed
|
|
54
|
+
|
|
55
|
+
- **`crs_decisions.transformation` now describes what PROJ did to the data's
|
|
56
|
+
own coordinates.** Which operation PROJ selects depends on where a
|
|
57
|
+
coordinate is, and the record asked at one point chosen from the source CRS
|
|
58
|
+
alone -- for EPSG:4326, the middle of the world, in the Gulf of Guinea. It
|
|
59
|
+
was wrong in both directions: WGS 84 data in Europe moved to EPSG:3035, or in
|
|
60
|
+
England to EPSG:27700, was recorded as a ballpark (`is_ballpark: true`) where
|
|
61
|
+
PROJ applied a published 1 m or 2 m shift; and NAD27 coordinates in Italy were
|
|
62
|
+
recorded as a 7 m shift where PROJ applied none -- the error a ballpark record
|
|
63
|
+
exists to report, hidden by it. Every operation that moves an input now hands
|
|
64
|
+
over the moved layer, PROJ is asked what it did at a spread of the data's own
|
|
65
|
+
points, inside and outside the areas of use of the operations it has -- outside
|
|
66
|
+
them it applies no shift for some pairs (NAD27) and stretches a published
|
|
67
|
+
operation for others (DHDN, the Swiss grid), so the answer is PROJ's and not
|
|
68
|
+
inferred. A layer straddling the edge of a datum grid is recorded as a
|
|
69
|
+
ballpark with `x-mapsmith:ballpark_share` saying how many of its vertices got
|
|
70
|
+
no shift; `x-mapsmith:outside_area_of_use` says how many lie outside the area
|
|
71
|
+
of every operation published for the pair, whatever PROJ then did with them;
|
|
72
|
+
a move of no coordinates at all records that nothing was transformed; and a
|
|
73
|
+
check with no data to hand asks where the two CRSs' areas of use overlap.
|
|
74
|
+
- **`reproject_layer` no longer applies an operation outside its area of use,
|
|
75
|
+
and its output changes where it did.** When PROJ's own choice at its probe
|
|
76
|
+
point was a ballpark it substituted the first published operation for the
|
|
77
|
+
pair, wherever the data was: WGS 84 data in Madrid moved into EPSG:3003 by
|
|
78
|
+
the Italian operation came out 84.5 m from PROJ's own result, and WGS 84
|
|
79
|
+
data in Italy moved into EPSG:27700 by the British one, 248 m. Now it
|
|
80
|
+
substitutes only an operation whose area of use contains every vertex, and
|
|
81
|
+
otherwise leaves each point where PROJ does. A geographic CRS counted from
|
|
82
|
+
another meridian whose operations are published only for its Greenwich twin
|
|
83
|
+
goes through that twin -- EPSG:4806 (Monte Mario, Rome) and EPSG:4819 (Nord
|
|
84
|
+
Sahara 1959, Paris) among the 23 tried; NTF Paris, EPSG:4807, has an
|
|
85
|
+
operation of its own and does not change. A station in Piedmont on EPSG:4806
|
|
86
|
+
now lands on the published 4 m operation's answer, where it was 6.3 m off.
|
|
87
|
+
- **`better_available_m` names only an operation that covers the data and is
|
|
88
|
+
not installed.** It reported any published operation for the pair: EPSG:4806
|
|
89
|
+
data in Piedmont was told a 44 m operation needed its grid installed, when
|
|
90
|
+
that operation needs none and is valid in Sardinia only.
|
|
91
|
+
- **A record written on a failing path no longer claims an earlier run's
|
|
92
|
+
output.** With a dataset already at the output path, a run refused by its
|
|
93
|
+
preconditions, or one that crashed before writing, recorded the digest of
|
|
94
|
+
the earlier bytes as its own output -- and a lineage walk by digest then
|
|
95
|
+
attributed them to the inputs of the run that failed -- and its record
|
|
96
|
+
replaced theirs, leaving them with none. Such a record now omits `output`
|
|
97
|
+
and says why, and where a record on disk still describes the bytes at the
|
|
98
|
+
path it is left in place and the failing run's record is written beside it
|
|
99
|
+
as `<output>.failed.provenance.json`, which the error names. Bytes the
|
|
100
|
+
failing run did write keep their digest, including when a check of the
|
|
101
|
+
output crashes after the write.
|
|
102
|
+
|
|
103
|
+
### Changed
|
|
104
|
+
|
|
105
|
+
- **Shapely 2.1 or later is required** (was 2.0): `apportion_by_area` uses its
|
|
106
|
+
maximum inscribed circle to check that a reprojected target still holds its
|
|
107
|
+
own interior.
|
|
108
|
+
- **pyarrow 23.0.1 or later is required** (was 21), the release fixing
|
|
109
|
+
CVE-2026-25087, a use-after-free reading Arrow IPC files; MapSmith reads none,
|
|
110
|
+
and the floor keeps an install at the minimum from carrying it. pytest 9.0.3
|
|
111
|
+
or later for the test extra.
|
|
112
|
+
|
|
9
113
|
## [0.8.0] - 2026-10-05
|
|
10
114
|
|
|
11
115
|
Records declare `spec_version` `1.0.0-draft.10`. `draft.9` defines the digest
|
|
@@ -12,8 +12,8 @@ identifiers:
|
|
|
12
12
|
value: "10.5281/zenodo.22213091"
|
|
13
13
|
description: "Concept DOI — always resolves to the latest release"
|
|
14
14
|
license: AGPL-3.0-or-later
|
|
15
|
-
version: "0.
|
|
16
|
-
date-released: "2026-10-
|
|
15
|
+
version: "0.9.0"
|
|
16
|
+
date-released: "2026-10-08"
|
|
17
17
|
keywords:
|
|
18
18
|
- GIS
|
|
19
19
|
- geospatial
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: mapsmith
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.9.0
|
|
4
4
|
Summary: Professional-grade GIS geoprocessing over MCP, with verifiable provenance
|
|
5
5
|
Project-URL: Homepage, https://github.com/mapsmith-ai/MapSmith
|
|
6
6
|
Project-URL: Repository, https://github.com/mapsmith-ai/MapSmith
|
|
@@ -21,10 +21,10 @@ Requires-Dist: duckdb<2,>=1.5
|
|
|
21
21
|
Requires-Dist: geopandas>=1.1.2
|
|
22
22
|
Requires-Dist: mcp<2,>=1.28.1
|
|
23
23
|
Requires-Dist: model2vec<1,>=0.9
|
|
24
|
-
Requires-Dist: pyarrow>=
|
|
24
|
+
Requires-Dist: pyarrow>=23.0.1
|
|
25
25
|
Requires-Dist: pyogrio>=0.9
|
|
26
26
|
Requires-Dist: pyproj>=3.6
|
|
27
|
-
Requires-Dist: shapely>=2.
|
|
27
|
+
Requires-Dist: shapely>=2.1
|
|
28
28
|
Provides-Extra: postgres
|
|
29
29
|
Requires-Dist: psycopg[binary]>=3.2; extra == 'postgres'
|
|
30
30
|
Provides-Extra: raster
|
|
@@ -35,7 +35,7 @@ Provides-Extra: sedona
|
|
|
35
35
|
Requires-Dist: apache-sedona[db]>=1.9; extra == 'sedona'
|
|
36
36
|
Provides-Extra: test
|
|
37
37
|
Requires-Dist: jsonschema>=4.23; extra == 'test'
|
|
38
|
-
Requires-Dist: pytest>=
|
|
38
|
+
Requires-Dist: pytest>=9.0.3; extra == 'test'
|
|
39
39
|
Requires-Dist: ruff<0.17,>=0.16; extra == 'test'
|
|
40
40
|
Provides-Extra: whitebox
|
|
41
41
|
Requires-Dist: whitebox-workflows<3,>=2.0.6; extra == 'whitebox'
|
|
@@ -49,7 +49,7 @@ Description-Content-Type: text/markdown
|
|
|
49
49
|
[](https://pypi.org/project/mapsmith/)
|
|
50
50
|
[](https://github.com/mapsmith-ai/MapSmith/pkgs/container/mapsmith)
|
|
51
51
|
[](https://modelcontextprotocol.io)
|
|
52
|
-
[](https://github.com/mapsmith-ai/MapSmith/blob/v0.
|
|
52
|
+
[](https://github.com/mapsmith-ai/MapSmith/blob/v0.9.0/LICENSE)
|
|
53
53
|
|
|
54
54
|
**Ask a question about your data. Get an analysis — and the record that proves it.**
|
|
55
55
|
|
|
@@ -88,12 +88,12 @@ MapSmith's private output: JSON Schema, a toolchain-free validator, a conformanc
|
|
|
88
88
|
hundred-line emitter that never imports MapSmith. Records carry `spec_version`, and CI validates
|
|
89
89
|
real MapSmith output against the spec's own validator. The specification is archived and citable
|
|
90
90
|
as [10.5281/zenodo.22205213](https://doi.org/10.5281/zenodo.22205213), a concept DOI that
|
|
91
|
-
resolves to the latest *archived* draft. Records written by 0.8.0 declare `1.0.0-draft.10`, and
|
|
91
|
+
resolves to the latest *archived* draft. Records written by 0.8.0 and 0.9.0 declare `1.0.0-draft.10`, and
|
|
92
92
|
0.7.x's declare `1.0.0-draft.8`; if the DOI shows an earlier one, the archive has not caught up, and the
|
|
93
93
|
[repository](https://github.com/mapsmith-ai/manifest-spec) holds the text they are written against. The names MapSmith puts
|
|
94
94
|
in a record beyond the ones the specification defines — every extension check name, and every
|
|
95
95
|
extension field in `crs_decisions` with the reason it is not a synonym of a key the specification
|
|
96
|
-
already has — are listed in [`docs/manifest-vocabulary.md`](https://github.com/mapsmith-ai/MapSmith/blob/v0.
|
|
96
|
+
already has — are listed in [`docs/manifest-vocabulary.md`](https://github.com/mapsmith-ai/MapSmith/blob/v0.9.0/docs/manifest-vocabulary.md),
|
|
97
97
|
generated from the source rather than maintained by hand.
|
|
98
98
|
|
|
99
99
|
Evidence before promises. A correctness suite in its own organisation,
|
|
@@ -102,9 +102,9 @@ computed on paper before any system runs: **0.00 silent errors, nothing skipped*
|
|
|
102
102
|
0.9032 for the obvious way of writing the same code. The runs from 2026-08-30 to 2026-09-15 said
|
|
103
103
|
0.00 too, and on one trap they were wrong: its truth was wrong in MapSmith's favour for 26 days, the error was ours
|
|
104
104
|
twice over, and the [erratum](https://github.com/argleton/argleton/blob/main/results/README.md#erratum-2026-09-25-trap-024) says so. Every run since 2026-09-26, the current one
|
|
105
|
-
included, is scored against the corrected truth. Getting here cost seven defects the suite sent back, and they are listed. Alongside it: an [A/B on GABench](https://github.com/mapsmith-ai/MapSmith/blob/v0.
|
|
105
|
+
included, is scored against the corrected truth. Getting here cost seven defects the suite sent back, and they are listed. Alongside it: an [A/B on GABench](https://github.com/mapsmith-ai/MapSmith/blob/v0.9.0/docs/benchmarks.md) whose
|
|
106
106
|
headline is a null result — with the analysis that took our own positive number apart —
|
|
107
|
-
[notebooks](https://github.com/mapsmith-ai/MapSmith/tree/v0.
|
|
107
|
+
[notebooks](https://github.com/mapsmith-ai/MapSmith/tree/v0.9.0/examples/) on a real USGS DEM of Mount St. Helens, an
|
|
108
108
|
[in-chat map panel](#see-results-inside-the-chat) that shows the verification status of
|
|
109
109
|
every layer it draws, and a
|
|
110
110
|
[measurement of our own tool discovery](#finding-the-right-operation) that retracted two numbers
|
|
@@ -113,7 +113,7 @@ this page had already published — including the one in the bullet list below.
|
|
|
113
113
|
## A whole analysis, start to finish
|
|
114
114
|
|
|
115
115
|
This is the thing MapSmith is for, and it is not a single tool call. One question — six
|
|
116
|
-
parcels, a river, an elevation grid — becomes a plan of five operations chosen out of
|
|
116
|
+
parcels, a river, an elevation grid — becomes a plan of five operations chosen out of 78,
|
|
117
117
|
validated before anything runs, executed step by step, and recorded: the search that
|
|
118
118
|
narrowed the catalogue, the arguments that mattered, the CRS decision behind each metric
|
|
119
119
|
step, and the checks that ran on every result.
|
|
@@ -124,7 +124,7 @@ plan, reads the manifests, and writes what follows; `tests/test_worked_example.p
|
|
|
124
124
|
page and that script disagree. The position column is BM25's rather than the default engine's,
|
|
125
125
|
because a published figure should not depend on whether a model download succeeded on the machine
|
|
126
126
|
that built the page — the narrowing, which is the point, is identical on both. Two things worth watching: the middle column, where the catalogue
|
|
127
|
-
goes from
|
|
127
|
+
goes from 78 operations to a handful the caller can read; and the CRS column, where every
|
|
128
128
|
metric operation says which coordinate system it moved the data into and why.
|
|
129
129
|
|
|
130
130
|
<!-- worked-example:start -->
|
|
@@ -135,15 +135,15 @@ flowchart TB
|
|
|
135
135
|
ASK --> PLAN{{"plan validated<br/>before anything runs"}}
|
|
136
136
|
PLAN -. "rejected: FORWARD_REFERENCE" .-> BAD["'mask_path' references '$buffer' which runs later — move step 'buffer' before 'near'"]
|
|
137
137
|
BAD:::bad
|
|
138
|
-
BUFFER["<b>buffer_layer</b><br/>
|
|
138
|
+
BUFFER["<b>buffer_layer</b><br/>78 operations → 27 candidates → chosen<br/>CRS EPSG:32610<br/>9/9 checks"]
|
|
139
139
|
PLAN --> BUFFER
|
|
140
|
-
NEAR["<b>clip_layer</b><br/>
|
|
140
|
+
NEAR["<b>clip_layer</b><br/>78 operations → 13 candidates → chosen<br/>CRS EPSG:4326<br/>12/12 checks"]
|
|
141
141
|
BUFFER --> NEAR
|
|
142
|
-
HEIGHT["<b>zonal_statistics</b><br/>
|
|
142
|
+
HEIGHT["<b>zonal_statistics</b><br/>78 operations → 2 candidates → chosen<br/>CRS EPSG:4326<br/>7/7 checks"]
|
|
143
143
|
NEAR --> HEIGHT
|
|
144
|
-
AREA["<b>measure_area</b><br/>
|
|
144
|
+
AREA["<b>measure_area</b><br/>78 operations → 24 candidates → chosen<br/>CRS WGS 84 (ellipsoidal)<br/>10/10 checks"]
|
|
145
145
|
HEIGHT --> AREA
|
|
146
|
-
FILTER["<b>select_features</b><br/>
|
|
146
|
+
FILTER["<b>select_features</b><br/>78 operations → 24 candidates → chosen<br/>CRS EPSG:4326<br/>10/10 checks"]
|
|
147
147
|
AREA --> FILTER
|
|
148
148
|
OUT[["3 parcels, each with elevation and ground area"]]
|
|
149
149
|
FILTER --> OUT
|
|
@@ -152,11 +152,11 @@ flowchart TB
|
|
|
152
152
|
|
|
153
153
|
| what the agent asks for | it declares | candidates | picked | at position |
|
|
154
154
|
|---|---|---|---|---|
|
|
155
|
-
| “everything within one and a half kilometres of the river” | vector, dataset:vector, 1 dataset(s), line | **27** of
|
|
156
|
-
| “keep only the parcels that fall inside that strip” | vector, dataset:vector, 2 dataset(s), polygon | **
|
|
157
|
-
| “how high is the ground under each of these parcels” | raster, dataset:vector, 2 dataset(s), polygon | **2** of
|
|
158
|
-
| “how big is each one on the ground” | vector, dataset:vector, 1 dataset(s), polygon | **24** of
|
|
159
|
-
| “drop the ones where the ground is above 120 metres” | vector, dataset:vector, 1 dataset(s), polygon | **24** of
|
|
155
|
+
| “everything within one and a half kilometres of the river” | vector, dataset:vector, 1 dataset(s), line | **27** of 78 | `buffer_layer` | 2 |
|
|
156
|
+
| “keep only the parcels that fall inside that strip” | vector, dataset:vector, 2 dataset(s), polygon | **13** of 78 | `clip_layer` | 1 |
|
|
157
|
+
| “how high is the ground under each of these parcels” | raster, dataset:vector, 2 dataset(s), polygon | **2** of 78 | `zonal_statistics` | 2 |
|
|
158
|
+
| “how big is each one on the ground” | vector, dataset:vector, 1 dataset(s), polygon | **24** of 78 | `measure_area` | 1 |
|
|
159
|
+
| “drop the ones where the ground is above 120 metres” | vector, dataset:vector, 1 dataset(s), polygon | **24** of 78 | `select_features` | 2 |
|
|
160
160
|
|
|
161
161
|
| step | operation | arguments that mattered | CRS decision, recorded | checks |
|
|
162
162
|
|---|---|---|---|---|
|
|
@@ -266,17 +266,31 @@ or from a terminal: `code --add-mcp '{"name":"mapsmith","command":"uvx","args":[
|
|
|
266
266
|
To check it runs before wiring a client, `uvx mapsmith` starts the server on stdio
|
|
267
267
|
(Ctrl-C to quit) — it speaks MCP, not a CLI, so a silent prompt means it is working.
|
|
268
268
|
|
|
269
|
-
This page describes **0.
|
|
269
|
+
This page describes **0.9.0**, which is what that command installs. When `main` runs ahead of
|
|
270
270
|
the published artifact this paragraph says so and names the difference — a reader should never
|
|
271
|
-
have to find out by calling a tool that is not there. **
|
|
271
|
+
have to find out by calling a tool that is not there. **From 0.8.0, 0.9.0** adds
|
|
272
|
+
`apportion_by_area`, which moves counts and rates from the zones they are reported in to zones
|
|
273
|
+
that cross them, and makes a record's `transformation` describe what PROJ did to the data's own
|
|
274
|
+
coordinates rather than to a point chosen from the CRS. **That changes what `reproject_layer`
|
|
275
|
+
writes in two cases**: coordinates outside every published operation's area are no longer
|
|
276
|
+
moved by an operation from elsewhere, and a datum counted from another meridian whose
|
|
277
|
+
operations are published only for its Greenwich twin -- Monte Mario from Rome, EPSG:4806 --
|
|
278
|
+
goes through that twin. The [changelog](https://github.com/mapsmith-ai/MapSmith/blob/v0.9.0/CHANGELOG.md#090---2026-10-08) names both. **If you run 0.4.0 to 0.6.1, 0.7.0 and
|
|
272
279
|
later change answers you have**: every position read from a point-registered raster
|
|
273
280
|
(`AREA_OR_POINT=Point`, the USGS and Copernicus DEMs) was half a cell off, and the
|
|
274
|
-
[changelog](https://github.com/mapsmith-ai/MapSmith/blob/v0.
|
|
281
|
+
[changelog](https://github.com/mapsmith-ai/MapSmith/blob/v0.9.0/CHANGELOG.md) says which operations and why. **From 0.7.x, 0.8.0** adds
|
|
275
282
|
`cluster_points_by_distance` and a `geometry` facet for the catalogue search, digests a
|
|
276
283
|
shapefile with its attributes and its CRS, reads any single-layer file geodatabase, and fixes
|
|
277
284
|
operations that accepted the wrong geometry and answered anyway (`zonal_statistics` on line
|
|
278
285
|
zones returned their length as a pixel count).
|
|
279
286
|
|
|
287
|
+
**If you read manifests, 0.9.0 renames no key**: records still declare `1.0.0-draft.10`.
|
|
288
|
+
Three keys of MapSmith's own are new: `crs_decisions.x-mapsmith:areas_measured_on`, from
|
|
289
|
+
`apportion_by_area`, and `x-mapsmith:ballpark_share` and `x-mapsmith:outside_area_of_use`
|
|
290
|
+
inside `transformation`; so are three check names, all from `apportion_by_area`. A run that
|
|
291
|
+
fails where an earlier run's dataset still sits writes its record beside the earlier one, as
|
|
292
|
+
`<output>.failed.provenance.json`, instead of replacing it.
|
|
293
|
+
|
|
280
294
|
**If you read manifests, 0.8.0 renames one key**: records declare `1.0.0-draft.10`, and a
|
|
281
295
|
second input's own georeferencing facts, `inputs[].x-mapsmith:environment` on 0.7.x, are
|
|
282
296
|
`inputs[].environment`, the name the specification gives them since `draft.10`. Each input of
|
|
@@ -316,7 +330,7 @@ for it:
|
|
|
316
330
|
```json
|
|
317
331
|
{
|
|
318
332
|
"spec_version": "1.0.0-draft.10",
|
|
319
|
-
"producer": {"name": "mapsmith", "version": "0.
|
|
333
|
+
"producer": {"name": "mapsmith", "version": "0.9.0"},
|
|
320
334
|
"operation": "buffer_layer",
|
|
321
335
|
"parameters": {"distance_meters": 300.0},
|
|
322
336
|
"inputs": [{
|
|
@@ -397,7 +411,7 @@ For the **analysis** rather than the step, `run_operation` with `get_lineage` ta
|
|
|
397
411
|
without the LLM is in there. No AI slop.
|
|
398
412
|
- **The engines compute, the model orchestrates.** Geometry and numbers only ever come
|
|
399
413
|
from deterministic tool executions — never from model output.
|
|
400
|
-
- **
|
|
414
|
+
- **78 operations, reached through 28 goal-level tools.** The catalogue is the breadth and
|
|
401
415
|
the tool list is the choice; it is searchable rather than dumped, because tool-selection
|
|
402
416
|
accuracy degrades once
|
|
403
417
|
a few dozen tools are exposed at once, and fastest when two of them apply to the same
|
|
@@ -408,7 +422,7 @@ For the **analysis** rather than the step, `run_operation` with `get_lineage` ta
|
|
|
408
422
|
- **Model-agnostic infrastructure.** Claude, GPT, Qwen, Kimi, GLM — anything that speaks
|
|
409
423
|
MCP, cloud or local. The leverage is better contracts (typed plans, actionable error
|
|
410
424
|
codes, a searchable catalog), not weights we would have to maintain. See
|
|
411
|
-
[the manifesto](https://github.com/mapsmith-ai/MapSmith/blob/v0.
|
|
425
|
+
[the manifesto](https://github.com/mapsmith-ai/MapSmith/blob/v0.9.0/MANIFESTO.md).
|
|
412
426
|
|
|
413
427
|
## Tools
|
|
414
428
|
|
|
@@ -446,8 +460,8 @@ For the **analysis** rather than the step, `run_operation` with `get_lineage` ta
|
|
|
446
460
|
### Finding the right operation
|
|
447
461
|
|
|
448
462
|
Those are the tools an agent chooses between. Behind them the **catalog** holds every
|
|
449
|
-
operation MapSmith can perform —
|
|
450
|
-
it is built to hold thousands. (Two of the
|
|
463
|
+
operation MapSmith can perform — 78 today, and 53 of them have no tool of their own — and
|
|
464
|
+
it is built to hold thousands. (Two of the 78 are marked `planned` and say so when asked:
|
|
451
465
|
the roadmap is in the catalog on purpose, so an agent can answer "not yet" instead of
|
|
452
466
|
inventing a call.)
|
|
453
467
|
|
|
@@ -466,11 +480,11 @@ was shown this catalog, because a model handed the entry writes a paraphrase of
|
|
|
466
480
|
|
|
467
481
|
| what the caller declares | candidates left | BM25, found@3 | embeddings, found@3 | **right answer in what comes back** |
|
|
468
482
|
|---|---|---|---|---|
|
|
469
|
-
| nothing — words alone |
|
|
470
|
-
| what data I have |
|
|
471
|
-
| + what I want back | 32 |
|
|
472
|
-
| + how many datasets I have | 17 |
|
|
473
|
-
| **+ what geometry it holds** | **15** | **
|
|
483
|
+
| nothing — words alone | 78 | 29% | 18% | 29% |
|
|
484
|
+
| what data I have | 52 | 34% | 21% | 35% |
|
|
485
|
+
| + what I want back | 32 | 44% | 38% | 53% |
|
|
486
|
+
| + how many datasets I have | 17 | 53% | 45% | 84% |
|
|
487
|
+
| **+ what geometry it holds** | **15** | **60%** | **56%** | **98%** |
|
|
474
488
|
|
|
475
489
|
**The last row is new on 2026-09-29, and the row above it shows why.** Thirty-one operations
|
|
476
490
|
now take one vector layer to another, one more than the threshold below, so a caller who
|
|
@@ -493,7 +507,7 @@ published under a sentence promising it could be checked.
|
|
|
493
507
|
The two also differ in a way worth seeing, and this page had it backwards
|
|
494
508
|
until 2026-08-30. It said the embedding engine overtakes BM25 once the facets
|
|
495
509
|
have narrowed. It does not overtake it anywhere: BM25 leads at every row of the
|
|
496
|
-
table above, by
|
|
510
|
+
table above, by eleven points on words alone and by four at the fullest
|
|
497
511
|
declaration, where the entries that survive are told apart by the words that
|
|
498
512
|
distinguish them — which is what `distinguishes` is for, and what an exact term
|
|
499
513
|
either matches or does not. The embedding engine earns its place on the
|
|
@@ -505,7 +519,7 @@ only the table beside it.)*
|
|
|
505
519
|
|
|
506
520
|
The last column is not an accuracy figure — it is a property, and the 98% rather than 100% is
|
|
507
521
|
worth a sentence. The narrowing never drops the right operation: that is asserted per entry and
|
|
508
|
-
holds for all
|
|
522
|
+
holds for all 78. What the column measures is whether the surviving set was small enough to hand
|
|
509
523
|
over WHOLE, and for a handful of requests it still is not, so those fall back to a ranked
|
|
510
524
|
shortlist and the answer can be outside the top three. Ranking decides the order; it does not
|
|
511
525
|
decide membership; and the 2% is the gap between "cannot lose the answer" and "can show you all
|
|
@@ -544,18 +558,18 @@ of comparison this page exists to refuse.)
|
|
|
544
558
|
That is the shape of the trade, and it says when the next facet is due. The figure to watch is
|
|
545
559
|
not found@3 — a ranker will always get worse as the catalogue grows, and it is a hint. It is the
|
|
546
560
|
**average** surviving set at the fullest declaration, the fourth column of that table:
|
|
547
|
-
9 at 51 operations, 14 at 61, 16 at 72, 16 at 74, 16 at 75, 17 at 76, 15 at 77. (The
|
|
548
|
-
is the first with the geometry declared; without it the set would be 17.) When that crosses the threshold, delivery stops being a
|
|
561
|
+
9 at 51 operations, 14 at 61, 16 at 72, 16 at 74, 16 at 75, 17 at 76, 15 at 77, 15 at 78. (The
|
|
562
|
+
point at 77 is the first with the geometry declared; without it the set would be 17.) When that crosses the threshold, delivery stops being a
|
|
549
563
|
property and starts being a ranking again, and the lasting answer is another fact the caller
|
|
550
564
|
already knows. A bigger threshold is an answer only as far as the caller still chooses well
|
|
551
565
|
among more, and that is measured below, not assumed. (It said *median* until 2026-08-29, and published the mean:
|
|
552
|
-
the median at
|
|
566
|
+
the median at 78 is 13. The distribution is skewed — most requests leave a small set and a few
|
|
553
567
|
leave a large one — so the two numbers say different things and the mean is the pessimistic
|
|
554
568
|
one, which is the right one to watch.)
|
|
555
569
|
|
|
556
570
|
The requests, both labels and the harness are all in the repository:
|
|
557
|
-
[`tests/data/discovery_queries.json`](https://github.com/mapsmith-ai/MapSmith/blob/v0.
|
|
558
|
-
[`benchmarks/discovery_report.py`](https://github.com/mapsmith-ai/MapSmith/blob/v0.
|
|
571
|
+
[`tests/data/discovery_queries.json`](https://github.com/mapsmith-ai/MapSmith/blob/v0.9.0/tests/data/discovery_queries.json) and
|
|
572
|
+
[`benchmarks/discovery_report.py`](https://github.com/mapsmith-ai/MapSmith/blob/v0.9.0/benchmarks/discovery_report.py), which recomputes every
|
|
559
573
|
number above from those files with no network and no model — so they can be checked rather than
|
|
560
574
|
believed, and `tests/test_discovery_report.py` fails if this page and the harness disagree. The
|
|
561
575
|
one exception is the 69%: reproducing that needs the model that did the choosing, and the report
|
|
@@ -565,15 +579,15 @@ choices are in the repository, so its figures can be recomputed without the mode
|
|
|
565
579
|
**So it hands over the set instead of picking for you.** At thirty survivors or fewer `list_operations`
|
|
566
580
|
answers with `status: "choose"`: every candidate, ordered as a hint that says it is a hint, each
|
|
567
581
|
carrying the sentence that separates it from its neighbours. Over those 118 requests the surviving
|
|
568
|
-
set has a median of
|
|
582
|
+
set has a median of 13 and exceeds 30 for two of them — which is the 98% in the table above,
|
|
569
583
|
seen from the other side. The payload is about 2,100 tokens, less than one wrong operation costs
|
|
570
584
|
to run and undo.
|
|
571
585
|
|
|
572
586
|
**Thirty is measured, and a larger threshold was tried first.** Raising it was the obvious answer
|
|
573
587
|
to the thirty-first vector-to-vector operation, and models get better at reading long lists, so
|
|
574
588
|
before moving it we measured whether one still chooses as well from more
|
|
575
|
-
([`benchmarks/choice_at_scale.py`](https://github.com/mapsmith-ai/MapSmith/blob/v0.
|
|
576
|
-
[`benchmarks/choice_at_scale.json`](https://github.com/mapsmith-ai/MapSmith/blob/v0.
|
|
589
|
+
([`benchmarks/choice_at_scale.py`](https://github.com/mapsmith-ai/MapSmith/blob/v0.9.0/benchmarks/choice_at_scale.py), every choice in
|
|
590
|
+
[`benchmarks/choice_at_scale.json`](https://github.com/mapsmith-ai/MapSmith/blob/v0.9.0/benchmarks/choice_at_scale.json)). Each of the 118 requests
|
|
577
591
|
was handed over in a set of 30, of 40 and of 50 candidates, filled with the operations the ranker
|
|
578
592
|
finds closest, so the extra ones are the confusable ones; twelve choosers (Claude Opus 5.5) so
|
|
579
593
|
that none saw a request twice; and each request compared with itself.
|
|
@@ -599,7 +613,7 @@ Three measurements say this is the right shape, and the third is the one that se
|
|
|
599
613
|
|
|
600
614
|
| | |
|
|
601
615
|
|---|---|
|
|
602
|
-
| our ranking puts the answer in the top three | **
|
|
616
|
+
| our ranking puts the answer in the top three | **60%** |
|
|
603
617
|
| a model handed the same candidates and asked to *choose* gets its first pick right | **69%** |
|
|
604
618
|
| the two labellers who wrote the ground truth agree **with each other** | **70%** |
|
|
605
619
|
|
|
@@ -619,7 +633,7 @@ adopted wholesale rather than judged one at a time. Two different strengths of e
|
|
|
619
633
|
the file says which is which.
|
|
620
634
|
|
|
621
635
|
**The two model figures are dated: the labels were written on 2026-08-28, against a catalogue of
|
|
622
|
-
51 operations.** It now has
|
|
636
|
+
51 operations.** It now has 78, so for any request whose right answer is one of the 27 added since,
|
|
623
637
|
neither labeller *could* have been right — the answer was not in the catalogue to name. Measured on
|
|
624
638
|
the first four requests a person has answered by hand, two of the four have both labellers wrong,
|
|
625
639
|
and both of those two name operations that did not exist on the 28th. So "both labellers wrong" and
|
|
@@ -630,7 +644,7 @@ against a human answer where there is one.
|
|
|
630
644
|
|
|
631
645
|
All three are over the same 118 requests, which matters: agreement measured over all 155 requests
|
|
632
646
|
in the file is 68%, and the difference is the 21 pairs where both labellers agreed a request was
|
|
633
|
-
unanswerable — true, and the easy half. Quoting that 68% beside a
|
|
647
|
+
unanswerable — true, and the easy half. Quoting that 68% beside a 60% computed over the 118 would
|
|
634
648
|
be comparing two populations, which this table did for half a day.
|
|
635
649
|
|
|
636
650
|
**The last row is a ceiling, not a baseline**, and the second row sits at it rather than below it.
|
|
@@ -687,9 +701,9 @@ catalog grows:
|
|
|
687
701
|
|
|
688
702
|
| catalog size | BM25 found@3 | embeddings found@3 |
|
|
689
703
|
|---|---|---|
|
|
690
|
-
| 10 |
|
|
691
|
-
| 30 |
|
|
692
|
-
|
|
|
704
|
+
| 10 | 73% | 82% |
|
|
705
|
+
| 30 | 65% | 57% |
|
|
706
|
+
| 78 | 55% | 40% |
|
|
693
707
|
|
|
694
708
|
**This table used to say the opposite, and the reversal is the finding.** Published at 10/30/51
|
|
695
709
|
it read 78/83, 47/65, 40/55 — embeddings ahead at every size — and the sentence under it said
|
|
@@ -740,11 +754,11 @@ number of features as the input. Those are structural properties of the operatio
|
|
|
740
754
|
checkable against the code rather than declared by hand, and they separate the pairs a bag of
|
|
741
755
|
words cannot: `spatial_join` from `overlay_layers`, `flow_accumulation` from `extract_streams`.
|
|
742
756
|
That work is not done, and until it is, the honest claim is the measured one: the guarantee above
|
|
743
|
-
holds at seventy-
|
|
757
|
+
holds at seventy-eight operations, not at eight hundred.
|
|
744
758
|
|
|
745
759
|
**How an entry has to be written is a published specification**, not a convention:
|
|
746
|
-
[`docs/catalog-entry-spec.md`](https://github.com/mapsmith-ai/MapSmith/blob/v0.
|
|
747
|
-
[JSON Schema](https://github.com/mapsmith-ai/MapSmith/blob/v0.
|
|
760
|
+
[`docs/catalog-entry-spec.md`](https://github.com/mapsmith-ai/MapSmith/blob/v0.9.0/docs/catalog-entry-spec.md), with a normative
|
|
761
|
+
[JSON Schema](https://github.com/mapsmith-ai/MapSmith/blob/v0.9.0/schema/operation.schema.json) that every entry validates against in CI. Each field
|
|
748
762
|
is there because a measurement said so — including the two that measured to nothing and are
|
|
749
763
|
documented as such, because a spec that only reports what worked is an advertisement.
|
|
750
764
|
|
|
@@ -919,7 +933,9 @@ route to offer.
|
|
|
919
933
|
Every tool that writes a dataset also writes `<output>.provenance.json` beside it and
|
|
920
934
|
verifies its own work — CRS agreement, geometry validity, raster dimensions, count and
|
|
921
935
|
extent invariants — recording the results in the manifest *before* raising anything, so
|
|
922
|
-
the audit trail survives the error.
|
|
936
|
+
the audit trail survives the error. A run that fails before writing, where an earlier
|
|
937
|
+
run's dataset still sits at the path, leaves that run's record in place and writes its own
|
|
938
|
+
as `<output>.failed.provenance.json`; the error names it.
|
|
923
939
|
|
|
924
940
|
Verification runs on the way in as well. Before an operation touches your data, MapSmith
|
|
925
941
|
checks the failures that produce *plausible* junk: an input with no CRS is refused
|
|
@@ -966,7 +982,7 @@ refusal to the remaining raster operations is on the roadmap below.
|
|
|
966
982
|
|
|
967
983
|
## See results inside the chat
|
|
968
984
|
|
|
969
|
-

|
|
970
986
|
|
|
971
987
|
`preview_map` renders your layers on an interactive map panel *inside* the chat — pan,
|
|
972
988
|
zoom, toggle layers, and read each layer's provenance card (operation, engine, and one of
|
|
@@ -1061,7 +1077,7 @@ layer that closes it is the right one: `INSTALL` and `LOAD` in a statement are r
|
|
|
1061
1077
|
outright, because an `INSTALL` is an HTTPS fetch of a native binary run in this process on
|
|
1062
1078
|
SQL a model wrote. Until 0.4.0 only the *implicit* forms were off, and an audit installed
|
|
1063
1079
|
DuckDB's `aws` extension and read this machine's real cloud credentials back through a tool
|
|
1064
|
-
result — the whole story is in [SECURITY.md](https://github.com/mapsmith-ai/MapSmith/blob/v0.
|
|
1080
|
+
result — the whole story is in [SECURITY.md](https://github.com/mapsmith-ai/MapSmith/blob/v0.9.0/SECURITY.md), including why none of the four
|
|
1065
1081
|
existing layers saw it. Extensions already loaded keep working, `spatial` included; to
|
|
1066
1082
|
acquire others, name them where the agent cannot reach:
|
|
1067
1083
|
**`MAPSMITH_ALLOW_EXTENSIONS=postgres,azure`** in the environment of the process that starts
|
|
@@ -1078,7 +1094,7 @@ GDAL parse attacker-chosen bytes in-process. Set **`MAPSMITH_ALLOW_REMOTE=1`** t
|
|
|
1078
1094
|
refuses them regardless, since containment and "fetch whatever URL the model names" cannot
|
|
1079
1095
|
both be true. The test suite asserts every branch by counting requests at a loopback server
|
|
1080
1096
|
(`tests/test_duckdb_sandbox.py`). The full threat model —
|
|
1081
|
-
and what is explicitly *not* covered — is in [SECURITY.md](https://github.com/mapsmith-ai/MapSmith/blob/v0.
|
|
1097
|
+
and what is explicitly *not* covered — is in [SECURITY.md](https://github.com/mapsmith-ai/MapSmith/blob/v0.9.0/SECURITY.md).
|
|
1082
1098
|
|
|
1083
1099
|
Fine print, because it changes how you deploy this: the path jail assumes a single trusted
|
|
1084
1100
|
writer of the workspace filesystem (paths are resolved at check time, so a symlink swap by
|
|
@@ -1127,13 +1143,13 @@ each question rather than its position.
|
|
|
1127
1143
|
## We measured whether this actually helps
|
|
1128
1144
|
|
|
1129
1145
|
Claims about agent performance are cheap, so
|
|
1130
|
-
[**docs/benchmarks.md**](https://github.com/mapsmith-ai/MapSmith/blob/v0.
|
|
1146
|
+
[**docs/benchmarks.md**](https://github.com/mapsmith-ai/MapSmith/blob/v0.9.0/docs/benchmarks.md) reports an A/B on
|
|
1131
1147
|
[GABench](https://github.com/GeoX-Lab/GABench) — 57 executable GIS tasks over a
|
|
1132
1148
|
133-tool server, scored by its deterministic evaluator — where the *only*
|
|
1133
1149
|
variable is whether the agent's typed plan is validated before the solver runs.
|
|
1134
1150
|
GABench is by Bo Yu et al. ([arXiv:2604.13888](https://arxiv.org/abs/2604.13888));
|
|
1135
1151
|
the counts here are of the checkout we ran and differ from the paper's, which
|
|
1136
|
-
[docs/benchmarks.md](https://github.com/mapsmith-ai/MapSmith/blob/v0.
|
|
1152
|
+
[docs/benchmarks.md](https://github.com/mapsmith-ai/MapSmith/blob/v0.9.0/docs/benchmarks.md) states side by side.
|
|
1137
1153
|
|
|
1138
1154
|
The honest headline is a **null result**, on a frontier model and on a small
|
|
1139
1155
|
one, and the interesting part is why:
|
|
@@ -1161,7 +1177,7 @@ accuracy (parity on tool selection, measurably worse on ordering). One of those
|
|
|
1161
1177
|
arms also refuted a conclusion this page had published two arms earlier; the
|
|
1162
1178
|
correction is kept in place rather than edited away.
|
|
1163
1179
|
|
|
1164
|
-
The harness is in [`benchmarks/gabench-ab/`](https://github.com/mapsmith-ai/MapSmith/tree/v0.
|
|
1180
|
+
The harness is in [`benchmarks/gabench-ab/`](https://github.com/mapsmith-ai/MapSmith/tree/v0.9.0/benchmarks/gabench-ab/), including
|
|
1165
1181
|
the `split_analysis.py` that took our own win apart and the
|
|
1166
1182
|
`rep_analysis.py` that bars every delta against a measured noise floor.
|
|
1167
1183
|
|
|
@@ -1190,7 +1206,7 @@ at all but a refusal: a raster and the sidecar beside it declare different geore
|
|
|
1190
1206
|
both readings are GDAL behaving as documented, and the right move is to stop and say so
|
|
1191
1207
|
rather than pick one. That family arrived on 2026-08-31, the morning after the list of
|
|
1192
1208
|
twenty-seven was closed, and it is the reason a manifest can now name its `environment`
|
|
1193
|
-
(see the [changelog](https://github.com/mapsmith-ai/MapSmith/blob/v0.
|
|
1209
|
+
(see the [changelog](https://github.com/mapsmith-ai/MapSmith/blob/v0.9.0/CHANGELOG.md)).
|
|
1194
1210
|
|
|
1195
1211
|
The twenty-ninth family arrived on 2026-09-02 and is the one that reads oddly until you
|
|
1196
1212
|
see it: a survey plot whose easement ring is wound the same way as its outline. A
|
|
@@ -1245,7 +1261,7 @@ suite outside:
|
|
|
1245
1261
|
|
|
1246
1262
|
## Notebook gallery
|
|
1247
1263
|
|
|
1248
|
-
Four executable walkthroughs in [`examples/`](https://github.com/mapsmith-ai/MapSmith/tree/v0.
|
|
1264
|
+
Four executable walkthroughs in [`examples/`](https://github.com/mapsmith-ai/MapSmith/tree/v0.9.0/examples/): verified buffer+clip with
|
|
1249
1265
|
provenance manifests, terrain and hydrology on a real 520×520 USGS DEM of **Mount St.
|
|
1250
1266
|
Helens**, a deliberately wrong plan rejected before execution and then repaired, and
|
|
1251
1267
|
**Copernicus** data on Monte Baldo — a DEM in degrees refused for slope and then reprojected,
|
|
@@ -1284,7 +1300,7 @@ detects it, converts the input first, and discloses the workaround in the manife
|
|
|
1284
1300
|
- **You need an authenticated remote server today.** The Streamable HTTP transport has no
|
|
1285
1301
|
authentication in this release: anyone who can reach the endpoint can run every tool
|
|
1286
1302
|
against everything the process can see. Loopback or a trusted network only
|
|
1287
|
-
([SECURITY.md](https://github.com/mapsmith-ai/MapSmith/blob/v0.
|
|
1303
|
+
([SECURITY.md](https://github.com/mapsmith-ai/MapSmith/blob/v0.9.0/SECURITY.md)).
|
|
1288
1304
|
- **You want a sandbox for arbitrary agent code.** MapSmith confines paths and the SQL
|
|
1289
1305
|
engine; there is no code-execution tool yet, and a path jail is not a container.
|
|
1290
1306
|
- **You need cartography.** No styling, no layouts, no print composer. Outputs are
|
|
@@ -1297,14 +1313,14 @@ detects it, converts the input first, and discloses the workaround in the manife
|
|
|
1297
1313
|
workspace — which is what the container runs with by default. DuckDB's own HTTP and S3
|
|
1298
1314
|
filesystems stay off in every mode, so `read_parquet('s3://…')` does not work even with
|
|
1299
1315
|
the opt-in: fetch the data down first, or run unconfined with remote reads on.
|
|
1300
|
-
- **You want the full breadth of a desktop GIS.**
|
|
1316
|
+
- **You want the full breadth of a desktop GIS.** 78 operations, and a catalogue that tells
|
|
1301
1317
|
the agent what does *not* exist yet rather than letting it improvise. The ~900 QGIS
|
|
1302
1318
|
Processing algorithms are on the roadmap, not in the box.
|
|
1303
1319
|
- **You expect plan validation to make a weak model strong.** Our own A/B says advisory
|
|
1304
1320
|
validation upstream of an improvising solver does approximately nothing at aggregate
|
|
1305
1321
|
level — and the enforced configuration MapSmith ships, measured afterwards, did not beat
|
|
1306
1322
|
it on accuracy either. What enforcement buys is reproducibility
|
|
1307
|
-
([the numbers](https://github.com/mapsmith-ai/MapSmith/blob/v0.
|
|
1323
|
+
([the numbers](https://github.com/mapsmith-ai/MapSmith/blob/v0.9.0/docs/benchmarks.md)).
|
|
1308
1324
|
- **You want us to debug your local geospatial toolchain.** Docker, or `uvx` where the
|
|
1309
1325
|
wheels work, are the only supported paths; a hand-built native GDAL stack is not, on
|
|
1310
1326
|
purpose.
|
|
@@ -1326,7 +1342,7 @@ Next, in the order we intend to do it. The linked items carry a written spec —
|
|
|
1326
1342
|
under Apache-2.0. What it measures in MapSmith, including the seven defects it has sent back,
|
|
1327
1343
|
has [a section of its own](#what-the-suite-found-in-mapsmith). #25 is closed against Argleton
|
|
1328
1344
|
rather than left open here.
|
|
1329
|
-
- [ ] [Agent-loop repair](https://github.com/mapsmith-ai/MapSmith/issues/26): hand verification failures back to the agent as structured, actionable errors, with a bounded retry budget recorded in the manifest. Our [own measurements](https://github.com/mapsmith-ai/MapSmith/blob/v0.
|
|
1345
|
+
- [ ] [Agent-loop repair](https://github.com/mapsmith-ai/MapSmith/issues/26): hand verification failures back to the agent as structured, actionable errors, with a bounded retry budget recorded in the manifest. Our [own measurements](https://github.com/mapsmith-ai/MapSmith/blob/v0.9.0/docs/benchmarks.md) say the runtime error message is the information channel that works
|
|
1330
1346
|
- [ ] [Tool contracts that carry their own rules](https://github.com/mapsmith-ai/MapSmith/issues/27): argument constraints enforced *and* stated, and errors that name the rule rather than only the violation. The one intervention in our benchmark work that moved a metric past its noise floor
|
|
1331
1347
|
- [ ] **A project brief for the requests that are a chain, not a call.** A third of real requests
|
|
1332
1348
|
are not one operation: on the fifty hardest requests in our own set — the ones two model
|
|
@@ -1343,7 +1359,7 @@ Next, in the order we intend to do it. The linked items carry a written spec —
|
|
|
1343
1359
|
validated plan — and every sentence in it has to trace back to a declared field, so a claim that
|
|
1344
1360
|
traces back to nothing is a claim somebody invented, which is checkable by machine. And
|
|
1345
1361
|
**MapSmith installs nothing**: the brief names what is missing and prints the command, and a
|
|
1346
|
-
person runs it. The reason is dated — see [SECURITY.md](https://github.com/mapsmith-ai/MapSmith/blob/v0.
|
|
1362
|
+
person runs it. The reason is dated — see [SECURITY.md](https://github.com/mapsmith-ai/MapSmith/blob/v0.9.0/SECURITY.md) on why `INSTALL` in a SQL
|
|
1347
1363
|
statement is refused in both modes since 0.4.0. A server that installs on authorisation is the
|
|
1348
1364
|
same shape with a consent dialog in front, and the consent comes from somebody who has just read
|
|
1349
1365
|
persuasive prose written by the process asking for it.
|
|
@@ -1351,7 +1367,7 @@ Next, in the order we intend to do it. The linked items carry a written spec —
|
|
|
1351
1367
|
Depends on the item above: the brief reads the rules the tools declare, so there is less to
|
|
1352
1368
|
derive from without them.
|
|
1353
1369
|
- [ ] [Satellite embeddings as a first-class input](https://github.com/mapsmith-ai/MapSmith/issues/24): per-zone embedding vectors (multiband zonal statistics) and similarity rasters against a reference location, over the open [AlphaEarth annual dataset](https://developers.google.com/earth-engine/guides/aef_on_gcs_readme) (CC-BY 4.0 COGs). Deterministic arithmetic on a raster — no model inference in MapSmith — with the tile, year and reference vector recorded in the manifest
|
|
1354
|
-
- [ ] Authenticated remote mode (OAuth on the existing Streamable HTTP transport) and [long-job progress via MCP Tasks](https://github.com/mapsmith-ai/MapSmith/issues/8). This is the item that closes the one limitation [SECURITY.md](https://github.com/mapsmith-ai/MapSmith/blob/v0.
|
|
1370
|
+
- [ ] Authenticated remote mode (OAuth on the existing Streamable HTTP transport) and [long-job progress via MCP Tasks](https://github.com/mapsmith-ai/MapSmith/issues/8). This is the item that closes the one limitation [SECURITY.md](https://github.com/mapsmith-ai/MapSmith/blob/v0.9.0/SECURITY.md) declares outright: the HTTP transport has no authentication today
|
|
1355
1371
|
- [x] Slope and aspect (Whitebox, closed-form tested; geographic-CRS DEMs refused)
|
|
1356
1372
|
- [x] Stream network extraction (Whitebox, from a flow-accumulation grid; the threshold and its unit recorded in the manifest)
|
|
1357
1373
|
- [x] More terrain & hydrology: curvature (six kinds, the kind required because profile and plan answer opposite questions), flow direction (d8/rho8/dinf/fd8, with the direction-code **table** written into the manifest — the engine's own manual documents its default table backwards, so a name would not have been enough), Euclidean distance and IDW interpolation
|
|
@@ -1362,24 +1378,24 @@ Next, in the order we intend to do it. The linked items carry a written spec —
|
|
|
1362
1378
|
|
|
1363
1379
|
## License and project
|
|
1364
1380
|
|
|
1365
|
-
- MapSmith server and engines: **AGPL-3.0-or-later** (see [LICENSE](https://github.com/mapsmith-ai/MapSmith/blob/v0.
|
|
1381
|
+
- MapSmith server and engines: **AGPL-3.0-or-later** (see [LICENSE](https://github.com/mapsmith-ai/MapSmith/blob/v0.9.0/LICENSE))
|
|
1366
1382
|
- Client SDK and tool-schema definitions (future `sdk/`): **Apache-2.0**
|
|
1367
1383
|
|
|
1368
1384
|
You can self-host MapSmith freely, forever. If you modify it and offer it as a service, the
|
|
1369
1385
|
AGPL asks you to share your changes — or [talk to us](mailto:mapsmith@proton.me) about a
|
|
1370
1386
|
commercial license.
|
|
1371
1387
|
|
|
1372
|
-
Nothing here has been funded so far. [`funding.json`](https://github.com/mapsmith-ai/MapSmith/blob/v0.
|
|
1388
|
+
Nothing here has been funded so far. [`funding.json`](https://github.com/mapsmith-ai/MapSmith/blob/v0.9.0/funding.json) lists, in the
|
|
1373
1389
|
[FLOSS/fund](https://fundingjson.org/) format, the two pieces of work money was asked for:
|
|
1374
1390
|
a public suite of geospatial traps with hand-computable answers, and the provenance
|
|
1375
1391
|
manifest as a specification other tools can implement. Both were built anyway, unfunded,
|
|
1376
1392
|
and are archived with DOIs; the entries stay in the file marked inactive, so the estimate
|
|
1377
1393
|
can be read against what came of it.
|
|
1378
1394
|
|
|
1379
|
-
Release notes are in [CHANGELOG.md](https://github.com/mapsmith-ai/MapSmith/blob/v0.
|
|
1380
|
-
[CONTRIBUTING.md](https://github.com/mapsmith-ai/MapSmith/blob/v0.
|
|
1381
|
-
[SECURITY.md](https://github.com/mapsmith-ai/MapSmith/blob/v0.
|
|
1382
|
-
[TRADEMARKS.md](https://github.com/mapsmith-ai/MapSmith/blob/v0.
|
|
1395
|
+
Release notes are in [CHANGELOG.md](https://github.com/mapsmith-ai/MapSmith/blob/v0.9.0/CHANGELOG.md), how to contribute in
|
|
1396
|
+
[CONTRIBUTING.md](https://github.com/mapsmith-ai/MapSmith/blob/v0.9.0/CONTRIBUTING.md), how to report a vulnerability in
|
|
1397
|
+
[SECURITY.md](https://github.com/mapsmith-ai/MapSmith/blob/v0.9.0/SECURITY.md). "MapSmith" is a trademark of the MapSmith project — see
|
|
1398
|
+
[TRADEMARKS.md](https://github.com/mapsmith-ai/MapSmith/blob/v0.9.0/TRADEMARKS.md). Updates:
|
|
1383
1399
|
[LinkedIn](https://www.linkedin.com/company/mapsmith) ·
|
|
1384
1400
|
[X](https://x.com/mapsmith_ai) ·
|
|
1385
1401
|
[Bluesky](https://bsky.app/profile/mapsmith.bsky.social).
|