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.
- {mapsmith-0.7.1 → mapsmith-0.9.0}/.gitignore +1 -0
- {mapsmith-0.7.1 → mapsmith-0.9.0}/.zenodo.json +1 -1
- {mapsmith-0.7.1 → mapsmith-0.9.0}/CHANGELOG.md +266 -1
- {mapsmith-0.7.1 → mapsmith-0.9.0}/CITATION.cff +2 -2
- {mapsmith-0.7.1 → mapsmith-0.9.0}/CLAUDE.md +1 -1
- mapsmith-0.7.1/README.md → mapsmith-0.9.0/PKG-INFO +1403 -1298
- mapsmith-0.7.1/PKG-INFO → mapsmith-0.9.0/README.md +1360 -1340
- {mapsmith-0.7.1 → mapsmith-0.9.0}/SECURITY.md +47 -5
- {mapsmith-0.7.1 → mapsmith-0.9.0}/docs/argleton-run.json +11 -11
- {mapsmith-0.7.1 → mapsmith-0.9.0}/docs/benchmarks.md +12 -6
- {mapsmith-0.7.1 → mapsmith-0.9.0}/docs/catalog-entry-spec.md +10 -9
- {mapsmith-0.7.1 → mapsmith-0.9.0}/docs/manifest-vocabulary.md +12 -6
- {mapsmith-0.7.1 → mapsmith-0.9.0}/examples/01_verified_geoprocessing.ipynb +25 -25
- {mapsmith-0.7.1 → mapsmith-0.9.0}/examples/02_terrain_hydrology.ipynb +36 -36
- {mapsmith-0.7.1 → mapsmith-0.9.0}/examples/03_validated_plans.ipynb +23 -23
- {mapsmith-0.7.1 → mapsmith-0.9.0}/examples/04_copernicus_terrain_vegetation.ipynb +31 -31
- {mapsmith-0.7.1 → mapsmith-0.9.0}/funding.json +1 -1
- {mapsmith-0.7.1 → mapsmith-0.9.0}/pyproject.toml +38 -6
- {mapsmith-0.7.1 → mapsmith-0.9.0}/schema/operation.schema.json +13 -0
- {mapsmith-0.7.1 → mapsmith-0.9.0}/server.json +3 -3
- {mapsmith-0.7.1 → mapsmith-0.9.0}/site/build.py +20 -5
- {mapsmith-0.7.1 → mapsmith-0.9.0}/site/index.template.html +22 -16
- {mapsmith-0.7.1 → mapsmith-0.9.0}/src/mapsmith/__init__.py +1 -1
- {mapsmith-0.7.1 → mapsmith-0.9.0}/src/mapsmith/catalog.py +248 -25
- mapsmith-0.9.0/src/mapsmith/datum.py +865 -0
- {mapsmith-0.7.1 → mapsmith-0.9.0}/src/mapsmith/engines/duckdb_engine.py +4 -2
- {mapsmith-0.7.1 → mapsmith-0.9.0}/src/mapsmith/engines/linework.py +29 -16
- {mapsmith-0.7.1 → mapsmith-0.9.0}/src/mapsmith/engines/network.py +26 -10
- {mapsmith-0.7.1 → mapsmith-0.9.0}/src/mapsmith/engines/raster.py +83 -25
- {mapsmith-0.7.1 → mapsmith-0.9.0}/src/mapsmith/engines/sampling.py +19 -6
- {mapsmith-0.7.1 → mapsmith-0.9.0}/src/mapsmith/engines/sedona_engine.py +4 -2
- {mapsmith-0.7.1 → mapsmith-0.9.0}/src/mapsmith/engines/spatial_stats.py +301 -14
- {mapsmith-0.7.1 → mapsmith-0.9.0}/src/mapsmith/engines/summaries.py +2 -2
- {mapsmith-0.7.1 → mapsmith-0.9.0}/src/mapsmith/engines/vector.py +4187 -3360
- {mapsmith-0.7.1 → mapsmith-0.9.0}/src/mapsmith/engines/whitebox_engine.py +8 -6
- {mapsmith-0.7.1 → mapsmith-0.9.0}/src/mapsmith/lineage.py +273 -34
- {mapsmith-0.7.1 → mapsmith-0.9.0}/src/mapsmith/plans/registry.py +28 -0
- {mapsmith-0.7.1 → mapsmith-0.9.0}/src/mapsmith/provenance.py +420 -41
- {mapsmith-0.7.1 → mapsmith-0.9.0}/src/mapsmith/readers.py +21 -0
- {mapsmith-0.7.1 → mapsmith-0.9.0}/src/mapsmith/server.py +9 -1
- {mapsmith-0.7.1 → mapsmith-0.9.0}/src/mapsmith/verify.py +35 -6
- {mapsmith-0.7.1 → mapsmith-0.9.0}/tests/conftest.py +50 -0
- {mapsmith-0.7.1 → mapsmith-0.9.0}/tests/data/manifest-v1.schema.json +20 -4
- mapsmith-0.9.0/tests/data/manifest_spec_multi_file_digest.py +84 -0
- {mapsmith-0.7.1 → mapsmith-0.9.0}/tests/data/manifest_spec_validator.py +400 -386
- mapsmith-0.9.0/tests/test_apportion_by_area.py +861 -0
- {mapsmith-0.7.1 → mapsmith-0.9.0}/tests/test_catalog_applicability.py +11 -1
- mapsmith-0.9.0/tests/test_choice_at_scale.py +48 -0
- {mapsmith-0.7.1 → mapsmith-0.9.0}/tests/test_contours_and_cost.py +40 -1
- mapsmith-0.9.0/tests/test_dataset_digest.py +214 -0
- {mapsmith-0.7.1 → mapsmith-0.9.0}/tests/test_datum.py +519 -9
- {mapsmith-0.7.1 → mapsmith-0.9.0}/tests/test_discovery_contract.py +7 -3
- {mapsmith-0.7.1 → mapsmith-0.9.0}/tests/test_discovery_report.py +7 -4
- mapsmith-0.9.0/tests/test_extract_from_gdb.py +89 -0
- mapsmith-0.9.0/tests/test_failure_record_and_earlier_bytes.py +215 -0
- mapsmith-0.9.0/tests/test_geometry_declarations.py +272 -0
- mapsmith-0.9.0/tests/test_lineage_digest_rules.py +269 -0
- {mapsmith-0.7.1 → mapsmith-0.9.0}/tests/test_linework.py +35 -1
- {mapsmith-0.7.1 → mapsmith-0.9.0}/tests/test_published_figures.py +27 -11
- {mapsmith-0.7.1 → mapsmith-0.9.0}/tests/test_raster.py +32 -4
- {mapsmith-0.7.1 → mapsmith-0.9.0}/tests/test_redaction.py +38 -0
- {mapsmith-0.7.1 → mapsmith-0.9.0}/tests/test_showcase.py +79 -2
- {mapsmith-0.7.1 → mapsmith-0.9.0}/tests/test_spatial_stats.py +226 -1
- {mapsmith-0.7.1 → mapsmith-0.9.0}/tests/test_verify.py +2516 -2513
- mapsmith-0.7.1/src/mapsmith/datum.py +0 -308
- {mapsmith-0.7.1 → mapsmith-0.9.0}/.dockerignore +0 -0
- {mapsmith-0.7.1 → mapsmith-0.9.0}/CONTRIBUTING.md +0 -0
- {mapsmith-0.7.1 → mapsmith-0.9.0}/Dockerfile +0 -0
- {mapsmith-0.7.1 → mapsmith-0.9.0}/LICENSE +0 -0
- {mapsmith-0.7.1 → mapsmith-0.9.0}/MANIFESTO.md +0 -0
- {mapsmith-0.7.1 → mapsmith-0.9.0}/TRADEMARKS.md +0 -0
- {mapsmith-0.7.1 → mapsmith-0.9.0}/docker-compose.yml +0 -0
- {mapsmith-0.7.1 → mapsmith-0.9.0}/examples/README.md +0 -0
- {mapsmith-0.7.1 → mapsmith-0.9.0}/examples/fixtures/COPERNICUS-NOTICE.md +0 -0
- {mapsmith-0.7.1 → mapsmith-0.9.0}/examples/fixtures/fetch_copernicus.py +0 -0
- {mapsmith-0.7.1 → mapsmith-0.9.0}/examples/fixtures/monte_baldo_dem.tif +0 -0
- {mapsmith-0.7.1 → mapsmith-0.9.0}/examples/fixtures/monte_baldo_s2.tif +0 -0
- {mapsmith-0.7.1 → mapsmith-0.9.0}/examples/fixtures/mount_st_helens_dem.tif +0 -0
- {mapsmith-0.7.1 → mapsmith-0.9.0}/glama.json +0 -0
- {mapsmith-0.7.1 → mapsmith-0.9.0}/src/mapsmith/antimeridian.py +0 -0
- {mapsmith-0.7.1 → mapsmith-0.9.0}/src/mapsmith/discovery_log.py +0 -0
- {mapsmith-0.7.1 → mapsmith-0.9.0}/src/mapsmith/engines/__init__.py +0 -0
- {mapsmith-0.7.1 → mapsmith-0.9.0}/src/mapsmith/engines/dispatch.py +0 -0
- {mapsmith-0.7.1 → mapsmith-0.9.0}/src/mapsmith/engines/esri.py +0 -0
- {mapsmith-0.7.1 → mapsmith-0.9.0}/src/mapsmith/engines/geodesy.py +0 -0
- {mapsmith-0.7.1 → mapsmith-0.9.0}/src/mapsmith/gdal_policy.py +0 -0
- {mapsmith-0.7.1 → mapsmith-0.9.0}/src/mapsmith/grid.py +0 -0
- {mapsmith-0.7.1 → mapsmith-0.9.0}/src/mapsmith/jobs.py +0 -0
- {mapsmith-0.7.1 → mapsmith-0.9.0}/src/mapsmith/limits.py +0 -0
- {mapsmith-0.7.1 → mapsmith-0.9.0}/src/mapsmith/plans/__init__.py +0 -0
- {mapsmith-0.7.1 → mapsmith-0.9.0}/src/mapsmith/plans/executor.py +0 -0
- {mapsmith-0.7.1 → mapsmith-0.9.0}/src/mapsmith/plans/models.py +0 -0
- {mapsmith-0.7.1 → mapsmith-0.9.0}/src/mapsmith/plans/validator.py +0 -0
- {mapsmith-0.7.1 → mapsmith-0.9.0}/src/mapsmith/preview.py +0 -0
- {mapsmith-0.7.1 → mapsmith-0.9.0}/src/mapsmith/retrieval.py +0 -0
- {mapsmith-0.7.1 → mapsmith-0.9.0}/src/mapsmith/sql_policy.py +0 -0
- {mapsmith-0.7.1 → mapsmith-0.9.0}/src/mapsmith/stacks.py +0 -0
- {mapsmith-0.7.1 → mapsmith-0.9.0}/src/mapsmith/ui.py +0 -0
- {mapsmith-0.7.1 → mapsmith-0.9.0}/src/mapsmith/workspace.py +0 -0
- {mapsmith-0.7.1 → mapsmith-0.9.0}/tests/data/discovery_queries.json +0 -0
- {mapsmith-0.7.1 → mapsmith-0.9.0}/tests/test_ambiguous_layer.py +0 -0
- {mapsmith-0.7.1 → mapsmith-0.9.0}/tests/test_antimeridian.py +0 -0
- {mapsmith-0.7.1 → mapsmith-0.9.0}/tests/test_band_math.py +0 -0
- {mapsmith-0.7.1 → mapsmith-0.9.0}/tests/test_catalog.py +0 -0
- {mapsmith-0.7.1 → mapsmith-0.9.0}/tests/test_catalog_entry_spec.py +0 -0
- {mapsmith-0.7.1 → mapsmith-0.9.0}/tests/test_catalog_retrieval.py +0 -0
- {mapsmith-0.7.1 → mapsmith-0.9.0}/tests/test_centroid_convert.py +0 -0
- {mapsmith-0.7.1 → mapsmith-0.9.0}/tests/test_clip_reclassify_raster.py +0 -0
- {mapsmith-0.7.1 → mapsmith-0.9.0}/tests/test_dashboard.py +0 -0
- {mapsmith-0.7.1 → mapsmith-0.9.0}/tests/test_describe_raster.py +0 -0
- {mapsmith-0.7.1 → mapsmith-0.9.0}/tests/test_discovery_log.py +0 -0
- {mapsmith-0.7.1 → mapsmith-0.9.0}/tests/test_duckdb_sandbox.py +0 -0
- {mapsmith-0.7.1 → mapsmith-0.9.0}/tests/test_engines.py +0 -0
- {mapsmith-0.7.1 → mapsmith-0.9.0}/tests/test_environment.py +0 -0
- {mapsmith-0.7.1 → mapsmith-0.9.0}/tests/test_failure_manifest.py +0 -0
- {mapsmith-0.7.1 → mapsmith-0.9.0}/tests/test_gdal_policy.py +0 -0
- {mapsmith-0.7.1 → mapsmith-0.9.0}/tests/test_geodesy.py +0 -0
- {mapsmith-0.7.1 → mapsmith-0.9.0}/tests/test_geoparquet2.py +0 -0
- {mapsmith-0.7.1 → mapsmith-0.9.0}/tests/test_grid_registration.py +0 -0
- {mapsmith-0.7.1 → mapsmith-0.9.0}/tests/test_limits.py +0 -0
- {mapsmith-0.7.1 → mapsmith-0.9.0}/tests/test_lineage.py +0 -0
- {mapsmith-0.7.1 → mapsmith-0.9.0}/tests/test_measure_area.py +0 -0
- {mapsmith-0.7.1 → mapsmith-0.9.0}/tests/test_merge_simplify.py +0 -0
- {mapsmith-0.7.1 → mapsmith-0.9.0}/tests/test_nearest_explode.py +0 -0
- {mapsmith-0.7.1 → mapsmith-0.9.0}/tests/test_network.py +0 -0
- {mapsmith-0.7.1 → mapsmith-0.9.0}/tests/test_output_guard.py +0 -0
- {mapsmith-0.7.1 → mapsmith-0.9.0}/tests/test_overlay_dissolve.py +0 -0
- {mapsmith-0.7.1 → mapsmith-0.9.0}/tests/test_path_containment.py +0 -0
- {mapsmith-0.7.1 → mapsmith-0.9.0}/tests/test_plans.py +0 -0
- {mapsmith-0.7.1 → mapsmith-0.9.0}/tests/test_preview.py +0 -0
- {mapsmith-0.7.1 → mapsmith-0.9.0}/tests/test_raster_reproject_bands.py +0 -0
- {mapsmith-0.7.1 → mapsmith-0.9.0}/tests/test_resample_raster.py +0 -0
- {mapsmith-0.7.1 → mapsmith-0.9.0}/tests/test_retrieval.py +0 -0
- {mapsmith-0.7.1 → mapsmith-0.9.0}/tests/test_retrieval_at_scale.py +0 -0
- {mapsmith-0.7.1 → mapsmith-0.9.0}/tests/test_retrieval_degradation.py +0 -0
- {mapsmith-0.7.1 → mapsmith-0.9.0}/tests/test_sampling.py +0 -0
- {mapsmith-0.7.1 → mapsmith-0.9.0}/tests/test_select_and_extract.py +0 -0
- {mapsmith-0.7.1 → mapsmith-0.9.0}/tests/test_smoke.py +0 -0
- {mapsmith-0.7.1 → mapsmith-0.9.0}/tests/test_south_up_and_mixed.py +0 -0
- {mapsmith-0.7.1 → mapsmith-0.9.0}/tests/test_sql_axis_order.py +0 -0
- {mapsmith-0.7.1 → mapsmith-0.9.0}/tests/test_sql_credentials.py +0 -0
- {mapsmith-0.7.1 → mapsmith-0.9.0}/tests/test_stacks.py +0 -0
- {mapsmith-0.7.1 → mapsmith-0.9.0}/tests/test_summaries.py +0 -0
- {mapsmith-0.7.1 → mapsmith-0.9.0}/tests/test_summarize_points_in_polygons.py +0 -0
- {mapsmith-0.7.1 → mapsmith-0.9.0}/tests/test_terrain_derivatives.py +0 -0
- {mapsmith-0.7.1 → mapsmith-0.9.0}/tests/test_tier_a_operations.py +0 -0
- {mapsmith-0.7.1 → mapsmith-0.9.0}/tests/test_ui.py +0 -0
- {mapsmith-0.7.1 → mapsmith-0.9.0}/tests/test_verification_status.py +0 -0
- {mapsmith-0.7.1 → mapsmith-0.9.0}/tests/test_verify_repair.py +0 -0
- {mapsmith-0.7.1 → mapsmith-0.9.0}/tests/test_voronoi.py +0 -0
- {mapsmith-0.7.1 → mapsmith-0.9.0}/tests/test_whitebox.py +0 -0
- {mapsmith-0.7.1 → mapsmith-0.9.0}/tests/test_whitebox_derivatives.py +0 -0
- {mapsmith-0.7.1 → mapsmith-0.9.0}/tests/test_whitebox_encoding.py +0 -0
- {mapsmith-0.7.1 → mapsmith-0.9.0}/tests/test_whitebox_geographic_refusal.py +0 -0
- {mapsmith-0.7.1 → mapsmith-0.9.0}/tests/test_worked_example.py +0 -0
- {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.
|
|
@@ -6,7 +6,272 @@ All notable changes to MapSmith are documented here, in the format of
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
-
|
|
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.
|
|
16
|
-
date-released: "2026-
|
|
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.
|