openmapstack 0.2.0__tar.gz → 0.3.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 (57) hide show
  1. {openmapstack-0.2.0/openmapstack.egg-info → openmapstack-0.3.0}/PKG-INFO +34 -2
  2. openmapstack-0.2.0/PKG-INFO → openmapstack-0.3.0/README.md +30 -17
  3. {openmapstack-0.2.0 → openmapstack-0.3.0}/openmapstack/__init__.py +1 -1
  4. openmapstack-0.3.0/openmapstack/api.py +319 -0
  5. openmapstack-0.3.0/openmapstack/checks/metamorphic.py +74 -0
  6. {openmapstack-0.2.0 → openmapstack-0.3.0}/openmapstack/checks/project.py +26 -0
  7. {openmapstack-0.2.0 → openmapstack-0.3.0}/openmapstack/checks/provenance.py +53 -13
  8. {openmapstack-0.2.0 → openmapstack-0.3.0}/openmapstack/cli.py +261 -0
  9. openmapstack-0.3.0/openmapstack/connectors/__init__.py +356 -0
  10. openmapstack-0.3.0/openmapstack/connectors/duckdb_local.py +240 -0
  11. openmapstack-0.3.0/openmapstack/connectors/postgis.py +296 -0
  12. openmapstack-0.3.0/openmapstack/metamorphic.py +661 -0
  13. openmapstack-0.3.0/openmapstack/parameters.py +190 -0
  14. {openmapstack-0.2.0 → openmapstack-0.3.0}/openmapstack/rerun.py +66 -26
  15. openmapstack-0.3.0/openmapstack/schemas/check-result-v1.schema.json +30 -0
  16. {openmapstack-0.2.0 → openmapstack-0.3.0}/openmapstack/schemas/project-v1.schema.json +158 -1
  17. openmapstack-0.3.0/openmapstack/schemas/verify-result-v1.schema.json +50 -0
  18. openmapstack-0.3.0/openmapstack/snapshot.py +200 -0
  19. openmapstack-0.3.0/openmapstack/sources.py +257 -0
  20. {openmapstack-0.2.0 → openmapstack-0.3.0}/openmapstack/validation.py +19 -0
  21. {openmapstack-0.2.0 → openmapstack-0.3.0}/openmapstack/verify.py +33 -4
  22. openmapstack-0.2.0/README.md → openmapstack-0.3.0/openmapstack.egg-info/PKG-INFO +49 -0
  23. {openmapstack-0.2.0 → openmapstack-0.3.0}/openmapstack.egg-info/SOURCES.txt +18 -1
  24. {openmapstack-0.2.0 → openmapstack-0.3.0}/openmapstack.egg-info/requires.txt +4 -1
  25. {openmapstack-0.2.0 → openmapstack-0.3.0}/pyproject.toml +5 -2
  26. openmapstack-0.3.0/tests/test_check_api.py +194 -0
  27. openmapstack-0.3.0/tests/test_connectors.py +384 -0
  28. {openmapstack-0.2.0 → openmapstack-0.3.0}/tests/test_evals.py +114 -1
  29. openmapstack-0.3.0/tests/test_metamorphic.py +451 -0
  30. openmapstack-0.3.0/tests/test_snapshot.py +126 -0
  31. openmapstack-0.3.0/tests/test_sources.py +224 -0
  32. openmapstack-0.3.0/tests/test_verify_foreign.py +297 -0
  33. {openmapstack-0.2.0 → openmapstack-0.3.0}/LICENSE +0 -0
  34. {openmapstack-0.2.0 → openmapstack-0.3.0}/openmapstack/__main__.py +0 -0
  35. {openmapstack-0.2.0 → openmapstack-0.3.0}/openmapstack/checks/__init__.py +0 -0
  36. {openmapstack-0.2.0 → openmapstack-0.3.0}/openmapstack/checks/geodata.py +0 -0
  37. {openmapstack-0.2.0 → openmapstack-0.3.0}/openmapstack/checks/overrides.py +0 -0
  38. {openmapstack-0.2.0 → openmapstack-0.3.0}/openmapstack/checks/presentation.py +0 -0
  39. {openmapstack-0.2.0 → openmapstack-0.3.0}/openmapstack/checks/qgis.py +0 -0
  40. {openmapstack-0.2.0 → openmapstack-0.3.0}/openmapstack/checks/rerun.py +0 -0
  41. {openmapstack-0.2.0 → openmapstack-0.3.0}/openmapstack/checks/spatial.py +0 -0
  42. {openmapstack-0.2.0 → openmapstack-0.3.0}/openmapstack/checks/validation.py +0 -0
  43. {openmapstack-0.2.0 → openmapstack-0.3.0}/openmapstack/checks/visual.py +0 -0
  44. {openmapstack-0.2.0 → openmapstack-0.3.0}/openmapstack/expectations.py +0 -0
  45. {openmapstack-0.2.0 → openmapstack-0.3.0}/openmapstack/integrity.py +0 -0
  46. {openmapstack-0.2.0 → openmapstack-0.3.0}/openmapstack/project.py +0 -0
  47. {openmapstack-0.2.0 → openmapstack-0.3.0}/openmapstack/schema.py +0 -0
  48. {openmapstack-0.2.0 → openmapstack-0.3.0}/openmapstack/schemas/__init__.py +0 -0
  49. {openmapstack-0.2.0 → openmapstack-0.3.0}/openmapstack.egg-info/dependency_links.txt +0 -0
  50. {openmapstack-0.2.0 → openmapstack-0.3.0}/openmapstack.egg-info/entry_points.txt +0 -0
  51. {openmapstack-0.2.0 → openmapstack-0.3.0}/openmapstack.egg-info/top_level.txt +0 -0
  52. {openmapstack-0.2.0 → openmapstack-0.3.0}/setup.cfg +0 -0
  53. {openmapstack-0.2.0 → openmapstack-0.3.0}/tests/test_cli.py +0 -0
  54. {openmapstack-0.2.0 → openmapstack-0.3.0}/tests/test_expectations.py +0 -0
  55. {openmapstack-0.2.0 → openmapstack-0.3.0}/tests/test_release.py +0 -0
  56. {openmapstack-0.2.0 → openmapstack-0.3.0}/tests/test_rerun.py +0 -0
  57. {openmapstack-0.2.0 → openmapstack-0.3.0}/tests/test_verify.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: openmapstack
