mapsmith 0.7.1__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.7.1 → mapsmith-0.9.0}/.gitignore +1 -0
  2. {mapsmith-0.7.1 → mapsmith-0.9.0}/.zenodo.json +1 -1
  3. {mapsmith-0.7.1 → mapsmith-0.9.0}/CHANGELOG.md +266 -1
  4. {mapsmith-0.7.1 → mapsmith-0.9.0}/CITATION.cff +2 -2
  5. {mapsmith-0.7.1 → mapsmith-0.9.0}/CLAUDE.md +1 -1
  6. mapsmith-0.7.1/README.md → mapsmith-0.9.0/PKG-INFO +1403 -1298
  7. mapsmith-0.7.1/PKG-INFO → mapsmith-0.9.0/README.md +1360 -1340
  8. {mapsmith-0.7.1 → mapsmith-0.9.0}/SECURITY.md +47 -5
  9. {mapsmith-0.7.1 → mapsmith-0.9.0}/docs/argleton-run.json +11 -11
  10. {mapsmith-0.7.1 → mapsmith-0.9.0}/docs/benchmarks.md +12 -6
  11. {mapsmith-0.7.1 → mapsmith-0.9.0}/docs/catalog-entry-spec.md +10 -9
  12. {mapsmith-0.7.1 → mapsmith-0.9.0}/docs/manifest-vocabulary.md +12 -6
  13. {mapsmith-0.7.1 → mapsmith-0.9.0}/examples/01_verified_geoprocessing.ipynb +25 -25
  14. {mapsmith-0.7.1 → mapsmith-0.9.0}/examples/02_terrain_hydrology.ipynb +36 -36
  15. {mapsmith-0.7.1 → mapsmith-0.9.0}/examples/03_validated_plans.ipynb +23 -23
  16. {mapsmith-0.7.1 → mapsmith-0.9.0}/examples/04_copernicus_terrain_vegetation.ipynb +31 -31
  17. {mapsmith-0.7.1 → mapsmith-0.9.0}/funding.json +1 -1
  18. {mapsmith-0.7.1 → mapsmith-0.9.0}/pyproject.toml +38 -6
  19. {mapsmith-0.7.1 → mapsmith-0.9.0}/schema/operation.schema.json +13 -0
  20. {mapsmith-0.7.1 → mapsmith-0.9.0}/server.json +3 -3
  21. {mapsmith-0.7.1 → mapsmith-0.9.0}/site/build.py +20 -5
  22. {mapsmith-0.7.1 → mapsmith-0.9.0}/site/index.template.html +22 -16
  23. {mapsmith-0.7.1 → mapsmith-0.9.0}/src/mapsmith/__init__.py +1 -1
  24. {mapsmith-0.7.1 → mapsmith-0.9.0}/src/mapsmith/catalog.py +248 -25
  25. mapsmith-0.9.0/src/mapsmith/datum.py +865 -0
  26. {mapsmith-0.7.1 → mapsmith-0.9.0}/src/mapsmith/engines/duckdb_engine.py +4 -2
  27. {mapsmith-0.7.1 → mapsmith-0.9.0}/src/mapsmith/engines/linework.py +29 -16
  28. {mapsmith-0.7.1 → mapsmith-0.9.0}/src/mapsmith/engines/network.py +26 -10
  29. {mapsmith-0.7.1 → mapsmith-0.9.0}/src/mapsmith/engines/raster.py +83 -25
  30. {mapsmith-0.7.1 → mapsmith-0.9.0}/src/mapsmith/engines/sampling.py +19 -6
  31. {mapsmith-0.7.1 → mapsmith-0.9.0}/src/mapsmith/engines/sedona_engine.py +4 -2
  32. {mapsmith-0.7.1 → mapsmith-0.9.0}/src/mapsmith/engines/spatial_stats.py +301 -14
  33. {mapsmith-0.7.1 → mapsmith-0.9.0}/src/mapsmith/engines/summaries.py +2 -2
  34. {mapsmith-0.7.1 → mapsmith-0.9.0}/src/mapsmith/engines/vector.py +4187 -3360
  35. {mapsmith-0.7.1 → mapsmith-0.9.0}/src/mapsmith/engines/whitebox_engine.py +8 -6
  36. {mapsmith-0.7.1 → mapsmith-0.9.0}/src/mapsmith/lineage.py +273 -34
  37. {mapsmith-0.7.1 → mapsmith-0.9.0}/src/mapsmith/plans/registry.py +28 -0
  38. {mapsmith-0.7.1 → mapsmith-0.9.0}/src/mapsmith/provenance.py +420 -41
  39. {mapsmith-0.7.1 → mapsmith-0.9.0}/src/mapsmith/readers.py +21 -0
  40. {mapsmith-0.7.1 → mapsmith-0.9.0}/src/mapsmith/server.py +9 -1
  41. {mapsmith-0.7.1 → mapsmith-0.9.0}/src/mapsmith/verify.py +35 -6
  42. {mapsmith-0.7.1 → mapsmith-0.9.0}/tests/conftest.py +50 -0
  43. {mapsmith-0.7.1 → mapsmith-0.9.0}/tests/data/manifest-v1.schema.json +20 -4
  44. mapsmith-0.9.0/tests/data/manifest_spec_multi_file_digest.py +84 -0
  45. {mapsmith-0.7.1 → mapsmith-0.9.0}/tests/data/manifest_spec_validator.py +400 -386
  46. mapsmith-0.9.0/tests/test_apportion_by_area.py +861 -0
  47. {mapsmith-0.7.1 → mapsmith-0.9.0}/tests/test_catalog_applicability.py +11 -1
  48. mapsmith-0.9.0/tests/test_choice_at_scale.py +48 -0
  49. {mapsmith-0.7.1 → mapsmith-0.9.0}/tests/test_contours_and_cost.py +40 -1
  50. mapsmith-0.9.0/tests/test_dataset_digest.py +214 -0
  51. {mapsmith-0.7.1 → mapsmith-0.9.0}/tests/test_datum.py +519 -9
  52. {mapsmith-0.7.1 → mapsmith-0.9.0}/tests/test_discovery_contract.py +7 -3
  53. {mapsmith-0.7.1 → mapsmith-0.9.0}/tests/test_discovery_report.py +7 -4
  54. mapsmith-0.9.0/tests/test_extract_from_gdb.py +89 -0
  55. mapsmith-0.9.0/tests/test_failure_record_and_earlier_bytes.py +215 -0
  56. mapsmith-0.9.0/tests/test_geometry_declarations.py +272 -0
  57. mapsmith-0.9.0/tests/test_lineage_digest_rules.py +269 -0
  58. {mapsmith-0.7.1 → mapsmith-0.9.0}/tests/test_linework.py +35 -1
  59. {mapsmith-0.7.1 → mapsmith-0.9.0}/tests/test_published_figures.py +27 -11
  60. {mapsmith-0.7.1 → mapsmith-0.9.0}/tests/test_raster.py +32 -4
  61. {mapsmith-0.7.1 → mapsmith-0.9.0}/tests/test_redaction.py +38 -0
  62. {mapsmith-0.7.1 → mapsmith-0.9.0}/tests/test_showcase.py +79 -2
  63. {mapsmith-0.7.1 → mapsmith-0.9.0}/tests/test_spatial_stats.py +226 -1
  64. {mapsmith-0.7.1 → mapsmith-0.9.0}/tests/test_verify.py +2516 -2513
  65. mapsmith-0.7.1/src/mapsmith/datum.py +0 -308
  66. {mapsmith-0.7.1 → mapsmith-0.9.0}/.dockerignore +0 -0
  67. {mapsmith-0.7.1 → mapsmith-0.9.0}/CONTRIBUTING.md +0 -0
  68. {mapsmith-0.7.1 → mapsmith-0.9.0}/Dockerfile +0 -0
  69. {mapsmith-0.7.1 → mapsmith-0.9.0}/LICENSE +0 -0
  70. {mapsmith-0.7.1 → mapsmith-0.9.0}/MANIFESTO.md +0 -0
  71. {mapsmith-0.7.1 → mapsmith-0.9.0}/TRADEMARKS.md +0 -0
  72. {mapsmith-0.7.1 → mapsmith-0.9.0}/docker-compose.yml +0 -0
  73. {mapsmith-0.7.1 → mapsmith-0.9.0}/examples/README.md +0 -0
  74. {mapsmith-0.7.1 → mapsmith-0.9.0}/examples/fixtures/COPERNICUS-NOTICE.md +0 -0
  75. {mapsmith-0.7.1 → mapsmith-0.9.0}/examples/fixtures/fetch_copernicus.py +0 -0
  76. {mapsmith-0.7.1 → mapsmith-0.9.0}/examples/fixtures/monte_baldo_dem.tif +0 -0
  77. {mapsmith-0.7.1 → mapsmith-0.9.0}/examples/fixtures/monte_baldo_s2.tif +0 -0
  78. {mapsmith-0.7.1 → mapsmith-0.9.0}/examples/fixtures/mount_st_helens_dem.tif +0 -0
  79. {mapsmith-0.7.1 → mapsmith-0.9.0}/glama.json +0 -0
  80. {mapsmith-0.7.1 → mapsmith-0.9.0}/src/mapsmith/antimeridian.py +0 -0
  81. {mapsmith-0.7.1 → mapsmith-0.9.0}/src/mapsmith/discovery_log.py +0 -0
  82. {mapsmith-0.7.1 → mapsmith-0.9.0}/src/mapsmith/engines/__init__.py +0 -0
  83. {mapsmith-0.7.1 → mapsmith-0.9.0}/src/mapsmith/engines/dispatch.py +0 -0
  84. {mapsmith-0.7.1 → mapsmith-0.9.0}/src/mapsmith/engines/esri.py +0 -0
  85. {mapsmith-0.7.1 → mapsmith-0.9.0}/src/mapsmith/engines/geodesy.py +0 -0
  86. {mapsmith-0.7.1 → mapsmith-0.9.0}/src/mapsmith/gdal_policy.py +0 -0
  87. {mapsmith-0.7.1 → mapsmith-0.9.0}/src/mapsmith/grid.py +0 -0
  88. {mapsmith-0.7.1 → mapsmith-0.9.0}/src/mapsmith/jobs.py +0 -0
  89. {mapsmith-0.7.1 → mapsmith-0.9.0}/src/mapsmith/limits.py +0 -0
  90. {mapsmith-0.7.1 → mapsmith-0.9.0}/src/mapsmith/plans/__init__.py +0 -0
  91. {mapsmith-0.7.1 → mapsmith-0.9.0}/src/mapsmith/plans/executor.py +0 -0
  92. {mapsmith-0.7.1 → mapsmith-0.9.0}/src/mapsmith/plans/models.py +0 -0
  93. {mapsmith-0.7.1 → mapsmith-0.9.0}/src/mapsmith/plans/validator.py +0 -0
  94. {mapsmith-0.7.1 → mapsmith-0.9.0}/src/mapsmith/preview.py +0 -0
  95. {mapsmith-0.7.1 → mapsmith-0.9.0}/src/mapsmith/retrieval.py +0 -0
  96. {mapsmith-0.7.1 → mapsmith-0.9.0}/src/mapsmith/sql_policy.py +0 -0
  97. {mapsmith-0.7.1 → mapsmith-0.9.0}/src/mapsmith/stacks.py +0 -0
  98. {mapsmith-0.7.1 → mapsmith-0.9.0}/src/mapsmith/ui.py +0 -0
  99. {mapsmith-0.7.1 → mapsmith-0.9.0}/src/mapsmith/workspace.py +0 -0
  100. {mapsmith-0.7.1 → mapsmith-0.9.0}/tests/data/discovery_queries.json +0 -0
  101. {mapsmith-0.7.1 → mapsmith-0.9.0}/tests/test_ambiguous_layer.py +0 -0
  102. {mapsmith-0.7.1 → mapsmith-0.9.0}/tests/test_antimeridian.py +0 -0
  103. {mapsmith-0.7.1 → mapsmith-0.9.0}/tests/test_band_math.py +0 -0
  104. {mapsmith-0.7.1 → mapsmith-0.9.0}/tests/test_catalog.py +0 -0
  105. {mapsmith-0.7.1 → mapsmith-0.9.0}/tests/test_catalog_entry_spec.py +0 -0
  106. {mapsmith-0.7.1 → mapsmith-0.9.0}/tests/test_catalog_retrieval.py +0 -0
  107. {mapsmith-0.7.1 → mapsmith-0.9.0}/tests/test_centroid_convert.py +0 -0
  108. {mapsmith-0.7.1 → mapsmith-0.9.0}/tests/test_clip_reclassify_raster.py +0 -0
  109. {mapsmith-0.7.1 → mapsmith-0.9.0}/tests/test_dashboard.py +0 -0
  110. {mapsmith-0.7.1 → mapsmith-0.9.0}/tests/test_describe_raster.py +0 -0
  111. {mapsmith-0.7.1 → mapsmith-0.9.0}/tests/test_discovery_log.py +0 -0
  112. {mapsmith-0.7.1 → mapsmith-0.9.0}/tests/test_duckdb_sandbox.py +0 -0
  113. {mapsmith-0.7.1 → mapsmith-0.9.0}/tests/test_engines.py +0 -0
  114. {mapsmith-0.7.1 → mapsmith-0.9.0}/tests/test_environment.py +0 -0
  115. {mapsmith-0.7.1 → mapsmith-0.9.0}/tests/test_failure_manifest.py +0 -0
  116. {mapsmith-0.7.1 → mapsmith-0.9.0}/tests/test_gdal_policy.py +0 -0
  117. {mapsmith-0.7.1 → mapsmith-0.9.0}/tests/test_geodesy.py +0 -0
  118. {mapsmith-0.7.1 → mapsmith-0.9.0}/tests/test_geoparquet2.py +0 -0
  119. {mapsmith-0.7.1 → mapsmith-0.9.0}/tests/test_grid_registration.py +0 -0
  120. {mapsmith-0.7.1 → mapsmith-0.9.0}/tests/test_limits.py +0 -0
  121. {mapsmith-0.7.1 → mapsmith-0.9.0}/tests/test_lineage.py +0 -0
  122. {mapsmith-0.7.1 → mapsmith-0.9.0}/tests/test_measure_area.py +0 -0
  123. {mapsmith-0.7.1 → mapsmith-0.9.0}/tests/test_merge_simplify.py +0 -0
  124. {mapsmith-0.7.1 → mapsmith-0.9.0}/tests/test_nearest_explode.py +0 -0
  125. {mapsmith-0.7.1 → mapsmith-0.9.0}/tests/test_network.py +0 -0
  126. {mapsmith-0.7.1 → mapsmith-0.9.0}/tests/test_output_guard.py +0 -0
  127. {mapsmith-0.7.1 → mapsmith-0.9.0}/tests/test_overlay_dissolve.py +0 -0
  128. {mapsmith-0.7.1 → mapsmith-0.9.0}/tests/test_path_containment.py +0 -0
  129. {mapsmith-0.7.1 → mapsmith-0.9.0}/tests/test_plans.py +0 -0
  130. {mapsmith-0.7.1 → mapsmith-0.9.0}/tests/test_preview.py +0 -0
  131. {mapsmith-0.7.1 → mapsmith-0.9.0}/tests/test_raster_reproject_bands.py +0 -0
  132. {mapsmith-0.7.1 → mapsmith-0.9.0}/tests/test_resample_raster.py +0 -0
  133. {mapsmith-0.7.1 → mapsmith-0.9.0}/tests/test_retrieval.py +0 -0
  134. {mapsmith-0.7.1 → mapsmith-0.9.0}/tests/test_retrieval_at_scale.py +0 -0
  135. {mapsmith-0.7.1 → mapsmith-0.9.0}/tests/test_retrieval_degradation.py +0 -0
  136. {mapsmith-0.7.1 → mapsmith-0.9.0}/tests/test_sampling.py +0 -0
  137. {mapsmith-0.7.1 → mapsmith-0.9.0}/tests/test_select_and_extract.py +0 -0
  138. {mapsmith-0.7.1 → mapsmith-0.9.0}/tests/test_smoke.py +0 -0
  139. {mapsmith-0.7.1 → mapsmith-0.9.0}/tests/test_south_up_and_mixed.py +0 -0
  140. {mapsmith-0.7.1 → mapsmith-0.9.0}/tests/test_sql_axis_order.py +0 -0
  141. {mapsmith-0.7.1 → mapsmith-0.9.0}/tests/test_sql_credentials.py +0 -0
  142. {mapsmith-0.7.1 → mapsmith-0.9.0}/tests/test_stacks.py +0 -0
  143. {mapsmith-0.7.1 → mapsmith-0.9.0}/tests/test_summaries.py +0 -0
  144. {mapsmith-0.7.1 → mapsmith-0.9.0}/tests/test_summarize_points_in_polygons.py +0 -0
  145. {mapsmith-0.7.1 → mapsmith-0.9.0}/tests/test_terrain_derivatives.py +0 -0
  146. {mapsmith-0.7.1 → mapsmith-0.9.0}/tests/test_tier_a_operations.py +0 -0
  147. {mapsmith-0.7.1 → mapsmith-0.9.0}/tests/test_ui.py +0 -0
  148. {mapsmith-0.7.1 → mapsmith-0.9.0}/tests/test_verification_status.py +0 -0
  149. {mapsmith-0.7.1 → mapsmith-0.9.0}/tests/test_verify_repair.py +0 -0
  150. {mapsmith-0.7.1 → mapsmith-0.9.0}/tests/test_voronoi.py +0 -0
  151. {mapsmith-0.7.1 → mapsmith-0.9.0}/tests/test_whitebox.py +0 -0
  152. {mapsmith-0.7.1 → mapsmith-0.9.0}/tests/test_whitebox_derivatives.py +0 -0
  153. {mapsmith-0.7.1 → mapsmith-0.9.0}/tests/test_whitebox_encoding.py +0 -0
  154. {mapsmith-0.7.1 → mapsmith-0.9.0}/tests/test_whitebox_geographic_refusal.py +0 -0
  155. {mapsmith-0.7.1 → mapsmith-0.9.0}/tests/test_worked_example.py +0 -0
  156. {mapsmith-0.7.1 → mapsmith-0.9.0}/tests/test_workspace.py +0 -0
