mapsmith 0.6.1__tar.gz → 0.7.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 (146) hide show
  1. {mapsmith-0.6.1 → mapsmith-0.7.0}/.zenodo.json +1 -1
  2. {mapsmith-0.6.1 → mapsmith-0.7.0}/CHANGELOG.md +143 -0
  3. {mapsmith-0.6.1 → mapsmith-0.7.0}/CITATION.cff +2 -2
  4. {mapsmith-0.6.1 → mapsmith-0.7.0}/PKG-INFO +66 -44
  5. {mapsmith-0.6.1 → mapsmith-0.7.0}/README.md +65 -43
  6. {mapsmith-0.6.1 → mapsmith-0.7.0}/docs/argleton-run.json +14 -11
  7. {mapsmith-0.6.1 → mapsmith-0.7.0}/docs/benchmarks.md +19 -7
  8. {mapsmith-0.6.1 → mapsmith-0.7.0}/docs/catalog-entry-spec.md +6 -6
  9. {mapsmith-0.6.1 → mapsmith-0.7.0}/docs/manifest-vocabulary.md +5 -2
  10. {mapsmith-0.6.1 → mapsmith-0.7.0}/examples/01_verified_geoprocessing.ipynb +24 -24
  11. {mapsmith-0.6.1 → mapsmith-0.7.0}/examples/02_terrain_hydrology.ipynb +37 -37
  12. {mapsmith-0.6.1 → mapsmith-0.7.0}/examples/03_validated_plans.ipynb +23 -23
  13. mapsmith-0.7.0/examples/04_copernicus_terrain_vegetation.ipynb +522 -0
  14. {mapsmith-0.6.1 → mapsmith-0.7.0}/examples/README.md +12 -3
  15. mapsmith-0.7.0/examples/fixtures/COPERNICUS-NOTICE.md +35 -0
  16. mapsmith-0.7.0/examples/fixtures/fetch_copernicus.py +123 -0
  17. mapsmith-0.7.0/examples/fixtures/monte_baldo_dem.tif +0 -0
  18. mapsmith-0.7.0/examples/fixtures/monte_baldo_s2.tif +0 -0
  19. {mapsmith-0.6.1 → mapsmith-0.7.0}/pyproject.toml +1 -1
  20. {mapsmith-0.6.1 → mapsmith-0.7.0}/server.json +3 -3
  21. {mapsmith-0.6.1 → mapsmith-0.7.0}/site/index.template.html +11 -10
  22. {mapsmith-0.6.1 → mapsmith-0.7.0}/src/mapsmith/__init__.py +1 -1
  23. {mapsmith-0.6.1 → mapsmith-0.7.0}/src/mapsmith/catalog.py +94 -29
  24. {mapsmith-0.6.1 → mapsmith-0.7.0}/src/mapsmith/engines/network.py +6 -6
  25. {mapsmith-0.6.1 → mapsmith-0.7.0}/src/mapsmith/engines/raster.py +215 -37
  26. {mapsmith-0.6.1 → mapsmith-0.7.0}/src/mapsmith/engines/sampling.py +414 -43
  27. {mapsmith-0.6.1 → mapsmith-0.7.0}/src/mapsmith/engines/whitebox_engine.py +95 -38
  28. {mapsmith-0.6.1 → mapsmith-0.7.0}/src/mapsmith/gdal_policy.py +17 -0
  29. {mapsmith-0.6.1 → mapsmith-0.7.0}/src/mapsmith/grid.py +63 -84
  30. {mapsmith-0.6.1 → mapsmith-0.7.0}/src/mapsmith/plans/registry.py +5 -2
  31. {mapsmith-0.6.1 → mapsmith-0.7.0}/src/mapsmith/provenance.py +31 -3
  32. {mapsmith-0.6.1 → mapsmith-0.7.0}/src/mapsmith/server.py +14 -1
  33. mapsmith-0.7.0/tests/test_grid_registration.py +530 -0
  34. mapsmith-0.7.0/tests/test_raster.py +358 -0
  35. {mapsmith-0.6.1 → mapsmith-0.7.0}/tests/test_sampling.py +272 -4
  36. {mapsmith-0.6.1 → mapsmith-0.7.0}/tests/test_showcase.py +17 -1
  37. {mapsmith-0.6.1 → mapsmith-0.7.0}/tests/test_stacks.py +3 -3
  38. {mapsmith-0.6.1 → mapsmith-0.7.0}/tests/test_verify.py +21 -2
  39. mapsmith-0.6.1/tests/test_grid_registration.py +0 -449
  40. mapsmith-0.6.1/tests/test_raster.py +0 -94
  41. {mapsmith-0.6.1 → mapsmith-0.7.0}/.dockerignore +0 -0
  42. {mapsmith-0.6.1 → mapsmith-0.7.0}/.gitignore +0 -0
  43. {mapsmith-0.6.1 → mapsmith-0.7.0}/CLAUDE.md +0 -0
  44. {mapsmith-0.6.1 → mapsmith-0.7.0}/CONTRIBUTING.md +0 -0
  45. {mapsmith-0.6.1 → mapsmith-0.7.0}/Dockerfile +0 -0
  46. {mapsmith-0.6.1 → mapsmith-0.7.0}/LICENSE +0 -0
  47. {mapsmith-0.6.1 → mapsmith-0.7.0}/MANIFESTO.md +0 -0
  48. {mapsmith-0.6.1 → mapsmith-0.7.0}/SECURITY.md +0 -0
  49. {mapsmith-0.6.1 → mapsmith-0.7.0}/TRADEMARKS.md +0 -0
  50. {mapsmith-0.6.1 → mapsmith-0.7.0}/docker-compose.yml +0 -0
  51. {mapsmith-0.6.1 → mapsmith-0.7.0}/examples/fixtures/mount_st_helens_dem.tif +0 -0
  52. {mapsmith-0.6.1 → mapsmith-0.7.0}/funding.json +0 -0
  53. {mapsmith-0.6.1 → mapsmith-0.7.0}/glama.json +0 -0
  54. {mapsmith-0.6.1 → mapsmith-0.7.0}/schema/operation.schema.json +0 -0
  55. {mapsmith-0.6.1 → mapsmith-0.7.0}/site/build.py +0 -0
  56. {mapsmith-0.6.1 → mapsmith-0.7.0}/src/mapsmith/antimeridian.py +0 -0
  57. {mapsmith-0.6.1 → mapsmith-0.7.0}/src/mapsmith/datum.py +0 -0
  58. {mapsmith-0.6.1 → mapsmith-0.7.0}/src/mapsmith/discovery_log.py +0 -0
  59. {mapsmith-0.6.1 → mapsmith-0.7.0}/src/mapsmith/engines/__init__.py +0 -0
  60. {mapsmith-0.6.1 → mapsmith-0.7.0}/src/mapsmith/engines/dispatch.py +0 -0
  61. {mapsmith-0.6.1 → mapsmith-0.7.0}/src/mapsmith/engines/duckdb_engine.py +0 -0
  62. {mapsmith-0.6.1 → mapsmith-0.7.0}/src/mapsmith/engines/esri.py +0 -0
  63. {mapsmith-0.6.1 → mapsmith-0.7.0}/src/mapsmith/engines/geodesy.py +0 -0
  64. {mapsmith-0.6.1 → mapsmith-0.7.0}/src/mapsmith/engines/linework.py +0 -0
  65. {mapsmith-0.6.1 → mapsmith-0.7.0}/src/mapsmith/engines/sedona_engine.py +0 -0
  66. {mapsmith-0.6.1 → mapsmith-0.7.0}/src/mapsmith/engines/spatial_stats.py +0 -0
  67. {mapsmith-0.6.1 → mapsmith-0.7.0}/src/mapsmith/engines/summaries.py +0 -0
  68. {mapsmith-0.6.1 → mapsmith-0.7.0}/src/mapsmith/engines/vector.py +0 -0
  69. {mapsmith-0.6.1 → mapsmith-0.7.0}/src/mapsmith/jobs.py +0 -0
  70. {mapsmith-0.6.1 → mapsmith-0.7.0}/src/mapsmith/lineage.py +0 -0
  71. {mapsmith-0.6.1 → mapsmith-0.7.0}/src/mapsmith/plans/__init__.py +0 -0
  72. {mapsmith-0.6.1 → mapsmith-0.7.0}/src/mapsmith/plans/executor.py +0 -0
  73. {mapsmith-0.6.1 → mapsmith-0.7.0}/src/mapsmith/plans/models.py +0 -0
  74. {mapsmith-0.6.1 → mapsmith-0.7.0}/src/mapsmith/plans/validator.py +0 -0
  75. {mapsmith-0.6.1 → mapsmith-0.7.0}/src/mapsmith/preview.py +0 -0
  76. {mapsmith-0.6.1 → mapsmith-0.7.0}/src/mapsmith/readers.py +0 -0
  77. {mapsmith-0.6.1 → mapsmith-0.7.0}/src/mapsmith/retrieval.py +0 -0
  78. {mapsmith-0.6.1 → mapsmith-0.7.0}/src/mapsmith/sql_policy.py +0 -0
  79. {mapsmith-0.6.1 → mapsmith-0.7.0}/src/mapsmith/stacks.py +0 -0
  80. {mapsmith-0.6.1 → mapsmith-0.7.0}/src/mapsmith/ui.py +0 -0
  81. {mapsmith-0.6.1 → mapsmith-0.7.0}/src/mapsmith/verify.py +0 -0
  82. {mapsmith-0.6.1 → mapsmith-0.7.0}/src/mapsmith/workspace.py +0 -0
  83. {mapsmith-0.6.1 → mapsmith-0.7.0}/tests/conftest.py +0 -0
  84. {mapsmith-0.6.1 → mapsmith-0.7.0}/tests/data/discovery_queries.json +0 -0
  85. {mapsmith-0.6.1 → mapsmith-0.7.0}/tests/data/manifest-v1.schema.json +0 -0
  86. {mapsmith-0.6.1 → mapsmith-0.7.0}/tests/data/manifest_spec_validator.py +0 -0
  87. {mapsmith-0.6.1 → mapsmith-0.7.0}/tests/test_ambiguous_layer.py +0 -0
  88. {mapsmith-0.6.1 → mapsmith-0.7.0}/tests/test_antimeridian.py +0 -0
  89. {mapsmith-0.6.1 → mapsmith-0.7.0}/tests/test_band_math.py +0 -0
  90. {mapsmith-0.6.1 → mapsmith-0.7.0}/tests/test_catalog.py +0 -0
  91. {mapsmith-0.6.1 → mapsmith-0.7.0}/tests/test_catalog_applicability.py +0 -0
  92. {mapsmith-0.6.1 → mapsmith-0.7.0}/tests/test_catalog_entry_spec.py +0 -0
  93. {mapsmith-0.6.1 → mapsmith-0.7.0}/tests/test_catalog_retrieval.py +0 -0
  94. {mapsmith-0.6.1 → mapsmith-0.7.0}/tests/test_centroid_convert.py +0 -0
  95. {mapsmith-0.6.1 → mapsmith-0.7.0}/tests/test_clip_reclassify_raster.py +0 -0
  96. {mapsmith-0.6.1 → mapsmith-0.7.0}/tests/test_contours_and_cost.py +0 -0
  97. {mapsmith-0.6.1 → mapsmith-0.7.0}/tests/test_dashboard.py +0 -0
  98. {mapsmith-0.6.1 → mapsmith-0.7.0}/tests/test_datum.py +0 -0
  99. {mapsmith-0.6.1 → mapsmith-0.7.0}/tests/test_describe_raster.py +0 -0
  100. {mapsmith-0.6.1 → mapsmith-0.7.0}/tests/test_discovery_contract.py +0 -0
  101. {mapsmith-0.6.1 → mapsmith-0.7.0}/tests/test_discovery_log.py +0 -0
  102. {mapsmith-0.6.1 → mapsmith-0.7.0}/tests/test_discovery_report.py +0 -0
  103. {mapsmith-0.6.1 → mapsmith-0.7.0}/tests/test_duckdb_sandbox.py +0 -0
  104. {mapsmith-0.6.1 → mapsmith-0.7.0}/tests/test_engines.py +0 -0
  105. {mapsmith-0.6.1 → mapsmith-0.7.0}/tests/test_environment.py +0 -0
  106. {mapsmith-0.6.1 → mapsmith-0.7.0}/tests/test_failure_manifest.py +0 -0
  107. {mapsmith-0.6.1 → mapsmith-0.7.0}/tests/test_gdal_policy.py +0 -0
  108. {mapsmith-0.6.1 → mapsmith-0.7.0}/tests/test_geodesy.py +0 -0
  109. {mapsmith-0.6.1 → mapsmith-0.7.0}/tests/test_geoparquet2.py +0 -0
  110. {mapsmith-0.6.1 → mapsmith-0.7.0}/tests/test_lineage.py +0 -0
  111. {mapsmith-0.6.1 → mapsmith-0.7.0}/tests/test_linework.py +0 -0
  112. {mapsmith-0.6.1 → mapsmith-0.7.0}/tests/test_measure_area.py +0 -0
  113. {mapsmith-0.6.1 → mapsmith-0.7.0}/tests/test_merge_simplify.py +0 -0
  114. {mapsmith-0.6.1 → mapsmith-0.7.0}/tests/test_nearest_explode.py +0 -0
  115. {mapsmith-0.6.1 → mapsmith-0.7.0}/tests/test_network.py +0 -0
  116. {mapsmith-0.6.1 → mapsmith-0.7.0}/tests/test_overlay_dissolve.py +0 -0
  117. {mapsmith-0.6.1 → mapsmith-0.7.0}/tests/test_path_containment.py +0 -0
  118. {mapsmith-0.6.1 → mapsmith-0.7.0}/tests/test_plans.py +0 -0
  119. {mapsmith-0.6.1 → mapsmith-0.7.0}/tests/test_preview.py +0 -0
  120. {mapsmith-0.6.1 → mapsmith-0.7.0}/tests/test_published_figures.py +0 -0
  121. {mapsmith-0.6.1 → mapsmith-0.7.0}/tests/test_raster_reproject_bands.py +0 -0
  122. {mapsmith-0.6.1 → mapsmith-0.7.0}/tests/test_redaction.py +0 -0
  123. {mapsmith-0.6.1 → mapsmith-0.7.0}/tests/test_resample_raster.py +0 -0
  124. {mapsmith-0.6.1 → mapsmith-0.7.0}/tests/test_retrieval.py +0 -0
  125. {mapsmith-0.6.1 → mapsmith-0.7.0}/tests/test_retrieval_at_scale.py +0 -0
  126. {mapsmith-0.6.1 → mapsmith-0.7.0}/tests/test_retrieval_degradation.py +0 -0
  127. {mapsmith-0.6.1 → mapsmith-0.7.0}/tests/test_select_and_extract.py +0 -0
  128. {mapsmith-0.6.1 → mapsmith-0.7.0}/tests/test_smoke.py +0 -0
  129. {mapsmith-0.6.1 → mapsmith-0.7.0}/tests/test_south_up_and_mixed.py +0 -0
  130. {mapsmith-0.6.1 → mapsmith-0.7.0}/tests/test_spatial_stats.py +0 -0
  131. {mapsmith-0.6.1 → mapsmith-0.7.0}/tests/test_sql_axis_order.py +0 -0
  132. {mapsmith-0.6.1 → mapsmith-0.7.0}/tests/test_sql_credentials.py +0 -0
  133. {mapsmith-0.6.1 → mapsmith-0.7.0}/tests/test_summaries.py +0 -0
  134. {mapsmith-0.6.1 → mapsmith-0.7.0}/tests/test_summarize_points_in_polygons.py +0 -0
  135. {mapsmith-0.6.1 → mapsmith-0.7.0}/tests/test_terrain_derivatives.py +0 -0
  136. {mapsmith-0.6.1 → mapsmith-0.7.0}/tests/test_tier_a_operations.py +0 -0
  137. {mapsmith-0.6.1 → mapsmith-0.7.0}/tests/test_ui.py +0 -0
  138. {mapsmith-0.6.1 → mapsmith-0.7.0}/tests/test_verification_status.py +0 -0
  139. {mapsmith-0.6.1 → mapsmith-0.7.0}/tests/test_verify_repair.py +0 -0
  140. {mapsmith-0.6.1 → mapsmith-0.7.0}/tests/test_voronoi.py +0 -0
  141. {mapsmith-0.6.1 → mapsmith-0.7.0}/tests/test_whitebox.py +0 -0
  142. {mapsmith-0.6.1 → mapsmith-0.7.0}/tests/test_whitebox_derivatives.py +0 -0
  143. {mapsmith-0.6.1 → mapsmith-0.7.0}/tests/test_whitebox_encoding.py +0 -0
  144. {mapsmith-0.6.1 → mapsmith-0.7.0}/tests/test_whitebox_geographic_refusal.py +0 -0
  145. {mapsmith-0.6.1 → mapsmith-0.7.0}/tests/test_worked_example.py +0 -0
  146. {mapsmith-0.6.1 → mapsmith-0.7.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.6.1",
4
+ "version": "0.7.0",
5
5
  "creators": [
6
6
  {"name": "The MapSmith project"}
7
7
  ],
@@ -8,6 +8,149 @@ All notable changes to MapSmith are documented here, in the format of
8
8
 
9
9
  Nothing yet.
10
10
 
11
+ ## [0.7.0] - 2026-09-26
12
+
13
+ **This release changes answers earlier releases gave, and that comes first.**
14
+ From 0.4.0 to 0.6.1 every position MapSmith read from a point-registered raster
15
+ (`AREA_OR_POINT=Point` — the USGS elevation products and the Copernicus DEM)
16
+ was half a cell north-west of the sample: GDAL already corrects for the tag, and
17
+ MapSmith corrected a second time. The same premise sat in the correctness suite
18
+ MapSmith is graded by, which scored the wrong answer as a pass; it has published
19
+ an [erratum](https://github.com/argleton/argleton/blob/main/results/README.md#erratum-2026-09-25-trap-024).
20
+ Terrain operations now accept point-registered DEMs, which they refused
21
+ outright. Two capabilities arrive on existing tools — weighted zonal statistics
22
+ and the gradient along a line — with a Copernicus notebook. No tool was added.
23
+ Two records change shape, both below: `elevation_profile` across two CRSs uses
24
+ the specification's `source_crs`/`target_crs`, and terrain operations record
25
+ the raster registration in `crs_decisions`.
26
+
27
+ Records declare `spec_version` `1.0.0-draft.8`, as in 0.6.1.
28
+
29
+ ### Added
30
+
31
+ - **A Copernicus notebook.** `examples/04_copernicus_terrain_vegetation.ipynb`,
32
+ on Monte Baldo: a Copernicus DEM in degrees refused for slope and reprojected,
33
+ a Sentinel-2 NDVI, and the treeline read off both. Two clips ship in
34
+ `examples/fixtures/` under their own licences, with the script that rebuilds
35
+ them. It found both fixes above, and one fact about the data: for the item it
36
+ uses, the Sentinel-2 catalogue declares a reflectance offset of -0.1 that the
37
+ pixels already contain — deep water would read -7% — so the clip declares the
38
+ scale alone, and the script re-checks it every time it runs.
39
+
40
+ - **Weighted zonal statistics.** `zonal_statistics` takes `weights_path`, a
41
+ raster of weights, and `weighted_mean`, `weighted_sum`, `weighted_stdev`,
42
+ `weighted_variance` and `weighted_frac`: the mean heat of a district weighted
43
+ by the population living in each cell, which used to take three operations
44
+ and a division and is a different number from the plain mean wherever people
45
+ are not spread evenly. Both rules were measured on the engine first.
46
+
47
+ One is in the record: a cell with a value and no weight -- nodata or NaN --
48
+ counts with weight 0, because exactextract's own default turns the zone's
49
+ weighted statistics into NaN for a single missing weight. The record carries
50
+ `weight_of_a_cell_with_no_weight: 0.0` and the weights by name
51
+ (`weights_path`) as well as in `inputs[]`, and a non-critical check,
52
+ `x-mapsmith:every_valued_cell_has_a_weight`, names the zones where it
53
+ happened.
54
+
55
+ The other is a refusal, so a record exists only when it held: the weights
56
+ must sit on the value raster's own grid -- same CRS, cells (compared relative
57
+ to the cell size) and registration -- because exactextract accepts a coarser
58
+ grid by repeating each coarse cell whole in every fine one, which inflates a
59
+ weighted sum of counts, and never compares the two coordinate systems. A
60
+ weights raster off the grid, with two bands, or with a negative weight inside
61
+ a zone is refused with how to align it; a negative weight outside every zone
62
+ changes no number and is not.
63
+
64
+ With two rasters in one record, each one's georeferencing facts stay with
65
+ it: the top-level `environment` describes `inputs[0]`, and the weights' are
66
+ on their own entry under `x-mapsmith:environment`. Records of operations
67
+ that read one raster are unchanged. Reached through the same tool; the tool
68
+ count is unchanged.
69
+
70
+ - **The gradient along a line.** `elevation_profile` takes `grade_base_length`
71
+ and, optionally, `grade_threshold_percent`: every point carries
72
+ `grade_percent`, the rise over the window of that length starting there, and
73
+ the result gives per line the steepest window and where it starts, the total
74
+ ascent and descent, and the stretches above the threshold. It closes four
75
+ railway requests that each took a plan -- "the steepest section in percent",
76
+ "the maximum gradient over any 100 m", "does any part exceed 2.5%" -- and
77
+ three traps are decided rather than defaulted: this is not `slope`, which is
78
+ the ground's steepest direction and on a track crossing a hillside at 45
79
+ degrees says 2% where the track climbs 1.41%; the window slides by the
80
+ spacing, so it answers "any 100 m stretch" and not "every 100 m from the
81
+ start"; and the base length has no default and must be a whole number of
82
+ steps. A window never crosses the gap between two parts of a multi-part line
83
+ that do not touch (each point carries `run_index`), nor a point the raster
84
+ could not read; a stretch is one direction of travel, so a climb and the
85
+ descent after it are two. A line whose unit is not the metre needs
86
+ `grade_height_unit`, because the rise comes from the raster's values, which
87
+ state no unit, and metres over US survey feet is 3.28 times too steep. A check
88
+ recomputes every stored grade from the profile as written and fails on a
89
+ grade missing where a window fits, with the count in its detail.
90
+ The new search phrasings moved discovery figures the right way: words alone
91
+ under BM25 at 76 entries from 50% to 55%.
92
+
93
+ ### Fixed
94
+
95
+ - **Every position read from a point-registered raster was half a cell off,
96
+ since 0.4.0.** On a file declaring `AREA_OR_POINT=Point` MapSmith moved each
97
+ sample half a cell north-west of where GDAL puts it: sampling,
98
+ `elevation_profile`, `line_of_sight`, `locate_extreme_cell`, `least_cost_path`,
99
+ and the zones of `zonal_statistics`, which were shifted for the coverage
100
+ computation. The premise was that GDAL reports the tag and leaves the
101
+ geotransform alone. It does the opposite — since RFC 33 the GTiff driver moves
102
+ a PixelIsPoint tie point half a cell on write and on read, so its geotransform
103
+ already centres each cell on its sample — and the correction was made twice.
104
+ Found by a Copernicus DEM, whose samples fall on whole arc-seconds by its own
105
+ documentation: 10.8 by GDAL, 10.79986 by MapSmith. Positions now come from
106
+ GDAL's geotransform whatever the tag says; the tag is still recorded, because
107
+ it says what a value represents. The same premise was in Argleton trap 024,
108
+ which scored MapSmith's wrong answer as correct for 26 days; the suite has
109
+ published an [erratum](https://github.com/argleton/argleton/blob/main/results/README.md#erratum-2026-09-25-trap-024).
110
+ A weights raster is no longer refused for a registration different from the
111
+ values': on the same geotransform the samples are in the same places. And the
112
+ fix no longer depends on the environment: `GTIFF_POINT_GEO_IGNORE=TRUE`, which
113
+ reverts GDAL to its pre-2010 reading, moved every answer half a cell back with
114
+ nothing in the record to say why, so MapSmith now pins it to `FALSE` on import,
115
+ whether or not remote paths are allowed — which also overrides an operator's
116
+ setting in a process that imports `mapsmith` as a library, deliberately.
117
+
118
+ - **The terrain operations refused every point-registered DEM, the Copernicus
119
+ DEM included.** The engine reads the stored tie point as a cell corner, and
120
+ the guard that compares its grid with GDAL's caught the half cell and refused,
121
+ so the numbers were never wrong and twelve operations were unusable on the
122
+ most used global DEM (the eleven that write a raster, and `contour_lines`). The engine now gets a copy declared area-registered on
123
+ GDAL's own geotransform — the same pattern as the TIFF predictor, disclosed in
124
+ the manifest — and the output gets the input's registration back. Their
125
+ manifests record the registration in `crs_decisions`, as the raster writers
126
+ already did, and say when MapSmith retagged an output after the engine wrote
127
+ it, since those bytes are no longer the engine's alone.
128
+ `contour_lines` loses a branch for point-registered DEMs that corrected half a
129
+ cell the wrong way and could not be reached.
130
+
131
+ - **`elevation_profile` measured the spacing in the DEM's units, and on a DEM
132
+ in degrees a 1000 m line came back as one point.** The line was reprojected
133
+ to the raster's CRS before its positions were computed, so `spacing=100`
134
+ meant 100 degrees, and the only trace was a GeoPandas warning on the console.
135
+ Positions are now measured along the line in its own projected CRS -- a
136
+ geographic line was already refused -- and only the sample points go to the
137
+ raster's CRS to be read. **The output is now in the line's CRS**: when the two
138
+ differed it used to be in the raster's, and a plan step reading it must now
139
+ expect the line's (the plan validator does). The record says what was
140
+ transformed in the specification's own keys: `source_crs` the line's,
141
+ `target_crs` the raster's, and the `transformation` the sample points took to
142
+ be read, while `output.crs` says where the result is. It used to list the line
143
+ under `x-mapsmith:inputs_reprojected` from its own CRS, which that key's
144
+ definition makes a contradiction. When line and DEM share a CRS nothing
145
+ changes. Found while building a gradient on top of it.
146
+
147
+ - **`elevation_profile` stopped short of the far end of a line.** A line whose
148
+ length is not a whole number of steps ended on the last whole step: 15 m at a
149
+ step of 4 ended at 12, and a summit in the last 3 m was not in the profile,
150
+ while the docstring and the name of the test said both ends were included --
151
+ and the test asserted the opposite. The far end is now a last, shorter step,
152
+ and the stepping check requires it.
153
+
11
154
  ## [0.6.1] - 2026-09-25
12
155
 
13
156
  A small release on the day of 0.6.0. It moves no manifest key and changes no
@@ -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.6.1"
16
- date-released: "2026-09-25"
15
+ version: "0.7.0"
16
+ date-released: "2026-09-26"
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.6.1
3
+ Version: 0.7.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
@@ -97,9 +97,11 @@ generated from the source rather than maintained by hand.
97
97
 
98
98
  Evidence before promises. A correctness suite in its own organisation,
99
99
  [**Argleton**](https://argleton.org), grades MapSmith on thirty-one traps whose answers are
100
- computed on paper before any system runs: **0.00 silent errors, nothing skipped**, against
101
- 0.9355 for the obvious way of writing the same code. Getting there cost six defects it sent
102
- back here, and they are listed. Alongside it: an [A/B on GABench](docs/benchmarks.md) whose
100
+ computed on paper before any system runs: **0.03 silent errors — one in thirty-one, nothing
101
+ skipped** — against 0.9032 for the obvious way of writing the same code. That one was published
102
+ as a pass: the trap's own truth was wrong in MapSmith's favour for 26 days, the error is ours
103
+ twice over, and the [erratum](https://github.com/argleton/argleton/blob/main/results/README.md#erratum-2026-09-25-trap-024) says so; the fix is on `main`. Getting here cost
104
+ seven defects the suite sent back, and they are listed. Alongside it: an [A/B on GABench](docs/benchmarks.md) whose
103
105
  headline is a null result — with the analysis that took our own positive number apart —
104
106
  [notebooks](examples/) on a real USGS DEM of Mount St. Helens, an
105
107
  [in-chat map panel](#see-results-inside-the-chat) that shows the verification status of
@@ -132,15 +134,15 @@ flowchart TB
132
134
  ASK --> PLAN{{"plan validated<br/>before anything runs"}}
133
135
  PLAN -. "rejected: FORWARD_REFERENCE" .-> BAD["'mask_path' references '$buffer' which runs later — move step 'buffer' before 'near'"]
134
136
  BAD:::bad
135
- BUFFER["<b>buffer_layer</b><br/>76 operations &rarr; 29 candidates &rarr; chosen<br/>CRS EPSG:32610<br/>9/9 checks"]
137
+ BUFFER["<b>buffer_layer</b><br/>76 operations &rarr; 30 candidates &rarr; chosen<br/>CRS EPSG:32610<br/>9/9 checks"]
136
138
  PLAN --> BUFFER
137
139
  NEAR["<b>clip_layer</b><br/>76 operations &rarr; 15 candidates &rarr; chosen<br/>CRS EPSG:4326<br/>12/12 checks"]
138
140
  BUFFER --> NEAR
139
141
  HEIGHT["<b>zonal_statistics</b><br/>76 operations &rarr; 4 candidates &rarr; chosen<br/>CRS EPSG:4326<br/>7/7 checks"]
140
142
  NEAR --> HEIGHT
141
- AREA["<b>measure_area</b><br/>76 operations &rarr; 29 candidates &rarr; chosen<br/>CRS WGS 84 &#40;ellipsoidal&#41;<br/>10/10 checks"]
143
+ AREA["<b>measure_area</b><br/>76 operations &rarr; 30 candidates &rarr; chosen<br/>CRS WGS 84 &#40;ellipsoidal&#41;<br/>10/10 checks"]
142
144
  HEIGHT --> AREA
143
- FILTER["<b>select_features</b><br/>76 operations &rarr; 29 candidates &rarr; chosen<br/>CRS EPSG:4326<br/>10/10 checks"]
145
+ FILTER["<b>select_features</b><br/>76 operations &rarr; 30 candidates &rarr; chosen<br/>CRS EPSG:4326<br/>10/10 checks"]
144
146
  AREA --> FILTER
145
147
  OUT[["3 parcels, each with elevation and ground area"]]
146
148
  FILTER --> OUT
@@ -149,11 +151,11 @@ flowchart TB
149
151
 
150
152
  | what the agent asks for | it declares | candidates | picked | at position |
151
153
  |---|---|---|---|---|
152
- | “everything within one and a half kilometres of the river” | vector, dataset:vector, 1 dataset(s) | **29** of 76 | `buffer_layer` | 2 |
154
+ | “everything within one and a half kilometres of the river” | vector, dataset:vector, 1 dataset(s) | **30** of 76 | `buffer_layer` | 2 |
153
155
  | “keep only the parcels that fall inside that strip” | vector, dataset:vector, 2 dataset(s) | **15** of 76 | `clip_layer` | 1 |
154
156
  | “how high is the ground under each of these parcels” | raster, dataset:vector, 2 dataset(s) | **4** of 76 | `zonal_statistics` | 3 |
155
- | “how big is each one on the ground” | vector, dataset:vector, 1 dataset(s) | **29** of 76 | `measure_area` | 1 |
156
- | “drop the ones where the ground is above 120 metres” | vector, dataset:vector, 1 dataset(s) | **29** of 76 | `select_features` | 2 |
157
+ | “how big is each one on the ground” | vector, dataset:vector, 1 dataset(s) | **30** of 76 | `measure_area` | 1 |
158
+ | “drop the ones where the ground is above 120 metres” | vector, dataset:vector, 1 dataset(s) | **30** of 76 | `select_features` | 2 |
157
159
 
158
160
  | step | operation | arguments that mattered | CRS decision, recorded | checks |
159
161
  |---|---|---|---|---|
@@ -263,12 +265,14 @@ or from a terminal: `code --add-mcp '{"name":"mapsmith","command":"uvx","args":[
263
265
  To check it runs before wiring a client, `uvx mapsmith` starts the server on stdio
264
266
  (Ctrl-C to quit) — it speaks MCP, not a CLI, so a silent prompt means it is working.
265
267
 
266
- This page describes **0.6.1**, which is what that command installs. When `main` runs ahead of
268
+ This page describes **0.7.0**, which is what that command installs. When `main` runs ahead of
267
269
  the published artifact this paragraph says so and names the difference — a reader should never
268
- have to find out by calling a tool that is not there. `main` is at 0.6.1 today, with nothing
269
- unreleased beyond it.
270
+ 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
271
+ changes answers you have**: every position read from a point-registered raster
272
+ (`AREA_OR_POINT=Point`, the USGS and Copernicus DEMs) was half a cell off, and the
273
+ [changelog](CHANGELOG.md) says which operations and why.
270
274
 
271
- **If you read manifests, 0.6.1 moves no key**: records declare `spec_version`
275
+ **If you read manifests, 0.7.0 renames no key**: records declare `spec_version`
272
276
  `1.0.0-draft.8`, two drafts past 0.6.0's `draft.6`, and both drafts only narrowed where a
273
277
  producer may add a key of its own — which MapSmith already did the narrower way. From 0.5.x,
274
278
  0.6.0 renamed `x-mapsmith:round_trip` to `round_trip`; from 0.4.0, 0.5.0 had renamed eight
@@ -301,7 +305,7 @@ for it:
301
305
  ```json
302
306
  {
303
307
  "spec_version": "1.0.0-draft.8",
304
- "producer": {"name": "mapsmith", "version": "0.6.1"},
308
+ "producer": {"name": "mapsmith", "version": "0.7.0"},
305
309
  "operation": "buffer_layer",
306
310
  "parameters": {"distance_meters": 300.0},
307
311
  "inputs": [{
@@ -451,10 +455,10 @@ was shown this catalog, because a model handed the entry writes a paraphrase of
451
455
 
452
456
  | what the caller declares | candidates left | BM25, found@3 | embeddings, found@3 | **right answer in what comes back** |
453
457
  |---|---|---|---|---|
454
- | nothing — words alone | 76 | 29% | 17% | 29% |
455
- | what data I have | 50 | 31% | 22% | 32% |
458
+ | nothing — words alone | 76 | 29% | 18% | 29% |
459
+ | what data I have | 50 | 32% | 21% | 33% |
456
460
  | + what I want back | 31 | 45% | 40% | 53% |
457
- | **+ how many datasets I have** | **16** | **55%** | **53%** | **98%** |
461
+ | **+ how many datasets I have** | **17** | **53%** | **49%** | **98%** |
458
462
 
459
463
  **Two ranking columns, and that is a correction.** This table used to carry one,
460
464
  computed with the default engine — which is the embedding one where its model
@@ -467,18 +471,22 @@ published under a sentence promising it could be checked.
467
471
  The two also differ in a way worth seeing, and this page had it backwards
468
472
  until 2026-08-30. It said the embedding engine overtakes BM25 once the facets
469
473
  have narrowed. It does not overtake it anywhere: BM25 leads at every row of the
470
- table above, by seven to twelve points, and the gap is widest at the fullest
471
- declaration. An exact term either matches or does not, and the entries that
472
- survive a full declaration are told apart by the words that distinguish them —
473
- which is what `distinguishes` is for. The embedding engine earns its place on
474
- the phrasings it has never seen, not on the ranking once the set is small.
474
+ table above, by eleven points on words alone and by four at the fullest
475
+ declaration, where the entries that survive are told apart by the words that
476
+ distinguish them — which is what `distinguishes` is for, and what an exact term
477
+ either matches or does not. The embedding engine earns its place on the
478
+ phrasings it has never seen, not on the ranking once the set is small. *(This
479
+ paragraph said until 2026-09-25 that the gap was seven to twelve points and
480
+ widest at the fullest declaration. It was the other way round with the figures
481
+ then published — narrowest there, at two — and nothing checked the sentence,
482
+ only the table beside it.)*
475
483
 
476
484
  The last column is not an accuracy figure — it is a property, and the 98% rather than 100% is
477
485
  worth a sentence. The narrowing never drops the right operation: that is asserted per entry and
478
486
  holds for all 76. What the column measures is whether the surviving set was small enough to hand
479
487
  over WHOLE, and for a handful of requests it still is not, so those fall back to a ranked
480
488
  shortlist and the answer can be outside the top three. Ranking decides the order; it does not
481
- decide membership; and the 3% is the gap between "cannot lose the answer" and "can show you all
489
+ decide membership; and the 2% is the gap between "cannot lose the answer" and "can show you all
482
490
  of it".
483
491
 
484
492
  **The third row is the scaling wall, and we hit it in one afternoon.** On 2026-08-29 the
@@ -509,10 +517,12 @@ of comparison this page exists to refuse.)
509
517
 
510
518
  **And at 76 the cost showed where the new operation lives.** `summarize_points_in_polygons` takes two vector layers and returns one, which is the busiest corner of the facet space — `count_in_polygons`, `spatial_join` and `clip_layer` are its neighbours — so the bottom row's found@3 fell from 58% to 55% under BM25 while the average surviving set stayed at 16 and *delivered* held at 98%. The operation was added because «average pH by field block» took two calls to answer and so, to somebody searching, did not exist; three points of ranking at the fullest declaration is what that cost, and the guarantee that matters did not move.
511
519
 
520
+ **And when an existing operation gained an optional input, with no operation added.** `zonal_statistics` took a weights raster on 2026-09-25, so it reads two datasets or three, and an arity that varies is declared as none at all — the rule `run_sql` and `merge_layers` follow, because declaring 2 would hide the weighted form from a caller who correctly says they hold three. The price is that it now survives every arity: the average surviving set went from 16 to 17 and the bottom row's found@3 from 55% to 53%, with *delivered* still at 98%. A facet that stops discriminating for one entry costs every search a candidate.
521
+
512
522
  That is the shape of the trade, and it says when the next facet is due. The figure to watch is
513
523
  not found@3 — a ranker will always get worse as the catalogue grows, and it is a hint. It is the
514
524
  **average** surviving set at the fullest declaration, the fourth column of that table:
515
- 9 at 51 operations, 14 at 61, 16 at 72, 16 at 74, 16 at 75, 16 at 76. When that crosses 30, delivery stops being a
525
+ 9 at 51 operations, 14 at 61, 16 at 72, 16 at 74, 16 at 75, 17 at 76. When that crosses 30, delivery stops being a
516
526
  property and starts being a ranking again, and the answer is another fact the caller already
517
527
  knows, not a bigger threshold. (It said *median* until 2026-08-29, and published the mean:
518
528
  the median at 76 is 15. The distribution is skewed — most requests leave a small set and a few
@@ -542,7 +552,7 @@ Three measurements say this is the right shape, and the third is the one that se
542
552
 
543
553
  | | |
544
554
  |---|---|
545
- | our ranking puts the answer in the top three | **55%** |
555
+ | our ranking puts the answer in the top three | **53%** |
546
556
  | a model handed the same candidates and asked to *choose* gets its first pick right | **69%** |
547
557
  | the two labellers who wrote the ground truth agree **with each other** | **70%** |
548
558
 
@@ -573,7 +583,7 @@ against a human answer where there is one.
573
583
 
574
584
  All three are over the same 118 requests, which matters: agreement measured over all 155 requests
575
585
  in the file is 68%, and the difference is the 21 pairs where both labellers agreed a request was
576
- unanswerable — true, and the easy half. Quoting that 68% beside a 55% computed over the 118 would
586
+ unanswerable — true, and the easy half. Quoting that 68% beside a 53% computed over the 118 would
577
587
  be comparing two populations, which this table did for half a day.
578
588
 
579
589
  **The last row is a ceiling, not a baseline**, and the second row sits at it rather than below it.
@@ -595,7 +605,7 @@ numbers are reported as *agreement with model-written labels* and never as accur
595
605
  used to be a hard filter like the others. It is not like the others: input kind and projected-CRS
596
606
  are facts about the data in hand and output kind is what the caller wants, but *family* is a guess
597
607
  about our taxonomy, which the caller cannot see. Measured, it removed six candidates out of
598
- sixteen — and when the guess was wrong it removed the right operation, with no error, leaving a
608
+ seventeen — and when the guess was wrong it removed the right operation, with no error, leaving a
599
609
  confident answer assembled from neighbours. Every request in the independent set has 4.4 plausible
600
610
  families. That is the silent-failure class [Argleton](https://argleton.org) measures in other
601
611
  people's systems, sitting in our own discovery layer, so it now sorts: declaring the family lifts
@@ -632,7 +642,7 @@ catalog grows:
632
642
  |---|---|---|
633
643
  | 10 | 77% | 80% |
634
644
  | 30 | 63% | 58% |
635
- | 76 | 50% | 40% |
645
+ | 76 | 55% | 40% |
636
646
 
637
647
  **This table used to say the opposite, and the reversal is the finding.** Published at 10/30/51
638
648
  it read 78/83, 47/65, 40/55 — embeddings ahead at every size — and the sentence under it said
@@ -1121,9 +1131,14 @@ under Apache-2.0 rather than in this repository, because an evaluation that live
1121
1131
  inside the thing it evaluates is easy to dismiss in one line. Closed-form truth, no
1122
1132
  model in the evaluator, fixtures rebuilt rather than vendored.
1123
1133
 
1124
- **The score.** On the current run — all twenty-nine families — MapSmith answers every
1125
- trap correctly: **0.00 silent errors over 31 traps, nothing skipped**, against 0.9355
1126
- for a careless composition of the same libraries. One of the thirty-one is not an answer
1134
+ **The score.** On the current run — all twenty-nine families — MapSmith answers thirty
1135
+ of the thirty-one traps correctly: **0.03 silent errors over 31 traps, nothing skipped**,
1136
+ against 0.9032 for a careless composition of the same libraries. That run was published as
1137
+ 0.00. The trap it failed, a point-registered DEM, had a truth derived from a premise about
1138
+ GDAL that GDAL documents the other way, and MapSmith had been built to that premise: both
1139
+ corrected half a cell that was already corrected. Found on 2026-09-25 by a Copernicus DEM
1140
+ whose samples fall on whole arc-seconds by its own documentation; the
1141
+ [erratum](https://github.com/argleton/argleton/blob/main/results/README.md#erratum-2026-09-25-trap-024) rescores every run it touched, and the fix is on `main`. One of the thirty-one is not an answer
1127
1142
  at all but a refusal: a raster and the sidecar beside it declare different georeferencing,
1128
1143
  both readings are GDAL behaving as documented, and the right move is to stop and say so
1129
1144
  rather than pick one. That family arrived on 2026-08-31, the morning after the list of
@@ -1152,7 +1167,7 @@ the newest and the least flattering: MapSmith **failed** that trap on 2026-08-26
1152
1167
  out, with a manifest recording a successful reprojection — and the pass is the fix, not
1153
1168
  the original behaviour. The run where it failed is still published.
1154
1169
 
1155
- **Six defects have come back from it**, which is the return we wanted from putting the
1170
+ **Seven defects have come back from it**, which is the return we wanted from putting the
1156
1171
  suite outside:
1157
1172
 
1158
1173
  - the `datum-ballpark` failure above — 74 m out with a manifest recording a successful
@@ -1167,21 +1182,28 @@ suite outside:
1167
1182
  line was added to 2 km of pipe in silence and every individual row stayed right;
1168
1183
  - two probes answered `unsupported` because nothing could say where a raster's lowest cell
1169
1184
  was;
1170
- - and three probes that came back `unsupported` because MapSmith had no area operation at
1185
+ - three probes that came back `unsupported` because MapSmith had no area operation at
1171
1186
  all — `measure_area` exists because a trap said so, and it carries the first check here
1172
- that asks whether the *number* is right rather than whether the operation ran.
1173
-
1174
- Grid registration was the one MapSmith could not attempt at all when it was published: a
1175
- DEM that declares its values sit at grid nodes rather than filling cells, where every
1176
- position moves half a cell if you ignore the tag. Nothing here read the tag, and no
1177
- operation reported *where* a cell is. Fixed in one module rather than at the point of
1178
- failure, because the defect was in every place that turned an index into a coordinate.
1187
+ that asks whether the *number* is right rather than whether the operation ran;
1188
+ - and the seventh, which the suite sent back by being wrong itself. Grid registration was
1189
+ the family MapSmith could not attempt when it was published, and the fix for it was built
1190
+ to the trap's premise: that GDAL leaves a point-registered file's geotransform alone, so
1191
+ the reader must move every position half a cell. GDAL documents the opposite (RFC 33) —
1192
+ it has already moved it — and MapSmith put every sample of such a file half a cell
1193
+ north-west, the Copernicus DEM included, while the trap scored that as a pass. A
1194
+ Copernicus DEM, whose samples fall on whole arc-seconds by its own documentation, found
1195
+ both on 2026-09-25 ([erratum](https://github.com/argleton/argleton/blob/main/results/README.md#erratum-2026-09-25-trap-024)). Positions now come from GDAL's geotransform
1196
+ whatever the tag says, and the terrain engine, which really does misread the tag, gets
1197
+ an area copy instead of a refusal.
1179
1198
 
1180
1199
  ## Notebook gallery
1181
1200
 
1182
- Three executable walkthroughs in [`examples/`](examples/): verified buffer+clip with
1201
+ Four executable walkthroughs in [`examples/`](examples/): verified buffer+clip with
1183
1202
  provenance manifests, terrain and hydrology on a real 520×520 USGS DEM of **Mount St.
1184
- Helens**, and a deliberately wrong plan rejected before execution and then repaired. The
1203
+ Helens**, a deliberately wrong plan rejected before execution and then repaired, and
1204
+ **Copernicus** data on Monte Baldo — a DEM in degrees refused for slope and then reprojected,
1205
+ and a Sentinel-2 NDVI whose reflectance offset the catalogue declares and the pixels already
1206
+ contain. The
1185
1207
  terrain notebook also shows what happens when reality bites: that DEM is stored with the
1186
1208
  standard TIFF predictor, which Whitebox Workflows 2.x does not undo when reading
1187
1209
  ([upstream report](https://github.com/jblindsay/whitebox_next_gen/issues/32)), so MapSmith
@@ -1254,7 +1276,7 @@ Next, in the order we intend to do it. The linked items carry a written spec —
1254
1276
  - [x] **A suite for the failure every existing benchmark misses** — a result that is wrong and
1255
1277
  reported as successful. It exists, it is not here, and it is not ours to grade:
1256
1278
  [**Argleton**](https://argleton.org) lives in [its own organisation](https://github.com/argleton/argleton)
1257
- under Apache-2.0. What it measures in MapSmith, including the six defects it has sent back,
1279
+ under Apache-2.0. What it measures in MapSmith, including the seven defects it has sent back,
1258
1280
  has [a section of its own](#what-the-suite-found-in-mapsmith). #25 is closed against Argleton
1259
1281
  rather than left open here.
1260
1282
  - [ ] [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](docs/benchmarks.md) say the runtime error message is the information channel that works