3
- Version: 0.2.0
3
+ Version: 0.3.0
4
4
  Summary: Validate, run, and inspect reproducible OpenMapStack projects
5
5
  Requires-Python: >=3.10
6
6
  Description-Content-Type: text/markdown
@@ -11,8 +11,10 @@ Provides-Extra: geo
11
11
  Requires-Dist: duckdb>=1.2; extra == "geo"
12
12
  Provides-Extra: visual
13
13
  Requires-Dist: playwright>=1.40; extra == "visual"
14
+ Provides-Extra: postgis
15
+ Requires-Dist: psycopg[binary]>=3.1; extra == "postgis"
14
16
  Provides-Extra: all
15
- Requires-Dist: openmapstack[geo,visual]; extra == "all"
17
+ Requires-Dist: openmapstack[geo,postgis,visual]; extra == "all"
16
18
  Dynamic: license-file
17
19
 
18
20
  # openmapstack
@@ -33,6 +35,7 @@ It is open-first and cloud-native by default, built on shoulders of the awesome
33
35
  - [SKILL.md](SKILL.md) — the skill entry point: triggers, global defaults, format and compute decision matrices, anti-patterns, and a quick triage guide.
34
36
  - [references/data-sources.md](references/data-sources.md) - lists OSM, Overture, Sentinel/Landsat, regional portals, STAC catalogs and others.
35
37
  - [references/services-and-scale.md](references/services-and-scale.md) - depending on case use local installs or hosted/SaaS services for global-scale basemaps, elevation, routing, geocoding, place search, and postcodes.
38
+ - [references/user-data-sources.md](references/user-data-sources.md) - the user's own warehouse data: credentials by reference, read-only discovery, approval-gated snapshots, and the pin classes that make a warehouse table reproducible.
36
39
  - [references/formats-and-crs.md](references/formats-and-crs.md) - how to choose formats, conversions, projections, EPSG codes.
37
40
  - [references/processing.md](references/processing.md) - when and how to use GDAL/OGR, GeoPandas, xarray, DuckDB, PostGIS, PDAL and other open geo processing tools.
38
41
  - [references/analytics.md](references/analytics.md) — do vector/raster analytics, terrain, hydrology, network, point clouds, geocoding etc.
@@ -44,6 +47,7 @@ It is open-first and cloud-native by default, built on shoulders of the awesome
44
47
  - [examples/tartu-development/](examples/tartu-development/) — a fully-worked reproducible project matching the acceptance scenario: source provenance + timestamps, explicit assumptions, two verified project overrides (a scenario attribute change with prior-value verification, and hypothetical scenario geometry), deterministic pipeline, machine-readable validation, and semantic presentation.
45
48
  - [evals/](evals/) — the eval suite grading whether an agent reaches the right analytical answer, respects the GIS-method guardrails, and reruns reproducibly, with the `openmapstack-project/v1` contract as the substrate that makes those independently checkable: `python evals/run.py --mode fixture` runs deterministic, no-LLM checks against real generated artifacts (analytical correctness against known geospatial truth, metric CRS, source immutability, schema, overrides, validation integrity, presentation contract, and clean reruns), plus adversarial cases and a pluggable live-agent benchmark (Claude Code, Codex, and any OpenAI-compatible API such as OpenRouter — URL and model via `OPENAI_COMPATIBLE_*` env, API key as a secret).
46
49
  - [`openmapstack/`](openmapstack/) — the installable `openmapstack validate/run/inspect` CLI for auditing and executing `openmapstack-project/v1` projects, plus [`openmapstack/checks/`](openmapstack/checks/): the reusable, semantic check library. All but five of its checks are oracle-free, so the same functions that grade the eval suite also grade a user's own project on data this repository has never seen.