@@ -33,6 +33,7 @@ data/
33
33
  tests/data/*
34
34
  !tests/data/manifest_spec_validator.py
35
35
  !tests/data/manifest-v1.schema.json
36
+ !tests/data/manifest_spec_multi_file_digest.py
36
37
  # The discovery benchmark: 95 queries written by a second model from work
37
38
  # scenarios, never from this catalog. It is tracked BECAUSE it is a dataset — an
38
39
  # evaluation set nobody can inspect is a claim, not a measurement.
@@ -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.7.1",
4
+ "version": "0.9.0",
5
5
  "creators": [
6
6
  {"name": "The MapSmith project"}
7
7
  ],
@@ -6,7 +6,272 @@ All notable changes to MapSmith are documented here, in the format of
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
- Nothing yet.
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
+
113
+ ## [0.8.0] - 2026-10-05
114
+
115
+ Records declare `spec_version` `1.0.0-draft.10`. `draft.9` defines the digest
116
+ of a dataset made of several files, which is what the first fix below
117
+ implements; `draft.10` gives each input a role and a configuration of its own.
118
+ **One key moves**: a second input's own georeferencing facts,
119
+ `inputs[].x-mapsmith:environment` on 0.7.x, are now `inputs[].environment`.
120
+
121
+ ### Added
122
+
123
+ - **`cluster_points_by_distance`: group points chained together within a
124
+ distance.** "Group everything within half a kilometre" is single linkage, not
125
+ thinning: two points share a cluster when a chain of points joins them with
126
+ every link at most the distance long, so chains merge, and every point is kept
127
+ with its `cluster_id` and `cluster_size`. `thin_points` keeps one point per
128
+ crowd, and its count is close to the number of groups but not equal to it. A
129
+ check re-reads the written file and re-derives the chains by a second
130
+ algorithm. A geographic CRS is refused, and so are lines, polygons and
131
+ multipart points, which a point cluster has no single position for.
132
+ - **`list_operations` takes a `geometry`: what the caller's layer holds**, as
133
+ `point`, `line` or `polygon`, or a list of them for a mixed layer. The
134
+ catalogue now has 31 operations taking one vector layer to another, one more
135
+ than the size it hands over whole, so a caller declaring everything it knew
136
+ got a shortlist back for the most common request there is; with the geometry
137
+ declared, the right operation is in the answer for 116 of the 118 benchmark
138
+ requests again. An entry declares the families it accepts only where its code
139
+ refuses the others (fifteen do), an entry without a declaration is kept for
140
+ every family, and a test calls each declared entry with every family it
141
+ leaves out and requires the refusal.
142
+ - **`benchmarks/choice_at_scale.py`**, with every choice it scores in
143
+ `benchmarks/choice_at_scale.json`: each of the 118 requests handed to Claude
144
+ Opus 5.5 in sets of 30, 40 and 50 candidates and compared with itself. From
145
+ 40 it chose right 6.9 points less often than from 30, from 50 5.1 points
146
+ less, so the size `list_operations` hands over whole stays 30, and a test
147
+ holds it at or below the largest size measured without loss.
148
+ - **Each input of an operation that reads more than one dataset says which
149
+ argument read it** (`inputs[].argument`, spec `draft.10`). The order of
150
+ `inputs` is not significant, so until now a weighted zonal statistic with
151
+ values and weights swapped produced a record that differed only in file
152
+ names. Eighteen operations name their inputs (twenty code paths:
153
+ `spatial_join` has three engines), with the names the input checks
154
+ already use in `verification[].argument`, so the two can be joined; a
155
+ single-input record is unchanged.
156
+
157
+ - **`get_lineage` takes a `layer`.** The history of one layer of a file
158
+ geodatabase now starts from the geodatabase itself: its record sits beside
159
+ the container (spec section 3.1), and the walk matches the layer at every hop
160
+ into a container. A layer given for a path that is not a container is
161
+ refused rather than ignored.
162
+
163
+ ### Changed
164
+
165
+ - **`get_lineage` steps carry `inputs[].argument`, `inputs[].environment` and
166
+ `output.layer`** when the records hold them.
167
+ - **A shapefile output whose member files are ambiguous** (`out.dbf` beside
168
+ `out.DBF`, possible only on a case-sensitive filesystem) is written with a
169
+ manifest that has no `output` digest and a note saying why, instead of a
170
+ digest that would choose one of the two.
171
+ - **`inputs[].x-mapsmith:environment` is `inputs[].environment`**, the field
172
+ spec `draft.10` defines for the configuration that changed how one input was
173
+ read. Same content; only the name moves, and a reader of 0.7.x records reads
174
+ the old one.
175
+ - **With more than one input, no file's facts sit on the record's
176
+ `environment`, the first input's included.** A sidecar beside the first
177
+ raster was recorded there, which the format reads as true of the whole run,
178
+ since the order of `inputs` means nothing: in a `clip_raster` it read as true
179
+ of the mask. It is now on that input's own `environment`, and only process
180
+ settings stay on the record. Seven operations can meet it (clip_raster,
181
+ sample_raster_at_points, elevation_profile, least_cost_path, viewshed,
182
+ watershed, zonal_statistics), and only when a sidecar is present. A
183
+ single-input record is unchanged.
184
+
185
+ ### Fixed
186
+
187
+ - **`cluster_points_by_distance` is bounded.** The pairs of points within the
188
+ distance grow with the square of the points when the distance is large, and
189
+ 6,000 points at a distance of 1e9 took two minutes and 2.3 GB. The pairs are
190
+ now found in blocks and the call is refused once they pass
191
+ `MAPSMITH_MAX_SAMPLES`, the operator's limit, before memory is spent.
192
+ `hot_spots` with `weights="distance_band"` uses the same query: its weights
193
+ were a double loop over every pair of features in Python, quadratic at any
194
+ band, and a band that is not a finite positive number is now refused.
195
+ - **`get_lineage` no longer hashes every shapefile in the workspace.** Since
196
+ the draft.9 digest rule it computed, while indexing, the other rule's digest
197
+ of every shapefile record's output, whatever the chain asked about. A
198
+ shapefile is now hashed only when a hop misses on it.
199
+ - **A Windows junction inside a `.gdb` is not followed.** The digest of a
200
+ directory container walked into junctions, which `is_symlink()` does not
201
+ report, so files outside the container could enter its digest. Section 3.3 of
202
+ the specification says links are neither members nor followed. The size used
203
+ for the re-hash cap now counts the same files as the digest.
204
+ - **A single-layer `.gdb` read by any operation records its layer.** Every
205
+ operation now reads a single-layer file geodatabase, not only
206
+ `extract_layer`, and the input records the layer it read: without it, a
207
+ lineage walk meeting the record of that layer stopped with a false "records
208
+ claim this container's digest, but for other layers".
209
+ - **`get_lineage` refuses a `layer` holding `:` or a control character.** On
210
+ Windows `:` in a file name opens an alternate data stream of another file.
211
+ - **`thin_points` refuses a distance that is not a finite positive number**,
212
+ like `cluster_points_by_distance`; and `least_cost_path` accepts a start or
213
+ end given as a single-part MultiPoint, which it used to count as no point.
214
+ - **`zonal_statistics` refuses zones that are not polygons.** Line zones were
215
+ accepted and returned the line's length in pixels as the `count`, with a
216
+ mean over the cells it crossed: a plausible table, and not a zonal
217
+ statistic. `count_in_polygons` and `summarize_points_in_polygons` refuse a
218
+ polygon layer holding anything else the same way. Found by the test that
219
+ holds the new geometry declarations to the code.
220
+ - **`least_cost_path` is found by a caller holding points.** It reads a cost
221
+ raster and two point layers but declared its input as raster only, so a
222
+ caller who said, correctly, that it held a vector layer never saw it. A start
223
+ or end layer holding a line or a polygon beside its point is now refused; it
224
+ used to pass with the extra feature ignored.
225
+ - **`snap_layer` snaps to a polygon or multipart reference.** Both crashed with
226
+ a raw `NotImplementedError` from shapely; every vertex of every part, holes
227
+ included, is now a target.
228
+ - **A feature with no geometry is refused, with a count, by `thin_points`,
229
+ `sample_raster_at_points`, `points_along_lines` and `line_intersections`.**
230
+ They crashed on it with an `AttributeError`, "cannot convert float NaN to
231
+ integer" and a failed sort.
232
+ - **A shapefile's digest covers its attributes and its CRS.** An input or
233
+ output shapefile was digested as its `.shp` alone, so a record kept matching
234
+ a dataset whose `.dbf` (attributes) or `.prj` (CRS) had been rewritten, while
235
+ it said `sha256` is of the bytes. The digest is now the listing of the
236
+ `.shp`, `.shx`, `.dbf`, `.prj` and `.cpg` files, as section 3.3 of the
237
+ specification defines it since draft.9; a file geodatabase input is digested
238
+ the same way, lock files excluded. `get_lineage` recomputes digests with the
239
+ same function, so a chain written by this release still resolves. A record
240
+ written by an earlier release for a shapefile carries the old digest:
241
+ `get_lineage` compares it under the rule it was written with, so an
242
+ unchanged shapefile is not reported as edited, and a chain that crosses
243
+ from a record written by 0.7.1 to one written now still resolves. A record
244
+ written before this release can only vouch for the `.shp`: an edit to the
245
+ attributes or the CRS made after it was written stays invisible to it, so
246
+ such a hop is reported as `reverified_shp_only`, not `reverified`, and a
247
+ history that contains one is not `verified`.
248
+ - **`extract_layer` works on a file geodatabase.** The catalog listed `.gdb`
249
+ among its containers, and the input digest opened the path as a file: a
250
+ geodatabase is a directory, so the operation died before writing anything.
251
+ - **`get_lineage` matches the layer when a digest belongs to a container.**
252
+ Two records for two layers of one geodatabase share the container's digest
253
+ (spec section 6, draft.9); a hop into a container now resolves to the record
254
+ for the layer it names, and stops with `other_layer` rather than attribute
255
+ one layer to the operation that wrote another. It re-checks a layer's record
256
+ against the container it sits beside (`<container>.<layer>.provenance.json`,
257
+ spec section 3.1), and two layers of one container reached on two branches
258
+ are two histories.
259
+ - **A URI in a manifest path keeps its `//`, so its credentials are masked.**
260
+ The path normaliser collapsed `https://user:pw@host/x` to `https:/user:pw@…`,
261
+ and the redaction of URI credentials looks for `://`: the password would have
262
+ stayed in the record. Not reachable while remote paths are refused, which is
263
+ the default; found in the 0.7.0 audit. The weights raster of
264
+ `zonal_statistics` is recorded through the same normaliser now.
265
+ - **A redaction in the environment of a secondary input is declared.** Masked
266
+ values there did not raise `parameters_redacted`, so the record changed a
267
+ value without saying so.
268
+ - **The package metadata names the Copernicus NOTICE** beside the AGPL licence,
269
+ since the source distribution ships the two Copernicus clips the notebook
270
+ reads.
271
+ - **The links on the PyPI page resolve.** PyPI renders the README with nothing
272
+ to resolve a relative link against, so every link to a document of the repo
273
+ and the map-panel screenshot answered 404 there. The build now rewrites them
274
+ to GitHub URLs pinned to the release tag; on GitHub the README is unchanged.
10
275
 
11
276
  ## [0.7.1] - 2026-09-26
12
277
 
@@ -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.7.1"
16
- date-released: "2026-09-26"
15
+ version: "0.9.0"
16
+ date-released: "2026-10-08"
17
17
  keywords:
18
18
  - GIS
19
19
  - geospatial
@@ -5,7 +5,7 @@ MapSmith is professional-grade geoprocessing with **verifiable provenance**, rea
5
5
  ## Non-negotiable invariants
6
6
 
7
7
  1. **Deterministic outputs only**: geometry and numbers always come from engine executions, never from a model.
8
- 2. **Provenance on every writer**: any function that writes a dataset emits `<output>.provenance.json` via `ProvenanceRecord` (inputs with sha256, exact parameters, motivated CRS decisions, engine+version).
8
+ 2. **Provenance on every writer**: any function that writes a dataset emits `<output>.provenance.json` via `ProvenanceRecord` (inputs with sha256, exact parameters, motivated CRS decisions, engine+version). The digest is `provenance.dataset_sha256` -- for a shapefile or a `.gdb`, the listing digest of spec section 3.3 -- never `sha256_of` on a dataset path, which hashes one file and missed a shapefile's attributes and CRS until 0.7.1.
9
9
  3. **Deterministic verification**: the order is the invariant — verify, repair what is mechanically repairable, write the manifest, and *only then* enforce, so the audit trail survives the error instead of being lost with it. `verify.audited(...)` does all four and is how a new writer is written. A minority still open-code the same sequence inline; that is a migration in progress rather than a second sanctioned shape, and two tests hold the line so this sentence does not have to carry a number that rots: one fails if any hand-written writer skips `verify.enforce`, the other ratchets their count downwards only. (It used to say writers go through `audited` "so the invariant cannot be bypassed", which was false for seventeen of fifty-seven — and one of them enforced nothing at all.) Preconditions: `verify.verify_loaded_inputs(...)` (per input: CRS presence is CRITICAL, emptiness is a warning) **before** any CRS alignment, and `verify.verify_input_pairs(...)` (extent overlap) **after** — get that order wrong and a raw pyproj error beats our own message. Never re-read data just to check it. `verify.audit_on_failure(...)` keeps the diagnosis on disk when the engine crashes. Mechanical repair (`make_valid`) is bounded (2 rounds), writes to a temp file and swaps atomically, uses `gdf.geometry.name` (never assumes "geometry"), refuses multi-layer containers and multi-file formats, and lands in `repairs` — in the manifest *and* in the tool result. Anything needing judgement (empty results, CRS choices) is a hinted check in `warnings`, never silently fixed. `on_empty` is `fail` only where emptiness is provably a bug (buffer, reproject), `warn` where it is legitimate but suspicious (clip, join).
10
10
  4. **Explicit CRS discipline**: reject inputs without a CRS (helpful message, naming the declaration when the file has one MapSmith will not resolve); never run metric ops on geographic CRS without a recorded reprojection decision. Resolving a CRS and labelling one are different jobs and live apart: `readers.native_crs` returns coordinate systems, `verify.crs_label` returns strings for manifests, and comparisons go through `verify.same_crs` — a label fed back in as a CRS is how a declared CRS got dropped (#28). The keys of `crs_decisions` are the specification's (`analysis_crs`, `source_crs`, `target_crs`, `transformation`, `reason`) or they carry the `x-mapsmith:` prefix that says they are ours — stricter than section 3.5, which only recommends a prefix, and a house rule because a SHOULD with nothing reading it let two operations invent synonyms of `analysis_crs` for a week. `tests/test_verify.py` derives the recommended set from the vendored schema and sweeps every writing site in the source; `tests/test_showcase.py` does the same for every manifest shown on the README or in the gallery, which the source sweep cannot see.
11
11
  5. **GPL boundary**: QGIS/GRASS only via subprocess (CLI/files/JSON), never in-process imports.