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.
Files changed (156) hide show
  1. {mapsmith-0.8.0 → mapsmith-0.9.0}/.zenodo.json +1 -1
  2. {mapsmith-0.8.0 → mapsmith-0.9.0}/CHANGELOG.md +104 -0
  3. {mapsmith-0.8.0 → mapsmith-0.9.0}/CITATION.cff +2 -2
  4. {mapsmith-0.8.0 → mapsmith-0.9.0}/PKG-INFO +90 -74
  5. {mapsmith-0.8.0 → mapsmith-0.9.0}/README.md +55 -39
  6. {mapsmith-0.8.0 → mapsmith-0.9.0}/SECURITY.md +26 -0
  7. {mapsmith-0.8.0 → mapsmith-0.9.0}/docs/benchmarks.md +9 -4
  8. {mapsmith-0.8.0 → mapsmith-0.9.0}/docs/catalog-entry-spec.md +8 -8
  9. {mapsmith-0.8.0 → mapsmith-0.9.0}/docs/manifest-vocabulary.md +185 -179
  10. {mapsmith-0.8.0 → mapsmith-0.9.0}/examples/01_verified_geoprocessing.ipynb +24 -24
  11. {mapsmith-0.8.0 → mapsmith-0.9.0}/examples/02_terrain_hydrology.ipynb +36 -36
  12. {mapsmith-0.8.0 → mapsmith-0.9.0}/examples/03_validated_plans.ipynb +23 -23
  13. {mapsmith-0.8.0 → mapsmith-0.9.0}/examples/04_copernicus_terrain_vegetation.ipynb +31 -31
  14. {mapsmith-0.8.0 → mapsmith-0.9.0}/funding.json +1 -1
  15. {mapsmith-0.8.0 → mapsmith-0.9.0}/pyproject.toml +4 -4
  16. {mapsmith-0.8.0 → mapsmith-0.9.0}/server.json +3 -3
  17. {mapsmith-0.8.0 → mapsmith-0.9.0}/site/index.template.html +8 -8
  18. {mapsmith-0.8.0 → mapsmith-0.9.0}/src/mapsmith/__init__.py +1 -1
  19. {mapsmith-0.8.0 → mapsmith-0.9.0}/src/mapsmith/catalog.py +68 -0
  20. mapsmith-0.9.0/src/mapsmith/datum.py +865 -0
  21. {mapsmith-0.8.0 → mapsmith-0.9.0}/src/mapsmith/engines/linework.py +3 -3
  22. {mapsmith-0.8.0 → mapsmith-0.9.0}/src/mapsmith/engines/network.py +6 -2
  23. {mapsmith-0.8.0 → mapsmith-0.9.0}/src/mapsmith/engines/raster.py +60 -17
  24. {mapsmith-0.8.0 → mapsmith-0.9.0}/src/mapsmith/engines/sampling.py +2 -2
  25. {mapsmith-0.8.0 → mapsmith-0.9.0}/src/mapsmith/engines/summaries.py +2 -2
  26. {mapsmith-0.8.0 → mapsmith-0.9.0}/src/mapsmith/engines/vector.py +831 -59
  27. {mapsmith-0.8.0 → mapsmith-0.9.0}/src/mapsmith/engines/whitebox_engine.py +2 -2
  28. {mapsmith-0.8.0 → mapsmith-0.9.0}/src/mapsmith/plans/registry.py +14 -0
  29. {mapsmith-0.8.0 → mapsmith-0.9.0}/src/mapsmith/provenance.py +142 -9
  30. {mapsmith-0.8.0 → mapsmith-0.9.0}/src/mapsmith/server.py +1 -1
  31. {mapsmith-0.8.0 → mapsmith-0.9.0}/src/mapsmith/verify.py +35 -6
  32. mapsmith-0.9.0/tests/test_apportion_by_area.py +861 -0
  33. {mapsmith-0.8.0 → mapsmith-0.9.0}/tests/test_datum.py +519 -9
  34. mapsmith-0.9.0/tests/test_failure_record_and_earlier_bytes.py +215 -0
  35. {mapsmith-0.8.0 → mapsmith-0.9.0}/tests/test_geometry_declarations.py +4 -0
  36. {mapsmith-0.8.0 → mapsmith-0.9.0}/tests/test_verify.py +3 -0
  37. mapsmith-0.8.0/src/mapsmith/datum.py +0 -308
  38. {mapsmith-0.8.0 → mapsmith-0.9.0}/.dockerignore +0 -0
  39. {mapsmith-0.8.0 → mapsmith-0.9.0}/.gitignore +0 -0
  40. {mapsmith-0.8.0 → mapsmith-0.9.0}/CLAUDE.md +0 -0
  41. {mapsmith-0.8.0 → mapsmith-0.9.0}/CONTRIBUTING.md +0 -0
  42. {mapsmith-0.8.0 → mapsmith-0.9.0}/Dockerfile +0 -0
  43. {mapsmith-0.8.0 → mapsmith-0.9.0}/LICENSE +0 -0
  44. {mapsmith-0.8.0 → mapsmith-0.9.0}/MANIFESTO.md +0 -0
  45. {mapsmith-0.8.0 → mapsmith-0.9.0}/TRADEMARKS.md +0 -0
  46. {mapsmith-0.8.0 → mapsmith-0.9.0}/docker-compose.yml +0 -0
  47. {mapsmith-0.8.0 → mapsmith-0.9.0}/docs/argleton-run.json +0 -0
  48. {mapsmith-0.8.0 → mapsmith-0.9.0}/examples/README.md +0 -0
  49. {mapsmith-0.8.0 → mapsmith-0.9.0}/examples/fixtures/COPERNICUS-NOTICE.md +0 -0
  50. {mapsmith-0.8.0 → mapsmith-0.9.0}/examples/fixtures/fetch_copernicus.py +0 -0
  51. {mapsmith-0.8.0 → mapsmith-0.9.0}/examples/fixtures/monte_baldo_dem.tif +0 -0
  52. {mapsmith-0.8.0 → mapsmith-0.9.0}/examples/fixtures/monte_baldo_s2.tif +0 -0
  53. {mapsmith-0.8.0 → mapsmith-0.9.0}/examples/fixtures/mount_st_helens_dem.tif +0 -0
  54. {mapsmith-0.8.0 → mapsmith-0.9.0}/glama.json +0 -0
  55. {mapsmith-0.8.0 → mapsmith-0.9.0}/schema/operation.schema.json +0 -0
  56. {mapsmith-0.8.0 → mapsmith-0.9.0}/site/build.py +0 -0
  57. {mapsmith-0.8.0 → mapsmith-0.9.0}/src/mapsmith/antimeridian.py +0 -0
  58. {mapsmith-0.8.0 → mapsmith-0.9.0}/src/mapsmith/discovery_log.py +0 -0
  59. {mapsmith-0.8.0 → mapsmith-0.9.0}/src/mapsmith/engines/__init__.py +0 -0
  60. {mapsmith-0.8.0 → mapsmith-0.9.0}/src/mapsmith/engines/dispatch.py +0 -0
  61. {mapsmith-0.8.0 → mapsmith-0.9.0}/src/mapsmith/engines/duckdb_engine.py +0 -0
  62. {mapsmith-0.8.0 → mapsmith-0.9.0}/src/mapsmith/engines/esri.py +0 -0
  63. {mapsmith-0.8.0 → mapsmith-0.9.0}/src/mapsmith/engines/geodesy.py +0 -0
  64. {mapsmith-0.8.0 → mapsmith-0.9.0}/src/mapsmith/engines/sedona_engine.py +0 -0
  65. {mapsmith-0.8.0 → mapsmith-0.9.0}/src/mapsmith/engines/spatial_stats.py +0 -0
  66. {mapsmith-0.8.0 → mapsmith-0.9.0}/src/mapsmith/gdal_policy.py +0 -0
  67. {mapsmith-0.8.0 → mapsmith-0.9.0}/src/mapsmith/grid.py +0 -0
  68. {mapsmith-0.8.0 → mapsmith-0.9.0}/src/mapsmith/jobs.py +0 -0
  69. {mapsmith-0.8.0 → mapsmith-0.9.0}/src/mapsmith/limits.py +0 -0
  70. {mapsmith-0.8.0 → mapsmith-0.9.0}/src/mapsmith/lineage.py +0 -0
  71. {mapsmith-0.8.0 → mapsmith-0.9.0}/src/mapsmith/plans/__init__.py +0 -0
  72. {mapsmith-0.8.0 → mapsmith-0.9.0}/src/mapsmith/plans/executor.py +0 -0
  73. {mapsmith-0.8.0 → mapsmith-0.9.0}/src/mapsmith/plans/models.py +0 -0
  74. {mapsmith-0.8.0 → mapsmith-0.9.0}/src/mapsmith/plans/validator.py +0 -0
  75. {mapsmith-0.8.0 → mapsmith-0.9.0}/src/mapsmith/preview.py +0 -0
  76. {mapsmith-0.8.0 → mapsmith-0.9.0}/src/mapsmith/readers.py +0 -0
  77. {mapsmith-0.8.0 → mapsmith-0.9.0}/src/mapsmith/retrieval.py +0 -0
  78. {mapsmith-0.8.0 → mapsmith-0.9.0}/src/mapsmith/sql_policy.py +0 -0
  79. {mapsmith-0.8.0 → mapsmith-0.9.0}/src/mapsmith/stacks.py +0 -0
  80. {mapsmith-0.8.0 → mapsmith-0.9.0}/src/mapsmith/ui.py +0 -0
  81. {mapsmith-0.8.0 → mapsmith-0.9.0}/src/mapsmith/workspace.py +0 -0
  82. {mapsmith-0.8.0 → mapsmith-0.9.0}/tests/conftest.py +0 -0
  83. {mapsmith-0.8.0 → mapsmith-0.9.0}/tests/data/discovery_queries.json +0 -0
  84. {mapsmith-0.8.0 → mapsmith-0.9.0}/tests/data/manifest-v1.schema.json +0 -0
  85. {mapsmith-0.8.0 → mapsmith-0.9.0}/tests/data/manifest_spec_multi_file_digest.py +0 -0
  86. {mapsmith-0.8.0 → mapsmith-0.9.0}/tests/data/manifest_spec_validator.py +0 -0
  87. {mapsmith-0.8.0 → mapsmith-0.9.0}/tests/test_ambiguous_layer.py +0 -0
  88. {mapsmith-0.8.0 → mapsmith-0.9.0}/tests/test_antimeridian.py +0 -0
  89. {mapsmith-0.8.0 → mapsmith-0.9.0}/tests/test_band_math.py +0 -0
  90. {mapsmith-0.8.0 → mapsmith-0.9.0}/tests/test_catalog.py +0 -0
  91. {mapsmith-0.8.0 → mapsmith-0.9.0}/tests/test_catalog_applicability.py +0 -0
  92. {mapsmith-0.8.0 → mapsmith-0.9.0}/tests/test_catalog_entry_spec.py +0 -0
  93. {mapsmith-0.8.0 → mapsmith-0.9.0}/tests/test_catalog_retrieval.py +0 -0
  94. {mapsmith-0.8.0 → mapsmith-0.9.0}/tests/test_centroid_convert.py +0 -0
  95. {mapsmith-0.8.0 → mapsmith-0.9.0}/tests/test_choice_at_scale.py +0 -0
  96. {mapsmith-0.8.0 → mapsmith-0.9.0}/tests/test_clip_reclassify_raster.py +0 -0
  97. {mapsmith-0.8.0 → mapsmith-0.9.0}/tests/test_contours_and_cost.py +0 -0
  98. {mapsmith-0.8.0 → mapsmith-0.9.0}/tests/test_dashboard.py +0 -0
  99. {mapsmith-0.8.0 → mapsmith-0.9.0}/tests/test_dataset_digest.py +0 -0
  100. {mapsmith-0.8.0 → mapsmith-0.9.0}/tests/test_describe_raster.py +0 -0
  101. {mapsmith-0.8.0 → mapsmith-0.9.0}/tests/test_discovery_contract.py +0 -0
  102. {mapsmith-0.8.0 → mapsmith-0.9.0}/tests/test_discovery_log.py +0 -0
  103. {mapsmith-0.8.0 → mapsmith-0.9.0}/tests/test_discovery_report.py +0 -0
  104. {mapsmith-0.8.0 → mapsmith-0.9.0}/tests/test_duckdb_sandbox.py +0 -0
  105. {mapsmith-0.8.0 → mapsmith-0.9.0}/tests/test_engines.py +0 -0
  106. {mapsmith-0.8.0 → mapsmith-0.9.0}/tests/test_environment.py +0 -0
  107. {mapsmith-0.8.0 → mapsmith-0.9.0}/tests/test_extract_from_gdb.py +0 -0
  108. {mapsmith-0.8.0 → mapsmith-0.9.0}/tests/test_failure_manifest.py +0 -0
  109. {mapsmith-0.8.0 → mapsmith-0.9.0}/tests/test_gdal_policy.py +0 -0
  110. {mapsmith-0.8.0 → mapsmith-0.9.0}/tests/test_geodesy.py +0 -0
  111. {mapsmith-0.8.0 → mapsmith-0.9.0}/tests/test_geoparquet2.py +0 -0
  112. {mapsmith-0.8.0 → mapsmith-0.9.0}/tests/test_grid_registration.py +0 -0
  113. {mapsmith-0.8.0 → mapsmith-0.9.0}/tests/test_limits.py +0 -0
  114. {mapsmith-0.8.0 → mapsmith-0.9.0}/tests/test_lineage.py +0 -0
  115. {mapsmith-0.8.0 → mapsmith-0.9.0}/tests/test_lineage_digest_rules.py +0 -0
  116. {mapsmith-0.8.0 → mapsmith-0.9.0}/tests/test_linework.py +0 -0
  117. {mapsmith-0.8.0 → mapsmith-0.9.0}/tests/test_measure_area.py +0 -0
  118. {mapsmith-0.8.0 → mapsmith-0.9.0}/tests/test_merge_simplify.py +0 -0
  119. {mapsmith-0.8.0 → mapsmith-0.9.0}/tests/test_nearest_explode.py +0 -0
  120. {mapsmith-0.8.0 → mapsmith-0.9.0}/tests/test_network.py +0 -0
  121. {mapsmith-0.8.0 → mapsmith-0.9.0}/tests/test_output_guard.py +0 -0
  122. {mapsmith-0.8.0 → mapsmith-0.9.0}/tests/test_overlay_dissolve.py +0 -0
  123. {mapsmith-0.8.0 → mapsmith-0.9.0}/tests/test_path_containment.py +0 -0
  124. {mapsmith-0.8.0 → mapsmith-0.9.0}/tests/test_plans.py +0 -0
  125. {mapsmith-0.8.0 → mapsmith-0.9.0}/tests/test_preview.py +0 -0
  126. {mapsmith-0.8.0 → mapsmith-0.9.0}/tests/test_published_figures.py +0 -0
  127. {mapsmith-0.8.0 → mapsmith-0.9.0}/tests/test_raster.py +0 -0
  128. {mapsmith-0.8.0 → mapsmith-0.9.0}/tests/test_raster_reproject_bands.py +0 -0
  129. {mapsmith-0.8.0 → mapsmith-0.9.0}/tests/test_redaction.py +0 -0
  130. {mapsmith-0.8.0 → mapsmith-0.9.0}/tests/test_resample_raster.py +0 -0
  131. {mapsmith-0.8.0 → mapsmith-0.9.0}/tests/test_retrieval.py +0 -0
  132. {mapsmith-0.8.0 → mapsmith-0.9.0}/tests/test_retrieval_at_scale.py +0 -0
  133. {mapsmith-0.8.0 → mapsmith-0.9.0}/tests/test_retrieval_degradation.py +0 -0
  134. {mapsmith-0.8.0 → mapsmith-0.9.0}/tests/test_sampling.py +0 -0
  135. {mapsmith-0.8.0 → mapsmith-0.9.0}/tests/test_select_and_extract.py +0 -0
  136. {mapsmith-0.8.0 → mapsmith-0.9.0}/tests/test_showcase.py +0 -0
  137. {mapsmith-0.8.0 → mapsmith-0.9.0}/tests/test_smoke.py +0 -0
  138. {mapsmith-0.8.0 → mapsmith-0.9.0}/tests/test_south_up_and_mixed.py +0 -0
  139. {mapsmith-0.8.0 → mapsmith-0.9.0}/tests/test_spatial_stats.py +0 -0
  140. {mapsmith-0.8.0 → mapsmith-0.9.0}/tests/test_sql_axis_order.py +0 -0
  141. {mapsmith-0.8.0 → mapsmith-0.9.0}/tests/test_sql_credentials.py +0 -0
  142. {mapsmith-0.8.0 → mapsmith-0.9.0}/tests/test_stacks.py +0 -0
  143. {mapsmith-0.8.0 → mapsmith-0.9.0}/tests/test_summaries.py +0 -0
  144. {mapsmith-0.8.0 → mapsmith-0.9.0}/tests/test_summarize_points_in_polygons.py +0 -0
  145. {mapsmith-0.8.0 → mapsmith-0.9.0}/tests/test_terrain_derivatives.py +0 -0
  146. {mapsmith-0.8.0 → mapsmith-0.9.0}/tests/test_tier_a_operations.py +0 -0
  147. {mapsmith-0.8.0 → mapsmith-0.9.0}/tests/test_ui.py +0 -0
  148. {mapsmith-0.8.0 → mapsmith-0.9.0}/tests/test_verification_status.py +0 -0
  149. {mapsmith-0.8.0 → mapsmith-0.9.0}/tests/test_verify_repair.py +0 -0
  150. {mapsmith-0.8.0 → mapsmith-0.9.0}/tests/test_voronoi.py +0 -0
  151. {mapsmith-0.8.0 → mapsmith-0.9.0}/tests/test_whitebox.py +0 -0
  152. {mapsmith-0.8.0 → mapsmith-0.9.0}/tests/test_whitebox_derivatives.py +0 -0
  153. {mapsmith-0.8.0 → mapsmith-0.9.0}/tests/test_whitebox_encoding.py +0 -0
  154. {mapsmith-0.8.0 → mapsmith-0.9.0}/tests/test_whitebox_geographic_refusal.py +0 -0
  155. {mapsmith-0.8.0 → mapsmith-0.9.0}/tests/test_worked_example.py +0 -0
  156. {mapsmith-0.8.0 → mapsmith-0.9.0}/tests/test_workspace.py +0 -0
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "upload_type": "software",
3
3
  "title": "MapSmith: geospatial processing over MCP, with verifiable provenance",