50
+ - [docs/openmapbench-interop.md](docs/openmapbench-interop.md) — the narrow, versioned contract a benchmark harness such as OpenMapBench consumes: `openmapstack checks` / `check` / `api-info` (`openmapstack-check-api/v1`), the packaged result schemas, skill snapshots, arm provenance, and exported task bundles.
47
51
  - [`.claude-plugin/`](.claude-plugin/) — Claude Code plugin and marketplace manifests, so the repository can also be installed with `/plugin install`. Validated in CI by [`.github/workflows/plugin.yml`](.github/workflows/plugin.yml).
48
52
 
49
53
  My local Estonia-specific guidance (Maa- ja Ruumiamet, ETAK, EPSG:3301 / L-EST97) is included for convenience. But all the global sources are incuded for world-wide coverage.
@@ -168,6 +172,15 @@ openmapstack run path/to/project.yaml
168
172
 
169
173
  # Review sources, versions, overrides, ordered steps, outputs, and latest run.
170
174
  openmapstack inspect path/to/project.yaml
175
+
176
+ # Copy SKILL.md, references/, and templates/ into a hashed, inspectable snapshot.
177
+ openmapstack skill-snapshot --out /tmp/oms-skill --json
178
+ openmapstack skill-snapshot --inspect /tmp/oms-skill
179
+
180
+ # Read-only discovery of a warehouse source, then an approval-gated snapshot.
181
+ openmapstack source discover path/to/project.yaml --source parcels
182
+ openmapstack source snapshot path/to/project.yaml --source parcels \
183
+ --query "SELECT id, geom FROM cadastre.parcels" --destination data/source/parcels.parquet --approve
171
184
  ```
172
185
 
173
186
  Useful automation options:
@@ -193,6 +206,7 @@ PyQGIS is available.
193
206
  ```bash
194
207
  openmapstack verify path/to/project.yaml
195
208
  openmapstack verify path/to/project.yaml --rerun # + rebuild from source and compare
209
+ openmapstack verify path/to/project.yaml --metamorphic # + run declared no-oracle relations
196
210
  openmapstack verify path/to/project.yaml --json --output validation/verify-report.json
197
211
  openmapstack verify path/to/project.yaml --strict # warnings and not-testable also return 1
