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.
- {openmapstack-0.2.0/openmapstack.egg-info → openmapstack-0.3.0}/PKG-INFO +34 -2
- openmapstack-0.2.0/PKG-INFO → openmapstack-0.3.0/README.md +30 -17
- {openmapstack-0.2.0 → openmapstack-0.3.0}/openmapstack/__init__.py +1 -1
- openmapstack-0.3.0/openmapstack/api.py +319 -0
- openmapstack-0.3.0/openmapstack/checks/metamorphic.py +74 -0
- {openmapstack-0.2.0 → openmapstack-0.3.0}/openmapstack/checks/project.py +26 -0
- {openmapstack-0.2.0 → openmapstack-0.3.0}/openmapstack/checks/provenance.py +53 -13
- {openmapstack-0.2.0 → openmapstack-0.3.0}/openmapstack/cli.py +261 -0
- openmapstack-0.3.0/openmapstack/connectors/__init__.py +356 -0
- openmapstack-0.3.0/openmapstack/connectors/duckdb_local.py +240 -0
- openmapstack-0.3.0/openmapstack/connectors/postgis.py +296 -0
- openmapstack-0.3.0/openmapstack/metamorphic.py +661 -0
- openmapstack-0.3.0/openmapstack/parameters.py +190 -0
- {openmapstack-0.2.0 → openmapstack-0.3.0}/openmapstack/rerun.py +66 -26
- openmapstack-0.3.0/openmapstack/schemas/check-result-v1.schema.json +30 -0
- {openmapstack-0.2.0 → openmapstack-0.3.0}/openmapstack/schemas/project-v1.schema.json +158 -1
- openmapstack-0.3.0/openmapstack/schemas/verify-result-v1.schema.json +50 -0
- openmapstack-0.3.0/openmapstack/snapshot.py +200 -0
- openmapstack-0.3.0/openmapstack/sources.py +257 -0
- {openmapstack-0.2.0 → openmapstack-0.3.0}/openmapstack/validation.py +19 -0
- {openmapstack-0.2.0 → openmapstack-0.3.0}/openmapstack/verify.py +33 -4
- openmapstack-0.2.0/README.md → openmapstack-0.3.0/openmapstack.egg-info/PKG-INFO +49 -0
- {openmapstack-0.2.0 → openmapstack-0.3.0}/openmapstack.egg-info/SOURCES.txt +18 -1
- {openmapstack-0.2.0 → openmapstack-0.3.0}/openmapstack.egg-info/requires.txt +4 -1
- {openmapstack-0.2.0 → openmapstack-0.3.0}/pyproject.toml +5 -2
- openmapstack-0.3.0/tests/test_check_api.py +194 -0
- openmapstack-0.3.0/tests/test_connectors.py +384 -0
- {openmapstack-0.2.0 → openmapstack-0.3.0}/tests/test_evals.py +114 -1
- openmapstack-0.3.0/tests/test_metamorphic.py +451 -0
- openmapstack-0.3.0/tests/test_snapshot.py +126 -0
- openmapstack-0.3.0/tests/test_sources.py +224 -0
- openmapstack-0.3.0/tests/test_verify_foreign.py +297 -0
- {openmapstack-0.2.0 → openmapstack-0.3.0}/LICENSE +0 -0
- {openmapstack-0.2.0 → openmapstack-0.3.0}/openmapstack/__main__.py +0 -0
- {openmapstack-0.2.0 → openmapstack-0.3.0}/openmapstack/checks/__init__.py +0 -0
- {openmapstack-0.2.0 → openmapstack-0.3.0}/openmapstack/checks/geodata.py +0 -0
- {openmapstack-0.2.0 → openmapstack-0.3.0}/openmapstack/checks/overrides.py +0 -0
- {openmapstack-0.2.0 → openmapstack-0.3.0}/openmapstack/checks/presentation.py +0 -0
- {openmapstack-0.2.0 → openmapstack-0.3.0}/openmapstack/checks/qgis.py +0 -0
- {openmapstack-0.2.0 → openmapstack-0.3.0}/openmapstack/checks/rerun.py +0 -0
- {openmapstack-0.2.0 → openmapstack-0.3.0}/openmapstack/checks/spatial.py +0 -0
- {openmapstack-0.2.0 → openmapstack-0.3.0}/openmapstack/checks/validation.py +0 -0
- {openmapstack-0.2.0 → openmapstack-0.3.0}/openmapstack/checks/visual.py +0 -0
- {openmapstack-0.2.0 → openmapstack-0.3.0}/openmapstack/expectations.py +0 -0
- {openmapstack-0.2.0 → openmapstack-0.3.0}/openmapstack/integrity.py +0 -0
- {openmapstack-0.2.0 → openmapstack-0.3.0}/openmapstack/project.py +0 -0
- {openmapstack-0.2.0 → openmapstack-0.3.0}/openmapstack/schema.py +0 -0
- {openmapstack-0.2.0 → openmapstack-0.3.0}/openmapstack/schemas/__init__.py +0 -0
- {openmapstack-0.2.0 → openmapstack-0.3.0}/openmapstack.egg-info/dependency_links.txt +0 -0
- {openmapstack-0.2.0 → openmapstack-0.3.0}/openmapstack.egg-info/entry_points.txt +0 -0
- {openmapstack-0.2.0 → openmapstack-0.3.0}/openmapstack.egg-info/top_level.txt +0 -0
- {openmapstack-0.2.0 → openmapstack-0.3.0}/setup.cfg +0 -0
- {openmapstack-0.2.0 → openmapstack-0.3.0}/tests/test_cli.py +0 -0
- {openmapstack-0.2.0 → openmapstack-0.3.0}/tests/test_expectations.py +0 -0
- {openmapstack-0.2.0 → openmapstack-0.3.0}/tests/test_release.py +0 -0
- {openmapstack-0.2.0 → openmapstack-0.3.0}/tests/test_rerun.py +0 -0
- {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.
|
|
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
|
|
@@ -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
|
+
)
|