4
- "version": "0.8.0",
4
+ "version": "0.9.0",
5
5
  "creators": [
6
6
  {"name": "The MapSmith project"}
7
7
  ],
@@ -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.8.0"
16
- date-released: "2026-10-05"
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.8.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>=21
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.0
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>=8.0; extra == 'test'
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
  [![PyPI](https://img.shields.io/pypi/v/mapsmith)](https://pypi.org/project/mapsmith/)
50
50
  [![Container](https://img.shields.io/badge/ghcr.io-mapsmith--ai%2Fmapsmith-2496ED?logo=docker&logoColor=white)](https://github.com/mapsmith-ai/MapSmith/pkgs/container/mapsmith)
51
51
  [![MCP](https://img.shields.io/badge/Model_Context_Protocol-server-654FF0)](https://modelcontextprotocol.io)
52
- [![License: AGPL-3.0](https://img.shields.io/badge/license-AGPL--3.0-blue)](https://github.com/mapsmith-ai/MapSmith/blob/v0.8.0/LICENSE)
52
+ [![License: AGPL-3.0](https://img.shields.io/badge/license-AGPL--3.0-blue)](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.8.0/docs/manifest-vocabulary.md),
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.8.0/docs/benchmarks.md) whose
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.8.0/examples/) on a real USGS DEM of Mount St. Helens, an
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 77,
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 77 operations to a handful the caller can read; and the CRS column, where every
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/>77 operations &rarr; 27 candidates &rarr; chosen<br/>CRS EPSG:32610<br/>9/9 checks"]
138
+ BUFFER["<b>buffer_layer</b><br/>78 operations &rarr; 27 candidates &rarr; chosen<br/>CRS EPSG:32610<br/>9/9 checks"]
139
139
  PLAN --> BUFFER
140
- NEAR["<b>clip_layer</b><br/>77 operations &rarr; 12 candidates &rarr; chosen<br/>CRS EPSG:4326<br/>12/12 checks"]
140
+ NEAR["<b>clip_layer</b><br/>78 operations &rarr; 13 candidates &rarr; chosen<br/>CRS EPSG:4326<br/>12/12 checks"]
141
141
  BUFFER --> NEAR
142
- HEIGHT["<b>zonal_statistics</b><br/>77 operations &rarr; 2 candidates &rarr; chosen<br/>CRS EPSG:4326<br/>7/7 checks"]
142
+ HEIGHT["<b>zonal_statistics</b><br/>78 operations &rarr; 2 candidates &rarr; chosen<br/>CRS EPSG:4326<br/>7/7 checks"]
143
143
  NEAR --> HEIGHT
144
- AREA["<b>measure_area</b><br/>77 operations &rarr; 24 candidates &rarr; chosen<br/>CRS WGS 84 &#40;ellipsoidal&#41;<br/>10/10 checks"]
144
+ AREA["<b>measure_area</b><br/>78 operations &rarr; 24 candidates &rarr; chosen<br/>CRS WGS 84 &#40;ellipsoidal&#41;<br/>10/10 checks"]
145
145
  HEIGHT --> AREA
146
- FILTER["<b>select_features</b><br/>77 operations &rarr; 24 candidates &rarr; chosen<br/>CRS EPSG:4326<br/>10/10 checks"]
146
+ FILTER["<b>select_features</b><br/>78 operations &rarr; 24 candidates &rarr; 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 77 | `buffer_layer` | 2 |
156
- | “keep only the parcels that fall inside that strip” | vector, dataset:vector, 2 dataset(s), polygon | **12** of 77 | `clip_layer` | 1 |
157
- | “how high is the ground under each of these parcels” | raster, dataset:vector, 2 dataset(s), polygon | **2** of 77 | `zonal_statistics` | 2 |
158
- | “how big is each one on the ground” | vector, dataset:vector, 1 dataset(s), polygon | **24** of 77 | `measure_area` | 1 |
159
- | “drop the ones where the ground is above 120 metres” | vector, dataset:vector, 1 dataset(s), polygon | **24** of 77 | `select_features` | 2 |
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.8.0**, which is what that command installs. When `main` runs ahead of
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. **If you run 0.4.0 to 0.6.1, 0.7.0 and
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.8.0/CHANGELOG.md) says which operations and why. **From 0.7.x, 0.8.0** adds
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.8.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
- - **77 operations, reached through 28 goal-level tools.** The catalogue is the breadth and
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.8.0/MANIFESTO.md).
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 — 77 today, and 52 of them have no tool of their own — and
450
- it is built to hold thousands. (Two of the 77 are marked `planned` and say so when asked:
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 | 77 | 29% | 19% | 29% |
470
- | what data I have | 51 | 35% | 22% | 36% |
471
- | + what I want back | 32 | 45% | 40% | 54% |
472
- | + how many datasets I have | 17 | 54% | 47% | 84% |
473
- | **+ what geometry it holds** | **15** | **61%** | **58%** | **98%** |
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 ten points on words alone and by three at the fullest
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 77. What the column measures is whether the surviving set was small enough to hand
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 last point
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 77 is 12. The distribution is skewed — most requests leave a small set and a few
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.8.0/tests/data/discovery_queries.json) and
558
- [`benchmarks/discovery_report.py`](https://github.com/mapsmith-ai/MapSmith/blob/v0.8.0/benchmarks/discovery_report.py), which recomputes every
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 12 and exceeds 30 for two of them — which is the 98% in the table above,
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.8.0/benchmarks/choice_at_scale.py), every choice in
576
- [`benchmarks/choice_at_scale.json`](https://github.com/mapsmith-ai/MapSmith/blob/v0.8.0/benchmarks/choice_at_scale.json)). Each of the 118 requests
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 | **61%** |
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 77, so for any request whose right answer is one of the 26 added since,
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 61% computed over the 118 would
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 | 75% | 80% |
691
- | 30 | 68% | 57% |
692
- | 77 | 55% | 40% |
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-seven operations, not at eight hundred.
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.8.0/docs/catalog-entry-spec.md), with a normative
747
- [JSON Schema](https://github.com/mapsmith-ai/MapSmith/blob/v0.8.0/schema/operation.schema.json) that every entry validates against in CI. Each field
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
- ![MapSmith's interactive map panel rendered inside Claude Desktop: OSM basemap, buffer and zone layers, and per-layer provenance cards with verification status](https://raw.githubusercontent.com/mapsmith-ai/MapSmith/v0.8.0/docs/images/map-panel.png)
985
+ ![MapSmith's interactive map panel rendered inside Claude Desktop: OSM basemap, buffer and zone layers, and per-layer provenance cards with verification status](https://raw.githubusercontent.com/mapsmith-ai/MapSmith/v0.9.0/docs/images/map-panel.png)
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.8.0/SECURITY.md), including why none of the four
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.8.0/SECURITY.md).
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.8.0/docs/benchmarks.md) reports an A/B on
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.8.0/docs/benchmarks.md) states side by side.
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.8.0/benchmarks/gabench-ab/), including
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.8.0/CHANGELOG.md)).
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.8.0/examples/): verified buffer+clip with
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.8.0/SECURITY.md)).
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.** 77 operations, and a catalogue that tells
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.8.0/docs/benchmarks.md)).
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.8.0/docs/benchmarks.md) say the runtime error message is the information channel that works
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.8.0/SECURITY.md) on why `INSTALL` in a SQL
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.8.0/SECURITY.md) declares outright: the HTTP transport has no authentication today
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.8.0/LICENSE))
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.8.0/funding.json) lists, in the
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.8.0/CHANGELOG.md), how to contribute in
1380
- [CONTRIBUTING.md](https://github.com/mapsmith-ai/MapSmith/blob/v0.8.0/CONTRIBUTING.md), how to report a vulnerability in
1381
- [SECURITY.md](https://github.com/mapsmith-ai/MapSmith/blob/v0.8.0/SECURITY.md). "MapSmith" is a trademark of the MapSmith project — see
1382
- [TRADEMARKS.md](https://github.com/mapsmith-ai/MapSmith/blob/v0.8.0/TRADEMARKS.md). Updates:
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).