198
212
  ```
@@ -234,6 +248,24 @@ inputs, or a retained local evidence file invalidates the attestation and
234
248
  returns it to warning status. See
235
249
  [the project contract](references/project-spec.md#26-validation).
236
250
 
251
+ Where no golden answer exists at all, `validation.metamorphic[]` declares
252
+ relations that must hold under a controlled perturbation: shuffle a source and
253
+ the result must not change, duplicate every feature and a keyed set must not
254
+ change, widen an inclusion buffer and no candidate may disappear. Each relation
255
+ states the precondition that makes it valid, is executed by
256
+ `verify --metamorphic` in an isolated copy against the project's own pipeline,
257
+ and reports `not_testable` with the reason when the precondition does not hold
258
+ on the actual data. See [the project contract](references/project-spec.md#26-validation).
259
+
260
+ `openmapstack source` is the connector pilot for the user's own data
261
+ (DuckDB local files and PostGIS). Credentials are referenced, never stored;
262
+ discovery is read-only with a statement timeout; a snapshot is a dry run
263
+ until `--approve`, is limited by rows and bytes, lands only under
264
+ `data/source/`, and hands back the `pin` block that makes the source
265
+ reproducible. A warehouse table with only a timestamp is not pinned; an
266
+ expired backend snapshot is reported as `not_reproducible`. See
267
+ [user data sources](references/user-data-sources.md).
268
+
237
269
  `validate` checks manifest structure, source retrieval/version/licensing data,
238
270
  CRS declarations, processing graph resolution, override provenance and files,
239
271
  output existence, validation-report parity/status propagation, override
@@ -1,20 +1,3 @@
1
- Metadata-Version: 2.4
2
- Name: openmapstack
3
- Version: 0.2.0
4
- Summary: Validate, run, and inspect reproducible OpenMapStack projects
5
- Requires-Python: >=3.10
6
- Description-Content-Type: text/markdown
7
- License-File: LICENSE
8
- Requires-Dist: jsonschema<5,>=4
9
- Requires-Dist: PyYAML<7,>=6
10
- Provides-Extra: geo
11
- Requires-Dist: duckdb>=1.2; extra == "geo"
12
- Provides-Extra: visual
13
- Requires-Dist: playwright>=1.40; extra == "visual"
14
- Provides-Extra: all
15
- Requires-Dist: openmapstack[geo,visual]; extra == "all"
16
- Dynamic: license-file
17
-
18
1
  # openmapstack
19
2
 
20
3
  **Geospatial questions → reproducible, validated GIS analysis project (with very nice interactive map).**
@@ -33,6 +16,7 @@ It is open-first and cloud-native by default, built on shoulders of the awesome
33
16
  - [SKILL.md](SKILL.md) — the skill entry point: triggers, global defaults, format and compute decision matrices, anti-patterns, and a quick triage guide.
34
17
  - [references/data-sources.md](references/data-sources.md) - lists OSM, Overture, Sentinel/Landsat, regional portals, STAC catalogs and others.
35
18
  - [references/services-and-scale.md](references/services-and-scale.md) - depending on case use local installs or hosted/SaaS services for global-scale basemaps, elevation, routing, geocoding, place search, and postcodes.
19
+ - [references/user-data-sources.md](references/user-data-sources.md) - the user's own warehouse data: credentials by reference, read-only discovery, approval-gated snapshots, and the pin classes that make a warehouse table reproducible.
36
20
  - [references/formats-and-crs.md](references/formats-and-crs.md) - how to choose formats, conversions, projections, EPSG codes.
37
21
  - [references/processing.md](references/processing.md) - when and how to use GDAL/OGR, GeoPandas, xarray, DuckDB, PostGIS, PDAL and other open geo processing tools.
38
22
  - [references/analytics.md](references/analytics.md) — do vector/raster analytics, terrain, hydrology, network, point clouds, geocoding etc.
@@ -44,6 +28,7 @@ It is open-first and cloud-native by default, built on shoulders of the awesome
44
28
  - [examples/tartu-development/](examples/tartu-development/) — a fully-worked reproducible project matching the acceptance scenario: source provenance + timestamps, explicit assumptions, two verified project overrides (a scenario attribute change with prior-value verification, and hypothetical scenario geometry), deterministic pipeline, machine-readable validation, and semantic presentation.
45
29
  - [evals/](evals/) — the eval suite grading whether an agent reaches the right analytical answer, respects the GIS-method guardrails, and reruns reproducibly, with the `openmapstack-project/v1` contract as the substrate that makes those independently checkable: `python evals/run.py --mode fixture` runs deterministic, no-LLM checks against real generated artifacts (analytical correctness against known geospatial truth, metric CRS, source immutability, schema, overrides, validation integrity, presentation contract, and clean reruns), plus adversarial cases and a pluggable live-agent benchmark (Claude Code, Codex, and any OpenAI-compatible API such as OpenRouter — URL and model via `OPENAI_COMPATIBLE_*` env, API key as a secret).
46
30
  - [`openmapstack/`](openmapstack/) — the installable `openmapstack validate/run/inspect` CLI for auditing and executing `openmapstack-project/v1` projects, plus [`openmapstack/checks/`](openmapstack/checks/): the reusable, semantic check library. All but five of its checks are oracle-free, so the same functions that grade the eval suite also grade a user's own project on data this repository has never seen.
31
+ - [docs/openmapbench-interop.md](docs/openmapbench-interop.md) — the narrow, versioned contract a benchmark harness such as OpenMapBench consumes: `openmapstack checks` / `check` / `api-info` (`openmapstack-check-api/v1`), the packaged result schemas, skill snapshots, arm provenance, and exported task bundles.
47
32
  - [`.claude-plugin/`](.claude-plugin/) — Claude Code plugin and marketplace manifests, so the repository can also be installed with `/plugin install`. Validated in CI by [`.github/workflows/plugin.yml`](.github/workflows/plugin.yml).
48
33
 
49
34
  My local Estonia-specific guidance (Maa- ja Ruumiamet, ETAK, EPSG:3301 / L-EST97) is included for convenience. But all the global sources are incuded for world-wide coverage.
@@ -168,6 +153,15 @@ openmapstack run path/to/project.yaml
168
153
 
169
154
  # Review sources, versions, overrides, ordered steps, outputs, and latest run.
170
155
  openmapstack inspect path/to/project.yaml
156
+
157
+ # Copy SKILL.md, references/, and templates/ into a hashed, inspectable snapshot.
158
+ openmapstack skill-snapshot --out /tmp/oms-skill --json
159
+ openmapstack skill-snapshot --inspect /tmp/oms-skill
160
+
161
+ # Read-only discovery of a warehouse source, then an approval-gated snapshot.
162
+ openmapstack source discover path/to/project.yaml --source parcels
163
+ openmapstack source snapshot path/to/project.yaml --source parcels \
164
+ --query "SELECT id, geom FROM cadastre.parcels" --destination data/source/parcels.parquet --approve
171
165
  ```
172
166
 
173
167
  Useful automation options:
@@ -193,6 +187,7 @@ PyQGIS is available.
193
187
  ```bash
194
188
  openmapstack verify path/to/project.yaml
195
189
  openmapstack verify path/to/project.yaml --rerun # + rebuild from source and compare
190
+ openmapstack verify path/to/project.yaml --metamorphic # + run declared no-oracle relations
196
191
  openmapstack verify path/to/project.yaml --json --output validation/verify-report.json
197
192
  openmapstack verify path/to/project.yaml --strict # warnings and not-testable also return 1
198
193
  ```
@@ -234,6 +229,24 @@ inputs, or a retained local evidence file invalidates the attestation and
234
229
  returns it to warning status. See
