mapsmith 0.6.0__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.0 → mapsmith-0.7.0}/.zenodo.json +1 -1
  2. {mapsmith-0.6.0 → mapsmith-0.7.0}/CHANGELOG.md +182 -0
  3. {mapsmith-0.6.0 → mapsmith-0.7.0}/CITATION.cff +2 -2
  4. {mapsmith-0.6.0 → mapsmith-0.7.0}/PKG-INFO +75 -52
  5. {mapsmith-0.6.0 → mapsmith-0.7.0}/README.md +74 -51
  6. {mapsmith-0.6.0 → mapsmith-0.7.0}/docs/argleton-run.json +14 -11
  7. {mapsmith-0.6.0 → mapsmith-0.7.0}/docs/benchmarks.md +19 -7
  8. {mapsmith-0.6.0 → mapsmith-0.7.0}/docs/catalog-entry-spec.md +6 -6
  9. {mapsmith-0.6.0 → mapsmith-0.7.0}/docs/manifest-vocabulary.md +10 -6
  10. {mapsmith-0.6.0 → mapsmith-0.7.0}/examples/01_verified_geoprocessing.ipynb +25 -25
  11. {mapsmith-0.6.0 → mapsmith-0.7.0}/examples/02_terrain_hydrology.ipynb +37 -37
  12. {mapsmith-0.6.0 → 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.0 → 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.0 → mapsmith-0.7.0}/pyproject.toml +1 -1
  20. {mapsmith-0.6.0 → mapsmith-0.7.0}/server.json +3 -3
  21. {mapsmith-0.6.0 → mapsmith-0.7.0}/site/build.py +54 -0
  22. {mapsmith-0.6.0 → mapsmith-0.7.0}/site/index.template.html +12 -10
  23. {mapsmith-0.6.0 → mapsmith-0.7.0}/src/mapsmith/__init__.py +1 -1
  24. {mapsmith-0.6.0 → mapsmith-0.7.0}/src/mapsmith/antimeridian.py +109 -19
  25. {mapsmith-0.6.0 → mapsmith-0.7.0}/src/mapsmith/catalog.py +94 -29
  26. {mapsmith-0.6.0 → mapsmith-0.7.0}/src/mapsmith/engines/network.py +6 -6
  27. {mapsmith-0.6.0 → mapsmith-0.7.0}/src/mapsmith/engines/raster.py +215 -37
  28. {mapsmith-0.6.0 → mapsmith-0.7.0}/src/mapsmith/engines/sampling.py +414 -43
  29. {mapsmith-0.6.0 → mapsmith-0.7.0}/src/mapsmith/engines/vector.py +13 -2
  30. {mapsmith-0.6.0 → mapsmith-0.7.0}/src/mapsmith/engines/whitebox_engine.py +95 -38
  31. {mapsmith-0.6.0 → mapsmith-0.7.0}/src/mapsmith/gdal_policy.py +17 -0
  32. {mapsmith-0.6.0 → mapsmith-0.7.0}/src/mapsmith/grid.py +71 -89
  33. {mapsmith-0.6.0 → mapsmith-0.7.0}/src/mapsmith/plans/registry.py +5 -2
  34. {mapsmith-0.6.0 → mapsmith-0.7.0}/src/mapsmith/provenance.py +36 -8
  35. {mapsmith-0.6.0 → mapsmith-0.7.0}/src/mapsmith/server.py +14 -1
  36. {mapsmith-0.6.0 → mapsmith-0.7.0}/tests/data/manifest-v1.schema.json +29 -4
  37. {mapsmith-0.6.0 → mapsmith-0.7.0}/tests/data/manifest_spec_validator.py +39 -1
  38. {mapsmith-0.6.0 → mapsmith-0.7.0}/tests/test_failure_manifest.py +4 -2
  39. mapsmith-0.7.0/tests/test_grid_registration.py +530 -0
  40. mapsmith-0.7.0/tests/test_raster.py +358 -0
  41. {mapsmith-0.6.0 → mapsmith-0.7.0}/tests/test_redaction.py +56 -54
  42. {mapsmith-0.6.0 → mapsmith-0.7.0}/tests/test_sampling.py +272 -4
  43. {mapsmith-0.6.0 → mapsmith-0.7.0}/tests/test_showcase.py +53 -1
  44. {mapsmith-0.6.0 → mapsmith-0.7.0}/tests/test_stacks.py +3 -3
  45. {mapsmith-0.6.0 → mapsmith-0.7.0}/tests/test_summarize_points_in_polygons.py +71 -0
  46. {mapsmith-0.6.0 → mapsmith-0.7.0}/tests/test_verify.py +39 -6
  47. mapsmith-0.6.0/tests/test_grid_registration.py +0 -449
  48. mapsmith-0.6.0/tests/test_raster.py +0 -94
  49. {mapsmith-0.6.0 → mapsmith-0.7.0}/.dockerignore +0 -0
  50. {mapsmith-0.6.0 → mapsmith-0.7.0}/.gitignore +0 -0
  51. {mapsmith-0.6.0 → mapsmith-0.7.0}/CLAUDE.md +0 -0
  52. {mapsmith-0.6.0 → mapsmith-0.7.0}/CONTRIBUTING.md +0 -0
  53. {mapsmith-0.6.0 → mapsmith-0.7.0}/Dockerfile +0 -0
  54. {mapsmith-0.6.0 → mapsmith-0.7.0}/LICENSE +0 -0
  55. {mapsmith-0.6.0 → mapsmith-0.7.0}/MANIFESTO.md +0 -0
  56. {mapsmith-0.6.0 → mapsmith-0.7.0}/SECURITY.md +0 -0
  57. {mapsmith-0.6.0 → mapsmith-0.7.0}/TRADEMARKS.md +0 -0
  58. {mapsmith-0.6.0 → mapsmith-0.7.0}/docker-compose.yml +0 -0
  59. {mapsmith-0.6.0 → mapsmith-0.7.0}/examples/fixtures/mount_st_helens_dem.tif +0 -0
  60. {mapsmith-0.6.0 → mapsmith-0.7.0}/funding.json +0 -0
  61. {mapsmith-0.6.0 → mapsmith-0.7.0}/glama.json +0 -0
  62. {mapsmith-0.6.0 → mapsmith-0.7.0}/schema/operation.schema.json +0 -0
  63. {mapsmith-0.6.0 → mapsmith-0.7.0}/src/mapsmith/datum.py +0 -0
  64. {mapsmith-0.6.0 → mapsmith-0.7.0}/src/mapsmith/discovery_log.py +0 -0
  65. {mapsmith-0.6.0 → mapsmith-0.7.0}/src/mapsmith/engines/__init__.py +0 -0
  66. {mapsmith-0.6.0 → mapsmith-0.7.0}/src/mapsmith/engines/dispatch.py +0 -0
  67. {mapsmith-0.6.0 → mapsmith-0.7.0}/src/mapsmith/engines/duckdb_engine.py +0 -0
  68. {mapsmith-0.6.0 → mapsmith-0.7.0}/src/mapsmith/engines/esri.py +0 -0
  69. {mapsmith-0.6.0 → mapsmith-0.7.0}/src/mapsmith/engines/geodesy.py +0 -0
  70. {mapsmith-0.6.0 → mapsmith-0.7.0}/src/mapsmith/engines/linework.py +0 -0
  71. {mapsmith-0.6.0 → mapsmith-0.7.0}/src/mapsmith/engines/sedona_engine.py +0 -0
  72. {mapsmith-0.6.0 → mapsmith-0.7.0}/src/mapsmith/engines/spatial_stats.py +0 -0
  73. {mapsmith-0.6.0 → mapsmith-0.7.0}/src/mapsmith/engines/summaries.py +0 -0
  74. {mapsmith-0.6.0 → mapsmith-0.7.0}/src/mapsmith/jobs.py +0 -0
  75. {mapsmith-0.6.0 → mapsmith-0.7.0}/src/mapsmith/lineage.py +0 -0
  76. {mapsmith-0.6.0 → mapsmith-0.7.0}/src/mapsmith/plans/__init__.py +0 -0
  77. {mapsmith-0.6.0 → mapsmith-0.7.0}/src/mapsmith/plans/executor.py +0 -0
  78. {mapsmith-0.6.0 → mapsmith-0.7.0}/src/mapsmith/plans/models.py +0 -0
  79. {mapsmith-0.6.0 → mapsmith-0.7.0}/src/mapsmith/plans/validator.py +0 -0
  80. {mapsmith-0.6.0 → mapsmith-0.7.0}/src/mapsmith/preview.py +0 -0
  81. {mapsmith-0.6.0 → mapsmith-0.7.0}/src/mapsmith/readers.py +0 -0
  82. {mapsmith-0.6.0 → mapsmith-0.7.0}/src/mapsmith/retrieval.py +0 -0
  83. {mapsmith-0.6.0 → mapsmith-0.7.0}/src/mapsmith/sql_policy.py +0 -0
  84. {mapsmith-0.6.0 → mapsmith-0.7.0}/src/mapsmith/stacks.py +0 -0
  85. {mapsmith-0.6.0 → mapsmith-0.7.0}/src/mapsmith/ui.py +0 -0
  86. {mapsmith-0.6.0 → mapsmith-0.7.0}/src/mapsmith/verify.py +0 -0
  87. {mapsmith-0.6.0 → mapsmith-0.7.0}/src/mapsmith/workspace.py +0 -0
  88. {mapsmith-0.6.0 → mapsmith-0.7.0}/tests/conftest.py +0 -0
  89. {mapsmith-0.6.0 → mapsmith-0.7.0}/tests/data/discovery_queries.json +0 -0
  90. {mapsmith-0.6.0 → mapsmith-0.7.0}/tests/test_ambiguous_layer.py +0 -0
  91. {mapsmith-0.6.0 → mapsmith-0.7.0}/tests/test_antimeridian.py +0 -0
  92. {mapsmith-0.6.0 → mapsmith-0.7.0}/tests/test_band_math.py +0 -0
  93. {mapsmith-0.6.0 → mapsmith-0.7.0}/tests/test_catalog.py +0 -0
  94. {mapsmith-0.6.0 → mapsmith-0.7.0}/tests/test_catalog_applicability.py +0 -0
  95. {mapsmith-0.6.0 → mapsmith-0.7.0}/tests/test_catalog_entry_spec.py +0 -0
  96. {mapsmith-0.6.0 → mapsmith-0.7.0}/tests/test_catalog_retrieval.py +0 -0
  97. {mapsmith-0.6.0 → mapsmith-0.7.0}/tests/test_centroid_convert.py +0 -0
  98. {mapsmith-0.6.0 → mapsmith-0.7.0}/tests/test_clip_reclassify_raster.py +0 -0
  99. {mapsmith-0.6.0 → mapsmith-0.7.0}/tests/test_contours_and_cost.py +0 -0
  100. {mapsmith-0.6.0 → mapsmith-0.7.0}/tests/test_dashboard.py +0 -0
  101. {mapsmith-0.6.0 → mapsmith-0.7.0}/tests/test_datum.py +0 -0
  102. {mapsmith-0.6.0 → mapsmith-0.7.0}/tests/test_describe_raster.py +0 -0
  103. {mapsmith-0.6.0 → mapsmith-0.7.0}/tests/test_discovery_contract.py +0 -0
  104. {mapsmith-0.6.0 → mapsmith-0.7.0}/tests/test_discovery_log.py +0 -0
  105. {mapsmith-0.6.0 → mapsmith-0.7.0}/tests/test_discovery_report.py +0 -0
  106. {mapsmith-0.6.0 → mapsmith-0.7.0}/tests/test_duckdb_sandbox.py +0 -0
  107. {mapsmith-0.6.0 → mapsmith-0.7.0}/tests/test_engines.py +0 -0
  108. {mapsmith-0.6.0 → mapsmith-0.7.0}/tests/test_environment.py +0 -0
  109. {mapsmith-0.6.0 → mapsmith-0.7.0}/tests/test_gdal_policy.py +0 -0
  110. {mapsmith-0.6.0 → mapsmith-0.7.0}/tests/test_geodesy.py +0 -0
  111. {mapsmith-0.6.0 → mapsmith-0.7.0}/tests/test_geoparquet2.py +0 -0
  112. {mapsmith-0.6.0 → mapsmith-0.7.0}/tests/test_lineage.py +0 -0
  113. {mapsmith-0.6.0 → mapsmith-0.7.0}/tests/test_linework.py +0 -0
  114. {mapsmith-0.6.0 → mapsmith-0.7.0}/tests/test_measure_area.py +0 -0
  115. {mapsmith-0.6.0 → mapsmith-0.7.0}/tests/test_merge_simplify.py +0 -0
  116. {mapsmith-0.6.0 → mapsmith-0.7.0}/tests/test_nearest_explode.py +0 -0
  117. {mapsmith-0.6.0 → mapsmith-0.7.0}/tests/test_network.py +0 -0
  118. {mapsmith-0.6.0 → mapsmith-0.7.0}/tests/test_overlay_dissolve.py +0 -0
  119. {mapsmith-0.6.0 → mapsmith-0.7.0}/tests/test_path_containment.py +0 -0
  120. {mapsmith-0.6.0 → mapsmith-0.7.0}/tests/test_plans.py +0 -0
  121. {mapsmith-0.6.0 → mapsmith-0.7.0}/tests/test_preview.py +0 -0
  122. {mapsmith-0.6.0 → mapsmith-0.7.0}/tests/test_published_figures.py +0 -0
  123. {mapsmith-0.6.0 → mapsmith-0.7.0}/tests/test_raster_reproject_bands.py +0 -0
  124. {mapsmith-0.6.0 → mapsmith-0.7.0}/tests/test_resample_raster.py +0 -0
  125. {mapsmith-0.6.0 → mapsmith-0.7.0}/tests/test_retrieval.py +0 -0
  126. {mapsmith-0.6.0 → mapsmith-0.7.0}/tests/test_retrieval_at_scale.py +0 -0
  127. {mapsmith-0.6.0 → mapsmith-0.7.0}/tests/test_retrieval_degradation.py +0 -0
  128. {mapsmith-0.6.0 → mapsmith-0.7.0}/tests/test_select_and_extract.py +0 -0
  129. {mapsmith-0.6.0 → mapsmith-0.7.0}/tests/test_smoke.py +0 -0
  130. {mapsmith-0.6.0 → mapsmith-0.7.0}/tests/test_south_up_and_mixed.py +0 -0
  131. {mapsmith-0.6.0 → mapsmith-0.7.0}/tests/test_spatial_stats.py +0 -0
  132. {mapsmith-0.6.0 → mapsmith-0.7.0}/tests/test_sql_axis_order.py +0 -0
  133. {mapsmith-0.6.0 → mapsmith-0.7.0}/tests/test_sql_credentials.py +0 -0
  134. {mapsmith-0.6.0 → mapsmith-0.7.0}/tests/test_summaries.py +0 -0
  135. {mapsmith-0.6.0 → mapsmith-0.7.0}/tests/test_terrain_derivatives.py +0 -0
  136. {mapsmith-0.6.0 → mapsmith-0.7.0}/tests/test_tier_a_operations.py +0 -0
  137. {mapsmith-0.6.0 → mapsmith-0.7.0}/tests/test_ui.py +0 -0
  138. {mapsmith-0.6.0 → mapsmith-0.7.0}/tests/test_verification_status.py +0 -0
  139. {mapsmith-0.6.0 → mapsmith-0.7.0}/tests/test_verify_repair.py +0 -0
  140. {mapsmith-0.6.0 → mapsmith-0.7.0}/tests/test_voronoi.py +0 -0
  141. {mapsmith-0.6.0 → mapsmith-0.7.0}/tests/test_whitebox.py +0 -0
  142. {mapsmith-0.6.0 → mapsmith-0.7.0}/tests/test_whitebox_derivatives.py +0 -0
  143. {mapsmith-0.6.0 → mapsmith-0.7.0}/tests/test_whitebox_encoding.py +0 -0
  144. {mapsmith-0.6.0 → mapsmith-0.7.0}/tests/test_whitebox_geographic_refusal.py +0 -0
  145. {mapsmith-0.6.0 → mapsmith-0.7.0}/tests/test_worked_example.py +0 -0
  146. {mapsmith-0.6.0 → 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.0",
4
+ "version": "0.7.0",
5
5
  "creators": [
6
6
  {"name": "The MapSmith project"}
7
7
  ],
@@ -8,6 +8,188 @@ 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
+
154
+ ## [0.6.1] - 2026-09-25
155
+
156
+ A small release on the day of 0.6.0. It moves no manifest key and changes no
157
+ answer that 0.6.0 got right: it stops a refusal 0.6.0 introduced from firing on
158
+ a layer written in 0..360 longitudes, gives two refusals the right way out, and
159
+ declares the specification's current draft.
160
+
161
+ ### Changed
162
+
163
+ - **Records declare `spec_version` `1.0.0-draft.8`.** Two drafts in one day,
164
+ both narrowings of where a producer may add a key: draft.7 requires every
165
+ key of `crs_decisions` the specification does not define to be named
166
+ `x-<producer>:<name>`, which it had left to one reading of an ambiguous
167
+ sentence, and draft.8 carries the rule inside `transformation`,
168
+ `round_trip` and `repairs[]`. MapSmith already named every one of them that
169
+ way, so no key moves: a reader keying on the label sees draft.8, a reader
170
+ keying on fields sees nothing change. The vendored schema and validator are
171
+ draft.8's, and the conformance sweep validates every writer's record
172
+ against them.
173
+
174
+ ### Fixed
175
+
176
+ - **A layer written in 0..360 longitudes had its seam in the wrong place.**
177
+ The antimeridian refusal of 0.6.0 took the edge of the plane to be -180/180,
178
+ so the world written as `box(0, -90, 360, 90)` was refused, the way 0.6.0's
179
+ own review had found Antarctica refused in -180..180. For such a layer the
180
+ edge is 0/360, and the ring that really is drawn naively crosses the prime
181
+ meridian: the refusal now says so and tells its author to split there, where
182
+ it used to send them to 180. A layer is read as 0..360 when it has no
183
+ negative longitude and some beyond 180 -- not whenever one vertex passes
184
+ 180, which the first version of this fix did, and which refused the world
185
+ beside a polygon overshooting 180 by 1e-7. Edges along either pair of seams
186
+ are exempt in every layer.
187
+ - **"No UTM zone" said the data was centred on the antimeridian, whatever the
188
+ data, and offered no way out.** The realistic cause is latitude — UTM stops
189
+ at 84N and 80S — and the message now says that and names a polar CRS to use
190
+ (UPS, or a polar stereographic), for data that crosses 180 and for data that
191
+ does not.
192
+
11
193
  ## [0.6.0] - 2026-09-25
12
194
 
13
195
  The reason to upgrade is in **Fixed**: five operations stop reading a zone drawn
@@ -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.0"
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.0
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
@@ -87,7 +87,7 @@ MapSmith's private output: JSON Schema, a toolchain-free validator, a conformanc
87
87
  hundred-line emitter that never imports MapSmith. Records carry `spec_version`, and CI validates
88
88
  real MapSmith output against the spec's own validator. The specification is archived and citable
89
89
  as [10.5281/zenodo.22205213](https://doi.org/10.5281/zenodo.22205213), a concept DOI that
90
- resolves to the latest *archived* draft. These records declare `1.0.0-draft.6`; if the DOI shows
90
+ resolves to the latest *archived* draft. These records declare `1.0.0-draft.8`; if the DOI shows
91
91
  an earlier one, the archive has not caught up, and the
92
92
  [repository](https://github.com/mapsmith-ai/manifest-spec) holds the text they are written against. The names MapSmith puts
93
93
  in a record beyond the ones the specification defines — every extension check name, and every
@@ -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,17 +265,20 @@ 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.0**, 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.0 today, with nothing
269
- unreleased beyond it.
270
-
271
- **If you read manifests, 0.6.0 moves one key**: `x-mapsmith:round_trip` is now `round_trip`,
272
- because the specification made it a core key in `1.0.0-draft.6` and forbids recording that
273
- fact under a prefixed name. The shape is unchanged, and records declare `spec_version`
274
- `1.0.0-draft.6`. From 0.4.0, 0.5.0 had already renamed eight `crs_decisions` keys, the
275
- top-level `mapsmith_version` (read `producer.version`), two fields of the round trip, two of
276
- `repairs[]` and `engine.geometry_library`; the changelog lists each with what to read instead.
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.
274
+
275
+ **If you read manifests, 0.7.0 renames no key**: records declare `spec_version`
276
+ `1.0.0-draft.8`, two drafts past 0.6.0's `draft.6`, and both drafts only narrowed where a
277
+ producer may add a key of its own — which MapSmith already did the narrower way. From 0.5.x,
278
+ 0.6.0 renamed `x-mapsmith:round_trip` to `round_trip`; from 0.4.0, 0.5.0 had renamed eight
279
+ `crs_decisions` keys, the top-level `mapsmith_version` (read `producer.version`), two fields of
280
+ the round trip, two of `repairs[]` and `engine.geometry_library`. The changelog lists each with
281
+ what to read instead.
277
282
 
278
283
  Then ask your agent things like:
279
284
 
@@ -299,8 +304,8 @@ for it:
299
304
 
300
305
  ```json
301
306
  {
302
- "spec_version": "1.0.0-draft.6",
303
- "producer": {"name": "mapsmith", "version": "0.6.0"},
307
+ "spec_version": "1.0.0-draft.8",
308
+ "producer": {"name": "mapsmith", "version": "0.7.0"},
304
309
  "operation": "buffer_layer",
305
310
  "parameters": {"distance_meters": 300.0},
306
311
  "inputs": [{
@@ -450,10 +455,10 @@ was shown this catalog, because a model handed the entry writes a paraphrase of
450
455
 
451
456
  | what the caller declares | candidates left | BM25, found@3 | embeddings, found@3 | **right answer in what comes back** |
452
457
  |---|---|---|---|---|
453
- | nothing — words alone | 76 | 29% | 17% | 29% |
454
- | 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% |
455
460
  | + what I want back | 31 | 45% | 40% | 53% |
456
- | **+ how many datasets I have** | **16** | **55%** | **53%** | **98%** |
461
+ | **+ how many datasets I have** | **17** | **53%** | **49%** | **98%** |
457
462
 
458
463
  **Two ranking columns, and that is a correction.** This table used to carry one,
459
464
  computed with the default engine — which is the embedding one where its model
@@ -466,18 +471,22 @@ published under a sentence promising it could be checked.
466
471
  The two also differ in a way worth seeing, and this page had it backwards
467
472
  until 2026-08-30. It said the embedding engine overtakes BM25 once the facets
468
473
  have narrowed. It does not overtake it anywhere: BM25 leads at every row of the
469
- table above, by seven to twelve points, and the gap is widest at the fullest
470
- declaration. An exact term either matches or does not, and the entries that
471
- survive a full declaration are told apart by the words that distinguish them —
472
- which is what `distinguishes` is for. The embedding engine earns its place on
473
- 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.)*
474
483
 
475
484
  The last column is not an accuracy figure — it is a property, and the 98% rather than 100% is
476
485
  worth a sentence. The narrowing never drops the right operation: that is asserted per entry and
477
486
  holds for all 76. What the column measures is whether the surviving set was small enough to hand
478
487
  over WHOLE, and for a handful of requests it still is not, so those fall back to a ranked
479
488
  shortlist and the answer can be outside the top three. Ranking decides the order; it does not
480
- 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
481
490
  of it".
482
491
 
483
492
  **The third row is the scaling wall, and we hit it in one afternoon.** On 2026-08-29 the
@@ -508,10 +517,12 @@ of comparison this page exists to refuse.)
508
517
 
509
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.
510
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
+
511
522
  That is the shape of the trade, and it says when the next facet is due. The figure to watch is
512
523
  not found@3 — a ranker will always get worse as the catalogue grows, and it is a hint. It is the
513
524
  **average** surviving set at the fullest declaration, the fourth column of that table:
514
- 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
515
526
  property and starts being a ranking again, and the answer is another fact the caller already
516
527
  knows, not a bigger threshold. (It said *median* until 2026-08-29, and published the mean:
517
528
  the median at 76 is 15. The distribution is skewed — most requests leave a small set and a few
@@ -541,7 +552,7 @@ Three measurements say this is the right shape, and the third is the one that se
541
552
 
542
553
  | | |
543
554
  |---|---|
544
- | our ranking puts the answer in the top three | **55%** |
555
+ | our ranking puts the answer in the top three | **53%** |
545
556
  | a model handed the same candidates and asked to *choose* gets its first pick right | **69%** |
546
557
  | the two labellers who wrote the ground truth agree **with each other** | **70%** |
547
558
 
@@ -572,7 +583,7 @@ against a human answer where there is one.
572
583
 
573
584
  All three are over the same 118 requests, which matters: agreement measured over all 155 requests
574
585
  in the file is 68%, and the difference is the 21 pairs where both labellers agreed a request was
575
- 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
576
587
  be comparing two populations, which this table did for half a day.
577
588
 
578
589
  **The last row is a ceiling, not a baseline**, and the second row sits at it rather than below it.
@@ -594,7 +605,7 @@ numbers are reported as *agreement with model-written labels* and never as accur
594
605
  used to be a hard filter like the others. It is not like the others: input kind and projected-CRS
595
606
  are facts about the data in hand and output kind is what the caller wants, but *family* is a guess
596
607
  about our taxonomy, which the caller cannot see. Measured, it removed six candidates out of
597
- 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
598
609
  confident answer assembled from neighbours. Every request in the independent set has 4.4 plausible
599
610
  families. That is the silent-failure class [Argleton](https://argleton.org) measures in other
600
611
  people's systems, sitting in our own discovery layer, so it now sorts: declaring the family lifts
@@ -631,7 +642,7 @@ catalog grows:
631
642
  |---|---|---|
632
643
  | 10 | 77% | 80% |
633
644
  | 30 | 63% | 58% |
634
- | 76 | 50% | 40% |
645
+ | 76 | 55% | 40% |
635
646
 
636
647
  **This table used to say the opposite, and the reversal is the finding.** Published at 10/30/51
637
648
  it read 78/83, 47/65, 40/55 — embeddings ahead at every size — and the sentence under it said
@@ -1120,9 +1131,14 @@ under Apache-2.0 rather than in this repository, because an evaluation that live
1120
1131
  inside the thing it evaluates is easy to dismiss in one line. Closed-form truth, no
1121
1132
  model in the evaluator, fixtures rebuilt rather than vendored.
1122
1133
 
1123
- **The score.** On the current run — all twenty-nine families — MapSmith answers every
1124
- trap correctly: **0.00 silent errors over 31 traps, nothing skipped**, against 0.9355
1125
- 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
1126
1142
  at all but a refusal: a raster and the sidecar beside it declare different georeferencing,
1127
1143
  both readings are GDAL behaving as documented, and the right move is to stop and say so
1128
1144
  rather than pick one. That family arrived on 2026-08-31, the morning after the list of
@@ -1151,7 +1167,7 @@ the newest and the least flattering: MapSmith **failed** that trap on 2026-08-26
1151
1167
  out, with a manifest recording a successful reprojection — and the pass is the fix, not
1152
1168
  the original behaviour. The run where it failed is still published.
1153
1169
 
1154
- **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
1155
1171
  suite outside:
1156
1172
 
1157
1173
  - the `datum-ballpark` failure above — 74 m out with a manifest recording a successful
@@ -1166,21 +1182,28 @@ suite outside:
1166
1182
  line was added to 2 km of pipe in silence and every individual row stayed right;
1167
1183
  - two probes answered `unsupported` because nothing could say where a raster's lowest cell
1168
1184
  was;
1169
- - 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
1170
1186
  all — `measure_area` exists because a trap said so, and it carries the first check here
1171
- that asks whether the *number* is right rather than whether the operation ran.
1172
-
1173
- Grid registration was the one MapSmith could not attempt at all when it was published: a
1174
- DEM that declares its values sit at grid nodes rather than filling cells, where every
1175
- position moves half a cell if you ignore the tag. Nothing here read the tag, and no
1176
- operation reported *where* a cell is. Fixed in one module rather than at the point of
1177
- 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.
1178
1198
 
1179
1199
  ## Notebook gallery
1180
1200
 
1181
- Three executable walkthroughs in [`examples/`](examples/): verified buffer+clip with
1201
+ Four executable walkthroughs in [`examples/`](examples/): verified buffer+clip with
1182
1202
  provenance manifests, terrain and hydrology on a real 520×520 USGS DEM of **Mount St.
1183
- 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
1184
1207
  terrain notebook also shows what happens when reality bites: that DEM is stored with the
1185
1208
  standard TIFF predictor, which Whitebox Workflows 2.x does not undo when reading
1186
1209
  ([upstream report](https://github.com/jblindsay/whitebox_next_gen/issues/32)), so MapSmith
@@ -1253,7 +1276,7 @@ Next, in the order we intend to do it. The linked items carry a written spec —
1253
1276
  - [x] **A suite for the failure every existing benchmark misses** — a result that is wrong and
1254
1277
  reported as successful. It exists, it is not here, and it is not ours to grade:
1255
1278
  [**Argleton**](https://argleton.org) lives in [its own organisation](https://github.com/argleton/argleton)
1256
- 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,
1257
1280
  has [a section of its own](#what-the-suite-found-in-mapsmith). #25 is closed against Argleton
1258
1281
  rather than left open here.
1259
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