mapsmith 0.7.0__tar.gz → 0.8.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 (153) hide show
  1. {mapsmith-0.7.0 → mapsmith-0.8.0}/.gitignore +1 -0
  2. {mapsmith-0.7.0 → mapsmith-0.8.0}/.zenodo.json +1 -1
  3. {mapsmith-0.7.0 → mapsmith-0.8.0}/CHANGELOG.md +192 -1
  4. {mapsmith-0.7.0 → mapsmith-0.8.0}/CITATION.cff +2 -2
  5. {mapsmith-0.7.0 → mapsmith-0.8.0}/CLAUDE.md +1 -1
  6. mapsmith-0.7.0/README.md → mapsmith-0.8.0/PKG-INFO +1387 -1298
  7. mapsmith-0.7.0/PKG-INFO → mapsmith-0.8.0/README.md +1344 -1340
  8. {mapsmith-0.7.0 → mapsmith-0.8.0}/SECURITY.md +34 -0
  9. {mapsmith-0.7.0 → mapsmith-0.8.0}/docs/argleton-run.json +11 -14
  10. {mapsmith-0.7.0 → mapsmith-0.8.0}/docs/benchmarks.md +7 -6
  11. {mapsmith-0.7.0 → mapsmith-0.8.0}/docs/catalog-entry-spec.md +10 -9
  12. {mapsmith-0.7.0 → mapsmith-0.8.0}/docs/manifest-vocabulary.md +179 -179
  13. {mapsmith-0.7.0 → mapsmith-0.8.0}/examples/01_verified_geoprocessing.ipynb +25 -25
  14. {mapsmith-0.7.0 → mapsmith-0.8.0}/examples/02_terrain_hydrology.ipynb +36 -36
  15. {mapsmith-0.7.0 → mapsmith-0.8.0}/examples/03_validated_plans.ipynb +23 -23
  16. {mapsmith-0.7.0 → mapsmith-0.8.0}/examples/04_copernicus_terrain_vegetation.ipynb +31 -31
  17. {mapsmith-0.7.0 → mapsmith-0.8.0}/funding.json +1 -1
  18. {mapsmith-0.7.0 → mapsmith-0.8.0}/pyproject.toml +35 -3
  19. {mapsmith-0.7.0 → mapsmith-0.8.0}/schema/operation.schema.json +13 -0
  20. {mapsmith-0.7.0 → mapsmith-0.8.0}/server.json +3 -3
  21. {mapsmith-0.7.0 → mapsmith-0.8.0}/site/build.py +20 -5
  22. {mapsmith-0.7.0 → mapsmith-0.8.0}/site/index.template.html +22 -15
  23. {mapsmith-0.7.0 → mapsmith-0.8.0}/src/mapsmith/__init__.py +1 -1
  24. {mapsmith-0.7.0 → mapsmith-0.8.0}/src/mapsmith/catalog.py +180 -25
  25. {mapsmith-0.7.0 → mapsmith-0.8.0}/src/mapsmith/engines/duckdb_engine.py +4 -2
  26. {mapsmith-0.7.0 → mapsmith-0.8.0}/src/mapsmith/engines/linework.py +40 -15
  27. {mapsmith-0.7.0 → mapsmith-0.8.0}/src/mapsmith/engines/network.py +20 -8
  28. {mapsmith-0.7.0 → mapsmith-0.8.0}/src/mapsmith/engines/raster.py +23 -8
  29. {mapsmith-0.7.0 → mapsmith-0.8.0}/src/mapsmith/engines/sampling.py +23 -4
  30. {mapsmith-0.7.0 → mapsmith-0.8.0}/src/mapsmith/engines/sedona_engine.py +4 -2
  31. {mapsmith-0.7.0 → mapsmith-0.8.0}/src/mapsmith/engines/spatial_stats.py +301 -14
  32. {mapsmith-0.7.0 → mapsmith-0.8.0}/src/mapsmith/engines/vector.py +3415 -3360
  33. {mapsmith-0.7.0 → mapsmith-0.8.0}/src/mapsmith/engines/whitebox_engine.py +6 -4
  34. mapsmith-0.8.0/src/mapsmith/limits.py +58 -0
  35. {mapsmith-0.7.0 → mapsmith-0.8.0}/src/mapsmith/lineage.py +273 -34
  36. {mapsmith-0.7.0 → mapsmith-0.8.0}/src/mapsmith/plans/registry.py +14 -0
  37. {mapsmith-0.7.0 → mapsmith-0.8.0}/src/mapsmith/plans/validator.py +30 -1
  38. {mapsmith-0.7.0 → mapsmith-0.8.0}/src/mapsmith/provenance.py +278 -32
  39. {mapsmith-0.7.0 → mapsmith-0.8.0}/src/mapsmith/readers.py +21 -0
  40. {mapsmith-0.7.0 → mapsmith-0.8.0}/src/mapsmith/server.py +41 -5
  41. {mapsmith-0.7.0 → mapsmith-0.8.0}/tests/conftest.py +50 -0
  42. {mapsmith-0.7.0 → mapsmith-0.8.0}/tests/data/manifest-v1.schema.json +20 -4
  43. mapsmith-0.8.0/tests/data/manifest_spec_multi_file_digest.py +84 -0
  44. {mapsmith-0.7.0 → mapsmith-0.8.0}/tests/data/manifest_spec_validator.py +400 -386
  45. {mapsmith-0.7.0 → mapsmith-0.8.0}/tests/test_catalog_applicability.py +11 -1
  46. mapsmith-0.8.0/tests/test_choice_at_scale.py +48 -0
  47. {mapsmith-0.7.0 → mapsmith-0.8.0}/tests/test_contours_and_cost.py +40 -1
  48. mapsmith-0.8.0/tests/test_dataset_digest.py +214 -0
  49. {mapsmith-0.7.0 → mapsmith-0.8.0}/tests/test_discovery_contract.py +7 -3
  50. {mapsmith-0.7.0 → mapsmith-0.8.0}/tests/test_discovery_report.py +7 -4
  51. mapsmith-0.8.0/tests/test_extract_from_gdb.py +89 -0
  52. mapsmith-0.8.0/tests/test_geometry_declarations.py +268 -0
  53. mapsmith-0.8.0/tests/test_limits.py +74 -0
  54. mapsmith-0.8.0/tests/test_lineage_digest_rules.py +269 -0
  55. {mapsmith-0.7.0 → mapsmith-0.8.0}/tests/test_linework.py +35 -1
  56. mapsmith-0.8.0/tests/test_output_guard.py +128 -0
  57. {mapsmith-0.7.0 → mapsmith-0.8.0}/tests/test_plans.py +21 -1
  58. {mapsmith-0.7.0 → mapsmith-0.8.0}/tests/test_published_figures.py +27 -11
  59. {mapsmith-0.7.0 → mapsmith-0.8.0}/tests/test_raster.py +32 -4
  60. {mapsmith-0.7.0 → mapsmith-0.8.0}/tests/test_redaction.py +38 -0
  61. {mapsmith-0.7.0 → mapsmith-0.8.0}/tests/test_showcase.py +87 -3
  62. {mapsmith-0.7.0 → mapsmith-0.8.0}/tests/test_spatial_stats.py +226 -1
  63. {mapsmith-0.7.0 → mapsmith-0.8.0}/tests/test_verify.py +2513 -2513
  64. {mapsmith-0.7.0 → mapsmith-0.8.0}/.dockerignore +0 -0
  65. {mapsmith-0.7.0 → mapsmith-0.8.0}/CONTRIBUTING.md +0 -0
  66. {mapsmith-0.7.0 → mapsmith-0.8.0}/Dockerfile +0 -0
  67. {mapsmith-0.7.0 → mapsmith-0.8.0}/LICENSE +0 -0
  68. {mapsmith-0.7.0 → mapsmith-0.8.0}/MANIFESTO.md +0 -0
  69. {mapsmith-0.7.0 → mapsmith-0.8.0}/TRADEMARKS.md +0 -0
  70. {mapsmith-0.7.0 → mapsmith-0.8.0}/docker-compose.yml +0 -0
  71. {mapsmith-0.7.0 → mapsmith-0.8.0}/examples/README.md +0 -0
  72. {mapsmith-0.7.0 → mapsmith-0.8.0}/examples/fixtures/COPERNICUS-NOTICE.md +0 -0
  73. {mapsmith-0.7.0 → mapsmith-0.8.0}/examples/fixtures/fetch_copernicus.py +0 -0
  74. {mapsmith-0.7.0 → mapsmith-0.8.0}/examples/fixtures/monte_baldo_dem.tif +0 -0
  75. {mapsmith-0.7.0 → mapsmith-0.8.0}/examples/fixtures/monte_baldo_s2.tif +0 -0
  76. {mapsmith-0.7.0 → mapsmith-0.8.0}/examples/fixtures/mount_st_helens_dem.tif +0 -0
  77. {mapsmith-0.7.0 → mapsmith-0.8.0}/glama.json +0 -0
  78. {mapsmith-0.7.0 → mapsmith-0.8.0}/src/mapsmith/antimeridian.py +0 -0
  79. {mapsmith-0.7.0 → mapsmith-0.8.0}/src/mapsmith/datum.py +0 -0
  80. {mapsmith-0.7.0 → mapsmith-0.8.0}/src/mapsmith/discovery_log.py +0 -0
  81. {mapsmith-0.7.0 → mapsmith-0.8.0}/src/mapsmith/engines/__init__.py +0 -0
  82. {mapsmith-0.7.0 → mapsmith-0.8.0}/src/mapsmith/engines/dispatch.py +0 -0
  83. {mapsmith-0.7.0 → mapsmith-0.8.0}/src/mapsmith/engines/esri.py +0 -0
  84. {mapsmith-0.7.0 → mapsmith-0.8.0}/src/mapsmith/engines/geodesy.py +0 -0
  85. {mapsmith-0.7.0 → mapsmith-0.8.0}/src/mapsmith/engines/summaries.py +0 -0
  86. {mapsmith-0.7.0 → mapsmith-0.8.0}/src/mapsmith/gdal_policy.py +0 -0
  87. {mapsmith-0.7.0 → mapsmith-0.8.0}/src/mapsmith/grid.py +0 -0
  88. {mapsmith-0.7.0 → mapsmith-0.8.0}/src/mapsmith/jobs.py +0 -0
  89. {mapsmith-0.7.0 → mapsmith-0.8.0}/src/mapsmith/plans/__init__.py +0 -0
  90. {mapsmith-0.7.0 → mapsmith-0.8.0}/src/mapsmith/plans/executor.py +0 -0
  91. {mapsmith-0.7.0 → mapsmith-0.8.0}/src/mapsmith/plans/models.py +0 -0
  92. {mapsmith-0.7.0 → mapsmith-0.8.0}/src/mapsmith/preview.py +0 -0
  93. {mapsmith-0.7.0 → mapsmith-0.8.0}/src/mapsmith/retrieval.py +0 -0
  94. {mapsmith-0.7.0 → mapsmith-0.8.0}/src/mapsmith/sql_policy.py +0 -0
  95. {mapsmith-0.7.0 → mapsmith-0.8.0}/src/mapsmith/stacks.py +0 -0
  96. {mapsmith-0.7.0 → mapsmith-0.8.0}/src/mapsmith/ui.py +0 -0
  97. {mapsmith-0.7.0 → mapsmith-0.8.0}/src/mapsmith/verify.py +0 -0
  98. {mapsmith-0.7.0 → mapsmith-0.8.0}/src/mapsmith/workspace.py +0 -0
  99. {mapsmith-0.7.0 → mapsmith-0.8.0}/tests/data/discovery_queries.json +0 -0
  100. {mapsmith-0.7.0 → mapsmith-0.8.0}/tests/test_ambiguous_layer.py +0 -0
  101. {mapsmith-0.7.0 → mapsmith-0.8.0}/tests/test_antimeridian.py +0 -0
  102. {mapsmith-0.7.0 → mapsmith-0.8.0}/tests/test_band_math.py +0 -0
  103. {mapsmith-0.7.0 → mapsmith-0.8.0}/tests/test_catalog.py +0 -0
  104. {mapsmith-0.7.0 → mapsmith-0.8.0}/tests/test_catalog_entry_spec.py +0 -0
  105. {mapsmith-0.7.0 → mapsmith-0.8.0}/tests/test_catalog_retrieval.py +0 -0
  106. {mapsmith-0.7.0 → mapsmith-0.8.0}/tests/test_centroid_convert.py +0 -0
  107. {mapsmith-0.7.0 → mapsmith-0.8.0}/tests/test_clip_reclassify_raster.py +0 -0
  108. {mapsmith-0.7.0 → mapsmith-0.8.0}/tests/test_dashboard.py +0 -0
  109. {mapsmith-0.7.0 → mapsmith-0.8.0}/tests/test_datum.py +0 -0
  110. {mapsmith-0.7.0 → mapsmith-0.8.0}/tests/test_describe_raster.py +0 -0
  111. {mapsmith-0.7.0 → mapsmith-0.8.0}/tests/test_discovery_log.py +0 -0
  112. {mapsmith-0.7.0 → mapsmith-0.8.0}/tests/test_duckdb_sandbox.py +0 -0
  113. {mapsmith-0.7.0 → mapsmith-0.8.0}/tests/test_engines.py +0 -0
  114. {mapsmith-0.7.0 → mapsmith-0.8.0}/tests/test_environment.py +0 -0
  115. {mapsmith-0.7.0 → mapsmith-0.8.0}/tests/test_failure_manifest.py +0 -0
  116. {mapsmith-0.7.0 → mapsmith-0.8.0}/tests/test_gdal_policy.py +0 -0
  117. {mapsmith-0.7.0 → mapsmith-0.8.0}/tests/test_geodesy.py +0 -0
  118. {mapsmith-0.7.0 → mapsmith-0.8.0}/tests/test_geoparquet2.py +0 -0
  119. {mapsmith-0.7.0 → mapsmith-0.8.0}/tests/test_grid_registration.py +0 -0
  120. {mapsmith-0.7.0 → mapsmith-0.8.0}/tests/test_lineage.py +0 -0
  121. {mapsmith-0.7.0 → mapsmith-0.8.0}/tests/test_measure_area.py +0 -0
  122. {mapsmith-0.7.0 → mapsmith-0.8.0}/tests/test_merge_simplify.py +0 -0
  123. {mapsmith-0.7.0 → mapsmith-0.8.0}/tests/test_nearest_explode.py +0 -0
  124. {mapsmith-0.7.0 → mapsmith-0.8.0}/tests/test_network.py +0 -0
  125. {mapsmith-0.7.0 → mapsmith-0.8.0}/tests/test_overlay_dissolve.py +0 -0
  126. {mapsmith-0.7.0 → mapsmith-0.8.0}/tests/test_path_containment.py +0 -0
  127. {mapsmith-0.7.0 → mapsmith-0.8.0}/tests/test_preview.py +0 -0
  128. {mapsmith-0.7.0 → mapsmith-0.8.0}/tests/test_raster_reproject_bands.py +0 -0
  129. {mapsmith-0.7.0 → mapsmith-0.8.0}/tests/test_resample_raster.py +0 -0
  130. {mapsmith-0.7.0 → mapsmith-0.8.0}/tests/test_retrieval.py +0 -0
  131. {mapsmith-0.7.0 → mapsmith-0.8.0}/tests/test_retrieval_at_scale.py +0 -0
  132. {mapsmith-0.7.0 → mapsmith-0.8.0}/tests/test_retrieval_degradation.py +0 -0
  133. {mapsmith-0.7.0 → mapsmith-0.8.0}/tests/test_sampling.py +0 -0
  134. {mapsmith-0.7.0 → mapsmith-0.8.0}/tests/test_select_and_extract.py +0 -0
  135. {mapsmith-0.7.0 → mapsmith-0.8.0}/tests/test_smoke.py +0 -0
  136. {mapsmith-0.7.0 → mapsmith-0.8.0}/tests/test_south_up_and_mixed.py +0 -0
  137. {mapsmith-0.7.0 → mapsmith-0.8.0}/tests/test_sql_axis_order.py +0 -0
  138. {mapsmith-0.7.0 → mapsmith-0.8.0}/tests/test_sql_credentials.py +0 -0
  139. {mapsmith-0.7.0 → mapsmith-0.8.0}/tests/test_stacks.py +0 -0
  140. {mapsmith-0.7.0 → mapsmith-0.8.0}/tests/test_summaries.py +0 -0
  141. {mapsmith-0.7.0 → mapsmith-0.8.0}/tests/test_summarize_points_in_polygons.py +0 -0
  142. {mapsmith-0.7.0 → mapsmith-0.8.0}/tests/test_terrain_derivatives.py +0 -0
  143. {mapsmith-0.7.0 → mapsmith-0.8.0}/tests/test_tier_a_operations.py +0 -0
  144. {mapsmith-0.7.0 → mapsmith-0.8.0}/tests/test_ui.py +0 -0
  145. {mapsmith-0.7.0 → mapsmith-0.8.0}/tests/test_verification_status.py +0 -0
  146. {mapsmith-0.7.0 → mapsmith-0.8.0}/tests/test_verify_repair.py +0 -0
  147. {mapsmith-0.7.0 → mapsmith-0.8.0}/tests/test_voronoi.py +0 -0
  148. {mapsmith-0.7.0 → mapsmith-0.8.0}/tests/test_whitebox.py +0 -0
  149. {mapsmith-0.7.0 → mapsmith-0.8.0}/tests/test_whitebox_derivatives.py +0 -0
  150. {mapsmith-0.7.0 → mapsmith-0.8.0}/tests/test_whitebox_encoding.py +0 -0
  151. {mapsmith-0.7.0 → mapsmith-0.8.0}/tests/test_whitebox_geographic_refusal.py +0 -0
  152. {mapsmith-0.7.0 → mapsmith-0.8.0}/tests/test_worked_example.py +0 -0
  153. {mapsmith-0.7.0 → mapsmith-0.8.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.0",
4
+ "version": "0.8.0",
5
5
  "creators": [
6
6
  {"name": "The MapSmith project"}
7
7
  ],
@@ -6,7 +6,198 @@ All notable changes to MapSmith are documented here, in the format of
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
- Nothing yet.
9
+ ## [0.8.0] - 2026-10-05
10
+
11
+ Records declare `spec_version` `1.0.0-draft.10`. `draft.9` defines the digest
12
+ of a dataset made of several files, which is what the first fix below
13
+ implements; `draft.10` gives each input a role and a configuration of its own.
14
+ **One key moves**: a second input's own georeferencing facts,
15
+ `inputs[].x-mapsmith:environment` on 0.7.x, are now `inputs[].environment`.
16
+
17
+ ### Added
18
+
19
+ - **`cluster_points_by_distance`: group points chained together within a
20
+ distance.** "Group everything within half a kilometre" is single linkage, not
21
+ thinning: two points share a cluster when a chain of points joins them with
22
+ every link at most the distance long, so chains merge, and every point is kept
23
+ with its `cluster_id` and `cluster_size`. `thin_points` keeps one point per
24
+ crowd, and its count is close to the number of groups but not equal to it. A
25
+ check re-reads the written file and re-derives the chains by a second
26
+ algorithm. A geographic CRS is refused, and so are lines, polygons and
27
+ multipart points, which a point cluster has no single position for.
28
+ - **`list_operations` takes a `geometry`: what the caller's layer holds**, as
29
+ `point`, `line` or `polygon`, or a list of them for a mixed layer. The
30
+ catalogue now has 31 operations taking one vector layer to another, one more
31
+ than the size it hands over whole, so a caller declaring everything it knew
32
+ got a shortlist back for the most common request there is; with the geometry
33
+ declared, the right operation is in the answer for 116 of the 118 benchmark
34
+ requests again. An entry declares the families it accepts only where its code
35
+ refuses the others (fifteen do), an entry without a declaration is kept for
36
+ every family, and a test calls each declared entry with every family it
37
+ leaves out and requires the refusal.
38
+ - **`benchmarks/choice_at_scale.py`**, with every choice it scores in
39
+ `benchmarks/choice_at_scale.json`: each of the 118 requests handed to Claude
40
+ Opus 5.5 in sets of 30, 40 and 50 candidates and compared with itself. From
41
+ 40 it chose right 6.9 points less often than from 30, from 50 5.1 points
42
+ less, so the size `list_operations` hands over whole stays 30, and a test
43
+ holds it at or below the largest size measured without loss.
44
+ - **Each input of an operation that reads more than one dataset says which
45
+ argument read it** (`inputs[].argument`, spec `draft.10`). The order of
46
+ `inputs` is not significant, so until now a weighted zonal statistic with
47
+ values and weights swapped produced a record that differed only in file
48
+ names. Eighteen operations name their inputs (twenty code paths:
49
+ `spatial_join` has three engines), with the names the input checks
50
+ already use in `verification[].argument`, so the two can be joined; a
51
+ single-input record is unchanged.
52
+
53
+ - **`get_lineage` takes a `layer`.** The history of one layer of a file
54
+ geodatabase now starts from the geodatabase itself: its record sits beside
55
+ the container (spec section 3.1), and the walk matches the layer at every hop
56
+ into a container. A layer given for a path that is not a container is
57
+ refused rather than ignored.
58
+
59
+ ### Changed
60
+
61
+ - **`get_lineage` steps carry `inputs[].argument`, `inputs[].environment` and
62
+ `output.layer`** when the records hold them.
63
+ - **A shapefile output whose member files are ambiguous** (`out.dbf` beside
64
+ `out.DBF`, possible only on a case-sensitive filesystem) is written with a
65
+ manifest that has no `output` digest and a note saying why, instead of a
66
+ digest that would choose one of the two.
67
+ - **`inputs[].x-mapsmith:environment` is `inputs[].environment`**, the field
68
+ spec `draft.10` defines for the configuration that changed how one input was
69
+ read. Same content; only the name moves, and a reader of 0.7.x records reads
70
+ the old one.
71
+ - **With more than one input, no file's facts sit on the record's
72
+ `environment`, the first input's included.** A sidecar beside the first
73
+ raster was recorded there, which the format reads as true of the whole run,
74
+ since the order of `inputs` means nothing: in a `clip_raster` it read as true
75
+ of the mask. It is now on that input's own `environment`, and only process
76
+ settings stay on the record. Seven operations can meet it (clip_raster,
77
+ sample_raster_at_points, elevation_profile, least_cost_path, viewshed,
78
+ watershed, zonal_statistics), and only when a sidecar is present. A
79
+ single-input record is unchanged.
80
+
81
+ ### Fixed
82
+
83
+ - **`cluster_points_by_distance` is bounded.** The pairs of points within the
84
+ distance grow with the square of the points when the distance is large, and
85
+ 6,000 points at a distance of 1e9 took two minutes and 2.3 GB. The pairs are
86
+ now found in blocks and the call is refused once they pass
87
+ `MAPSMITH_MAX_SAMPLES`, the operator's limit, before memory is spent.
88
+ `hot_spots` with `weights="distance_band"` uses the same query: its weights
89
+ were a double loop over every pair of features in Python, quadratic at any
90
+ band, and a band that is not a finite positive number is now refused.
91
+ - **`get_lineage` no longer hashes every shapefile in the workspace.** Since
92
+ the draft.9 digest rule it computed, while indexing, the other rule's digest
93
+ of every shapefile record's output, whatever the chain asked about. A
94
+ shapefile is now hashed only when a hop misses on it.
95
+ - **A Windows junction inside a `.gdb` is not followed.** The digest of a
96
+ directory container walked into junctions, which `is_symlink()` does not
97
+ report, so files outside the container could enter its digest. Section 3.3 of
98
+ the specification says links are neither members nor followed. The size used
99
+ for the re-hash cap now counts the same files as the digest.
100
+ - **A single-layer `.gdb` read by any operation records its layer.** Every
101
+ operation now reads a single-layer file geodatabase, not only
102
+ `extract_layer`, and the input records the layer it read: without it, a
103
+ lineage walk meeting the record of that layer stopped with a false "records
104
+ claim this container's digest, but for other layers".
105
+ - **`get_lineage` refuses a `layer` holding `:` or a control character.** On
106
+ Windows `:` in a file name opens an alternate data stream of another file.
107
+ - **`thin_points` refuses a distance that is not a finite positive number**,
108
+ like `cluster_points_by_distance`; and `least_cost_path` accepts a start or
109
+ end given as a single-part MultiPoint, which it used to count as no point.
110
+ - **`zonal_statistics` refuses zones that are not polygons.** Line zones were
111
+ accepted and returned the line's length in pixels as the `count`, with a
112
+ mean over the cells it crossed: a plausible table, and not a zonal
113
+ statistic. `count_in_polygons` and `summarize_points_in_polygons` refuse a
114
+ polygon layer holding anything else the same way. Found by the test that
115
+ holds the new geometry declarations to the code.
116
+ - **`least_cost_path` is found by a caller holding points.** It reads a cost
117
+ raster and two point layers but declared its input as raster only, so a
118
+ caller who said, correctly, that it held a vector layer never saw it. A start
119
+ or end layer holding a line or a polygon beside its point is now refused; it
120
+ used to pass with the extra feature ignored.
121
+ - **`snap_layer` snaps to a polygon or multipart reference.** Both crashed with
122
+ a raw `NotImplementedError` from shapely; every vertex of every part, holes
123
+ included, is now a target.
124
+ - **A feature with no geometry is refused, with a count, by `thin_points`,
125
+ `sample_raster_at_points`, `points_along_lines` and `line_intersections`.**
126
+ They crashed on it with an `AttributeError`, "cannot convert float NaN to
127
+ integer" and a failed sort.
128
+ - **A shapefile's digest covers its attributes and its CRS.** An input or
129
+ output shapefile was digested as its `.shp` alone, so a record kept matching
130
+ a dataset whose `.dbf` (attributes) or `.prj` (CRS) had been rewritten, while
131
+ it said `sha256` is of the bytes. The digest is now the listing of the
132
+ `.shp`, `.shx`, `.dbf`, `.prj` and `.cpg` files, as section 3.3 of the
133
+ specification defines it since draft.9; a file geodatabase input is digested
134
+ the same way, lock files excluded. `get_lineage` recomputes digests with the
135
+ same function, so a chain written by this release still resolves. A record
136
+ written by an earlier release for a shapefile carries the old digest:
137
+ `get_lineage` compares it under the rule it was written with, so an
138
+ unchanged shapefile is not reported as edited, and a chain that crosses
139
+ from a record written by 0.7.1 to one written now still resolves. A record
140
+ written before this release can only vouch for the `.shp`: an edit to the
141
+ attributes or the CRS made after it was written stays invisible to it, so
142
+ such a hop is reported as `reverified_shp_only`, not `reverified`, and a
143
+ history that contains one is not `verified`.
144
+ - **`extract_layer` works on a file geodatabase.** The catalog listed `.gdb`
145
+ among its containers, and the input digest opened the path as a file: a
146
+ geodatabase is a directory, so the operation died before writing anything.
147
+ - **`get_lineage` matches the layer when a digest belongs to a container.**
148
+ Two records for two layers of one geodatabase share the container's digest
149
+ (spec section 6, draft.9); a hop into a container now resolves to the record
150
+ for the layer it names, and stops with `other_layer` rather than attribute
151
+ one layer to the operation that wrote another. It re-checks a layer's record
152
+ against the container it sits beside (`<container>.<layer>.provenance.json`,
153
+ spec section 3.1), and two layers of one container reached on two branches
154
+ are two histories.
155
+ - **A URI in a manifest path keeps its `//`, so its credentials are masked.**
156
+ The path normaliser collapsed `https://user:pw@host/x` to `https:/user:pw@…`,
157
+ and the redaction of URI credentials looks for `://`: the password would have
158
+ stayed in the record. Not reachable while remote paths are refused, which is
159
+ the default; found in the 0.7.0 audit. The weights raster of
160
+ `zonal_statistics` is recorded through the same normaliser now.
161
+ - **A redaction in the environment of a secondary input is declared.** Masked
162
+ values there did not raise `parameters_redacted`, so the record changed a
163
+ value without saying so.
164
+ - **The package metadata names the Copernicus NOTICE** beside the AGPL licence,
165
+ since the source distribution ships the two Copernicus clips the notebook
166
+ reads.
167
+ - **The links on the PyPI page resolve.** PyPI renders the README with nothing
168
+ to resolve a relative link against, so every link to a document of the repo
169
+ and the map-panel screenshot answered 404 there. The build now rewrites them
170
+ to GitHub URLs pinned to the release tag; on GitHub the README is unchanged.
171
+
172
+ ## [0.7.1] - 2026-09-26
173
+
174
+ Two fixes from the pre-release audit of 0.7.0, both present in earlier releases,
175
+ and the Argleton run scored against trap 024's corrected truth, which arrived
176
+ after the 0.7.0 tag: that release's PyPI page still quotes the recount of the
177
+ old run. No answer 0.7.0 got right changes, and no manifest key moves: records
178
+ declare `spec_version` `1.0.0-draft.8`, as in 0.7.0.
179
+
180
+ ### Fixed
181
+
182
+ - **An output that is one of the inputs, or has an extension its writer cannot
183
+ honour, is refused before anything is written**
184
+ ([#31](https://github.com/mapsmith-ai/MapSmith/issues/31)). `zonal_statistics`
185
+ with `output_path` equal to its weights raster replaced the raster with a
186
+ directory of shapefiles — a vector writer given `.tif` falls back to the
187
+ Shapefile driver — and failed before writing a manifest. The dedicated tools
188
+ now refuse both at the boundary, as `run_operation` already refused the
189
+ collision; and the plan validator turns an extension no writer can honour from
190
+ a warning into an error (`OUTPUT_EXTENSION_REFUSED`), keeping the warning for
191
+ writable but non-canonical ones such as `.geojson`.
192
+
193
+ - **A spacing could ask for any number of points.** `elevation_profile` and
194
+ `points_along_lines` materialised every point before looking at how many
195
+ there were: a spacing of `1e-6` on a 90 m line is ninety million. Both now
196
+ count the points from the line lengths and refuse above
197
+ `MAPSMITH_MAX_SAMPLES` (default 1 000 000), an environment variable of the
198
+ server that no tool argument can move; `SECURITY.md` says what is and is not
199
+ limited. `points_along_lines` also refuses a NaN or infinite spacing, which
200
+ `spacing <= 0` let through.
10
201
 
11
202
  ## [0.7.0] - 2026-09-26
12
203
 
@@ -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.0"
16
- date-released: "2026-09-26"
15
+ version: "0.8.0"
16
+ date-released: "2026-10-05"
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.