235
230
  [the project contract](references/project-spec.md#26-validation).
236
231
 
232
+ Where no golden answer exists at all, `validation.metamorphic[]` declares
233
+ relations that must hold under a controlled perturbation: shuffle a source and
234
+ the result must not change, duplicate every feature and a keyed set must not
235
+ change, widen an inclusion buffer and no candidate may disappear. Each relation
236
+ states the precondition that makes it valid, is executed by
237
+ `verify --metamorphic` in an isolated copy against the project's own pipeline,
238
+ and reports `not_testable` with the reason when the precondition does not hold
239
+ on the actual data. See [the project contract](references/project-spec.md#26-validation).
240
+
241
+ `openmapstack source` is the connector pilot for the user's own data
242
+ (DuckDB local files and PostGIS). Credentials are referenced, never stored;
243
+ discovery is read-only with a statement timeout; a snapshot is a dry run
244
+ until `--approve`, is limited by rows and bytes, lands only under
245
+ `data/source/`, and hands back the `pin` block that makes the source
246
+ reproducible. A warehouse table with only a timestamp is not pinned; an
247
+ expired backend snapshot is reported as `not_reproducible`. See
248
+ [user data sources](references/user-data-sources.md).
249
+
237
250
  `validate` checks manifest structure, source retrieval/version/licensing data,
238
251
  CRS declarations, processing graph resolution, override provenance and files,
239
252
  output existence, validation-report parity/status propagation, override
@@ -3,4 +3,4 @@
3
3
  from .validation import Check, ValidationResult, validate_project
4
4
 
5
5
  __all__ = ["Check", "ValidationResult", "validate_project"]
6
- __version__ = "0.2.0"
6
+ __version__ = "0.3.0"
@@ -0,0 +1,319 @@
1
+ """The versioned check API that external harnesses consume.
2
+
3
+ OpenMapBench (and any other benchmark or CI system) grades produced projects
4
+ with this package's checks. It must be able to do so without vendoring the
5
+ check implementations and without depending on module layout: the surface
6
+ it may rely on is exactly
7
+
8
+ - ``CHECK_API_VERSION`` and ``api_info()`` for negotiation;
9
+ - ``list_checks()`` for the catalogue of check names and their parameters;
10
+ - ``run_check()`` for one check, returning a record that validates against
11
+ ``openmapstack-check-result/v1``;
12
+ - ``openmapstack verify --json``, returning ``openmapstack-verify-result/v1``;
13
+ - the JSON schemas packaged under ``openmapstack/schemas/``.
14
+
15
+ Everything else in ``openmapstack.checks`` is implementation.
16
+
17
+ Versioning: ``CHECK_API_VERSION`` follows ``<name>/v<major>``. A new check,
18
+ a new optional parameter, or a new result field is additive and does not
19
+ change the major. Renaming or removing a check, changing a parameter's
20
+ meaning, or changing the four-state status vocabulary does. A consumer
21
+ pins the major and the minimum package version it was tested against, and
22
+ ``negotiate()`` answers whether the installed package satisfies both.
23
+
24
+ Result semantics that a consumer may rely on:
25
+
26
+ - ``status`` is one of ``passed | failed | warning | not_testable`` and a
27
+ check that could not establish its predicate is never ``passed``;
28
+ - ``code`` is a stable machine-readable identifier when the status is not
29
+ ``passed``; consumers grade on ``status`` and, for mutation-style
30
+ expectations, ``code`` -- never on ``detail`` text;
31
+ - ``dimension`` is the reporting bucket the check belongs to; buckets are
32
+ reported separately and must not be collapsed into one score;
33
+ - ``oracle_free`` is ``false`` for the checks that need a known answer.
34
+ Those transfer to arbitrary data only through attested expectations.
35
+
36
+ Setup failures (a check that raises) are reported as ``not_testable`` with
37
+ ``code: check_error`` by ``run_check`` so a broken environment cannot
38
+ produce either a pass or a graded failure; benchmark harnesses keep them
39
+ out of scored denominators.
40
+ """
41
+
42
+ from __future__ import annotations
43
+
44
+ import importlib
45
+ import inspect
46
+ import json
47
+ import re
48
+ from dataclasses import dataclass, field
49
+ from pathlib import Path
50
+ from typing import Any
51
+
52
+ from . import __version__
53
+ from .checks import STATUSES, AssertionResult, not_testable
54
+ from .schema import validation_errors
55
+
56
+ CHECK_API_VERSION = "openmapstack-check-api/v1"
57
+ CHECK_RESULT_SCHEMA = "openmapstack-check-result/v1"
58
+ API_INFO_SCHEMA = "openmapstack-api-info/v1"
59
+ VERIFY_RESULT_SCHEMA = "openmapstack-verify-result/v1"
60
+ PROJECT_SCHEMA = "openmapstack-project/v1"
61
+
62
+ CHECK_MODULES = (
63
+ "project",
64
+ "provenance",
65
+ "overrides",
66
+ "validation",
67
+ "geodata",
68
+ "presentation",
69
+ "qgis",
70
+ "visual",
71
+ "rerun",
72
+ "metamorphic",
73
+ )
74
+
75
+ # Reporting buckets. Shared with evals/run.py, which asserts equality in
76
+ # tests so the two cannot drift apart.
77
+ DIMENSIONS = {
78
+ "project": "reproducibility_compliance",
79
+ "overrides": "override_handling",
80
+ "provenance": "provenance",
81
+ "geodata": "gis_correctness",
82
+ "validation": "validation_integrity",
83
+ "qgis": "presentation_contract",
84
+ "presentation": "presentation_contract",
85
+ "visual": "visual_judgement",
86
+ "rerun": "rerun_success",
87
+ "metamorphic": "metamorphic_evidence",
88
+ }
89
+
90
+ # The checks that need a known answer. They are reachable on user data
91
+ # only through validation.expectations[] attestations.
92
+ KNOWN_ANSWER_CHECKS = frozenset(
93
+ {
94
+ "geodata.row_count",
95
+ "geodata.feature_present",
96
+ "geodata.feature_absent",
97
+ "geodata.feature_field_equals",
98
+ "geodata.field_range",
99
+ }
100
+ )
101
+
102
+ _SCHEMA_DIR = Path(__file__).resolve().parent / "schemas"
103
+ _VERSION = re.compile(r"^(\d+)\.(\d+)\.(\d+)")
104
+
105
+
106
+ class CheckAPIError(ValueError):
107
+ """The consumer asked for something the API does not provide."""
108
+
109
+
110
+ @dataclass(frozen=True)
111
+ class CheckParameter:
112
+ name: str
113
+ required: bool
114
+ default: Any = None
115
+
116
+ def to_dict(self) -> dict[str, Any]:
117
+ payload: dict[str, Any] = {"name": self.name, "required": self.required}
118
+ if not self.required:
119
+ payload["default"] = self.default
120
+ return payload
121
+
122
+
123
+ @dataclass(frozen=True)
124
+ class CheckDescriptor:
125
+ name: str
126
+ module: str
127
+ dimension: str
128
+ oracle_free: bool
129
+ summary: str
130
+ parameters: tuple[CheckParameter, ...] = field(default_factory=tuple)
131
+
132
+ def to_dict(self) -> dict[str, Any]:
133
+ return {
134
+ "name": self.name,
135
+ "module": self.module,
136
+ "dimension": self.dimension,
137
+ "oracle_free": self.oracle_free,
138
+ "summary": self.summary,
139
+ "parameters": [parameter.to_dict() for parameter in self.parameters],
140
+ }
141
+
142
+
143
+ def _load_schema(name: str) -> dict[str, Any]:
144
+ return json.loads((_SCHEMA_DIR / name).read_text(encoding="utf-8"))
145
+
146
+
147
+ def _describe(module_name: str, function_name: str, function: Any) -> CheckDescriptor:
148
+ signature = inspect.signature(function)
149
+ parameters: list[CheckParameter] = []
150
+ for index, parameter in enumerate(signature.parameters.values()):
151
+ if index == 0: # workspace
152
+ continue
153
+ if parameter.kind in (inspect.Parameter.VAR_POSITIONAL, inspect.Parameter.VAR_KEYWORD):
154
+ continue
155
+ required = parameter.default is inspect.Parameter.empty
156
+ default = None if required else parameter.default
157
+ if isinstance(default, tuple):
158
+ default = list(default)
159
+ parameters.append(CheckParameter(parameter.name, required, default))
160
+ doc = inspect.getdoc(function) or ""
161
+ summary = doc.strip().splitlines()[0].strip() if doc.strip() else ""
162
+ name = f"{module_name}.{function_name}"
163
+ return CheckDescriptor(
164
+ name=name,
165
+ module=module_name,
166
+ dimension=DIMENSIONS.get(module_name, "other"),
167
+ oracle_free=name not in KNOWN_ANSWER_CHECKS,
168
+ summary=summary,
169
+ parameters=tuple(parameters),
170
+ )
171
+
172
+
173
+ def _is_check(function: Any) -> bool:
174
+ if not inspect.isfunction(function) or function.__name__.startswith("_"):
175
+ return False
176
+ try:
177
+ parameters = list(inspect.signature(function).parameters.values())
178
+ except (TypeError, ValueError):
179
+ return False
180
+ return bool(parameters) and parameters[0].name == "workspace"
181
+
182
+
183
+ def list_checks() -> list[CheckDescriptor]:
184
+ """Every public check, discovered from the shipped modules."""
185
+ descriptors: list[CheckDescriptor] = []
186
+ for module_name in CHECK_MODULES:
187
+ module = importlib.import_module(f"openmapstack.checks.{module_name}")
188
+ for function_name, function in sorted(vars(module).items()):
189
+ if getattr(function, "__module__", None) != module.__name__:
190
+ continue
191
+ if _is_check(function):
192
+ descriptors.append(_describe(module_name, function_name, function))
193
+ return descriptors
194
+
195
+
196
+ def describe_check(name: str) -> CheckDescriptor:
197
+ module_name, _, function_name = name.partition(".")
198
+ function = _resolve(name)
199
+ if getattr(function, "__module__", None) != f"openmapstack.checks.{module_name}":
200
+ raise CheckAPIError(f"unknown check {name!r}; see list_checks()")
201
+ return _describe(module_name, function_name, function)
202
+
203
+
204
+ def _resolve(name: str) -> Any:
205
+ module_name, _, function_name = name.partition(".")
206
+ if module_name not in CHECK_MODULES or not function_name:
207
+ raise CheckAPIError(f"unknown check {name!r}; see list_checks()")
208
+ module = importlib.import_module(f"openmapstack.checks.{module_name}")
209
+ function = getattr(module, function_name, None)
210
+ if function is None or not _is_check(function):
211
+ raise CheckAPIError(f"unknown check {name!r}; see list_checks()")
212
+ return function
213
+
214
+
215
+ def run_check(name: str, workspace: str | Path, args: dict[str, Any] | None = None) -> dict[str, Any]:
216
+ """Execute one check and return an ``openmapstack-check-result/v1`` record.
217
+
218
+ Unknown check names and malformed arguments raise ``CheckAPIError``
219
+ (a consumer configuration error). A check that raises while running is
220
+ reported as ``not_testable`` with ``code: check_error`` -- never as a
221
+ pass, and never as a graded failure.
222
+ """
223
+ descriptor = describe_check(name)
224
+ function = _resolve(name)
225
+ args = dict(args or {})
226
+ declared = {parameter.name for parameter in descriptor.parameters}
227
+ unknown = sorted(set(args) - declared)
228
+ missing = sorted(parameter.name for parameter in descriptor.parameters if parameter.required and parameter.name not in args)
229
+ if unknown or missing:
230
+ raise CheckAPIError(f"{name}: unknown args {unknown}, missing required args {missing}")
231
+ try:
232
+ result = function(Path(workspace), **args)
233
+ except Exception as exc: # noqa: BLE001 - a check must never take a harness down
234
+ result = not_testable(f"{type(exc).__name__}: {exc}", code="check_error")
235
+ if not isinstance(result, AssertionResult) or result.status not in STATUSES:
236
+ result = not_testable("check returned a malformed result", code="check_error")
237
+ data = {key: value for key, value in result.data.items() if key != "code"}
238
+ record: dict[str, Any] = {
239
+ "schema": CHECK_RESULT_SCHEMA,
240
+ "api_version": CHECK_API_VERSION,
241
+ "package_version": __version__,
242
+ "check": name,
243
+ "dimension": descriptor.dimension,
244
+ "oracle_free": descriptor.oracle_free,
245
+ "args": args,
246
+ "status": result.status,
247
+ "code": result.data.get("code"),
248
+ "detail": result.detail,
249
+ "data": json.loads(json.dumps(data, default=str)),
250
+ }
251
+ errors = validation_errors(record, _load_schema("check-result-v1.schema.json"))
252
+ if errors: # pragma: no cover - the record is built here; a failure is a bug
253
+ raise CheckAPIError(f"internal: check result does not validate: {errors}")
254
+ return record
255
+
256
+
257
+ def api_info() -> dict[str, Any]:
258
+ """What this installation offers, for a consumer to negotiate against."""
259
+ checks = list_checks()
260
+ return {
261
+ "schema": API_INFO_SCHEMA,
262
+ "package": "openmapstack",
263
+ "package_version": __version__,
264
+ "check_api_version": CHECK_API_VERSION,
265
+ "project_schema": PROJECT_SCHEMA,
266
+ "result_schemas": {
267
+ "check": CHECK_RESULT_SCHEMA,
268
+ "verify": VERIFY_RESULT_SCHEMA,
269
+ },
270
+ "statuses": list(STATUSES),
271
+ "dimensions": sorted(set(DIMENSIONS.values())),
272
+ "checks": len(checks),
273
+ "oracle_free_checks": sum(descriptor.oracle_free for descriptor in checks),
274
+ "known_answer_checks": sorted(KNOWN_ANSWER_CHECKS),
275
+ }
276
+
277
+
278
+ def _parse_version(text: str) -> tuple[int, int, int]:
279
+ match = _VERSION.match(text or "")
280
+ if match is None:
281
+ raise CheckAPIError(f"not a semantic version: {text!r}")
282
+ return tuple(int(part) for part in match.groups()) # type: ignore[return-value]
283
+
284
+
285
+ def negotiate(
286
+ *,
287
+ required_api: str = CHECK_API_VERSION,
288
+ min_package_version: str | None = None,
289
+ required_checks: list[str] | None = None,
290
+ ) -> dict[str, Any]:
291
+ """Answer whether this installation satisfies a consumer's requirements.
292
+
293
+ A consumer states the API major it was built for, the oldest package
294
+ version it was tested against, and the checks it needs. The answer
295
+ lists every unmet requirement so a harness can report *why* it is
296
+ refusing to grade rather than grading with a checker it does not
297
+ understand.
298
+ """
299
+ problems: list[str] = []
300
+ if required_api != CHECK_API_VERSION:
301
+ problems.append(f"check API {required_api!r} is not provided; this package offers {CHECK_API_VERSION!r}")
302
+ if min_package_version is not None and _parse_version(__version__) < _parse_version(min_package_version):
303
+ problems.append(f"package version {__version__} is older than the required {min_package_version}")
304
+ available = {descriptor.name for descriptor in list_checks()}
305
+ missing = sorted(name for name in (required_checks or []) if name not in available)
306
+ if missing:
307
+ problems.append(f"checks not provided: {missing}")
308
+ return {
309
+ "schema": "openmapstack-api-negotiation/v1",
310
+ "compatible": not problems,
311
+ "package_version": __version__,
312
+ "check_api_version": CHECK_API_VERSION,
313
+ "problems": problems,
314
+ }
315
+
316
+
317
+ def validate_verify_result(payload: dict[str, Any]) -> list[str]:
318
+ """Schema-validate an ``openmapstack verify --json`` document."""
319
+ return validation_errors(payload, _load_schema("verify-result-v1.schema.json"))
@@ -0,0 +1,74 @@
1
+ """Metamorphic-relation assertions.
2
+
3
+ Thin check-library entry points over ``openmapstack.metamorphic`` so an eval
4
+ case (``assert: metamorphic.relation_holds``) and ``openmapstack verify
5
+ --metamorphic`` grade the same thing. See that module for the relations,
6
+ their preconditions, and the result vocabulary.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from pathlib import Path
12
+ from typing import Any
13
+
14
+ from . import AssertionResult, failed, load_project_yaml, not_testable, passed, project_root
15
+
16
+
17
+ def declarations_valid(workspace: Path, project_dir: str = ".") -> AssertionResult:
18
+ """Every ``validation.metamorphic[]`` entry parses, names a known relation,
19
+ and addresses declared outputs. Structural only: nothing is executed."""
20
+ from ..metamorphic import DeclarationError, declared_relations, parse_declaration
21
+
22
+ proj = load_project_yaml(workspace, project_dir)
23
+ if proj is None:
24
+ return failed("project.yaml missing", code="manifest_missing")
25
+ try:
26
+ raw_declarations = declared_relations(proj)
27
+ except DeclarationError as exc:
28
+ return failed(str(exc), code="metamorphic_declaration_invalid")
29
+ if not raw_declarations:
30
+ return not_testable("no metamorphic relations are declared", code="metamorphic_undeclared")
31
+ errors: list[str] = []
32
+ seen: set[str] = set()
33
+ outputs = proj.get("outputs") if isinstance(proj.get("outputs"), dict) else {}
34
+ for raw in raw_declarations:
35
+ try:
36
+ declaration = parse_declaration(raw)
37
+ except DeclarationError as exc:
38
+ errors.append(str(exc))
39
+ continue
40
+ if declaration.id in seen:
41
+ errors.append(f"{declaration.id}: duplicate relation id")
42
+ seen.add(declaration.id)
43
+ missing = [key for key in declaration.outputs if key not in outputs]
44
+ if missing:
45
+ errors.append(f"{declaration.id}: outputs {missing} are not declared outputs")
46
+ if errors:
47
+ return failed("; ".join(errors), code="metamorphic_declaration_invalid")
48
+ return passed(f"{len(raw_declarations)} metamorphic relation(s) are well-formed")
49
+
50
+
51
+ def relation_holds(
52
+ workspace: Path,
53
+ id: str,
54
+ project_dir: str = ".",
55
+ forbidden_fragments: list[str] | None = None,
56
+ ) -> AssertionResult:
57
+ """Execute the declared relation ``id`` in an isolated variant workspace."""
58
+ from ..metamorphic import DeclarationError, declared_relations, run_relation
59
+
60
+ root = project_root(workspace, project_dir)
61
+ proj = load_project_yaml(workspace, project_dir)
62
+ if proj is None:
63
+ return failed("project.yaml missing", code="manifest_missing")
64
+ try:
65
+ raw_declarations = declared_relations(proj)
66
+ except DeclarationError as exc:
67
+ return failed(str(exc), code="metamorphic_declaration_invalid")
68
+ matches = [raw for raw in raw_declarations if isinstance(raw, dict) and raw.get("id") == id]
69
+ if not matches:
70
+ return failed(f"no metamorphic relation with id {id!r} is declared", code="metamorphic_relation_undeclared")
71
+ result, evidence = run_relation(root, proj, matches[0], forbidden_fragments=tuple(forbidden_fragments or ()))
72
+ data: dict[str, Any] = dict(result.data)
73
+ data["evidence"] = evidence
74
+ return AssertionResult(result.status, result.detail, data)
@@ -259,3 +259,29 @@ def assumptions_have_rationale(workspace: Path, project_dir: str = ".") -> Asser
259
259
  code="assumption_missing_rationale",
260
260
  )
261
261
  return passed(f"all {len(assumptions)} assumptions have statement + rationale")
262
+
263
+
264
+ def parameters_match_steps(workspace: Path, project_dir: str = ".") -> AssertionResult:
265
+ """``runtime.implementation.parameters`` is well-formed and each parameter
266
+ bound to a processing step agrees with that step's declared value.
267
+
268
+ A manifest that advertises one threshold under ``parameters`` while the
269
+ step declares another is drift of the same kind as a presentation
270
+ control that disagrees with the pipeline: the whole view becomes a
271
+ confident lie. See ``openmapstack.parameters``.
272
+ """
273
+ from ..parameters import ParameterError, declared_parameters
274
+
275
+ proj = load_project_yaml(workspace, project_dir)
276
+ if proj is None:
277
+ return failed("project.yaml missing", code="manifest_missing")
278
+ try:
279
+ parameters = declared_parameters(proj)
280
+ except ParameterError as exc:
281
+ return failed(str(exc), code="parameters_invalid")
282
+ if not parameters:
283
+ return not_testable("no runtime parameters are declared", code="parameters_undeclared")
284
+ bound = sum(1 for parameter in parameters if parameter.step)
285
+ return passed(
286
+ f"{len(parameters)} runtime parameter(s) declared; {bound} bound to a processing step agree with it"
287
+ )