Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions SKILL.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
name: open-map-stack
description: "Use textual agent instructions for GIS and geospatial work: source discovery and provenance, vector/raster/point-cloud pipelines, CRS and metric analysis, spatial SQL, routing and isochrones, QGIS projects, tile generation, and web maps. Use advanced tools and formats such as OSM, Overture, STAC, Sentinel/Landsat, LiDAR, GeoPackage, GeoParquet, COG, PMTiles, WMS/WFS/OGC APIs, GDAL, GeoPandas, DuckDB Spatial, PostGIS, QGIS, MapLibre, and Estonian spatial data including ETAK and EPSG:3301. Open-first, with hosted services when scale or reliability requires them. Do not use for casual map references, simple place lookups, or ordinary travel directions without analytical GIS work."
description: "Use textual agent instructions for GIS and geospatial work: source discovery and provenance, vector/raster/point-cloud pipelines, CRS and metric analysis, spatial SQL, routing and isochrones, QGIS projects, tile generation, and web maps. Use advanced tools and formats such as OSM, Overture, STAC, Sentinel/Landsat, LiDAR, GeoPackage, GeoParquet, COG, PMTiles, WMS/WFS/OGC APIs, Portolan catalogs, GDAL, GeoPandas, DuckDB Spatial, PostGIS, QGIS, MapLibre, and Estonian spatial data including ETAK and EPSG:3301. Open-first, with hosted services when scale or reliability requires them. Do not use for casual map references, simple place lookups, or ordinary travel directions without analytical GIS work."
---

# OpenMapStack Toolkit
Expand Down Expand Up @@ -59,7 +59,7 @@ Hard rules for every material analysis — each is expanded in `references/proje

| If the task involves... | Read |
|---|---|
| Finding or sourcing data (OSM, Overture, Sentinel, Landsat, building footprints, regional portals, STAC catalogs, MCP-based discovery) | `references/data-sources.md` |
| Finding or sourcing data (OSM, Overture, Sentinel, Landsat, building footprints, regional portals, STAC and Portolan catalogs, MCP-based discovery) | `references/data-sources.md` |
| Reading the user's own warehouse or database (PostGIS, DuckDB, GeoParquet directories): credentials by reference, read-only discovery, approved snapshots, pin classes | `references/user-data-sources.md` |
| Choosing local processing vs online/hosted/SaaS services for global or continental scale; basemaps, elevation, routing, geocoding, place search, postcode lookup APIs | `references/services-and-scale.md` |
| Choosing a format, converting between formats, or any CRS / projection / EPSG question | `references/formats-and-crs.md` |
Expand Down Expand Up @@ -135,7 +135,7 @@ For simple one-shot questions (single CRS conversion, one `ogr2ogr` invocation),

Before diving into a task, classify it:

1. **Discovery** ("what data exists for…?", "is there a dataset of…?") → start with `data-sources.md`. STAC search if raster; Overture or OSM if vector basemap.
1. **Discovery** ("what data exists for…?", "is there a dataset of…?", "read this catalog") → start with `data-sources.md`. STAC search if raster; Overture or OSM if vector basemap; for a Portolan catalog read its `AGENTS.md` before querying.
2. **Conversion / CRS** ("convert this to…", "reproject to…", "the projection looks wrong") → `formats-and-crs.md`. Usually one `ogr2ogr` or `gdalwarp` call.
3. **Analysis** ("what's the average elevation in…", "how many buildings within 500m of…", "where are the hotspots?") → `analytics.md` and likely `processing.md`. Push to DuckDB/PostGIS first.
4. **Delivery** ("publish this as a web map", "generate tiles for…") → `web-delivery.md`. PMTiles + Martin + MapLibre is the default.
Expand Down
53 changes: 53 additions & 0 deletions evals/cases/017-portolan-catalog/expected.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
id: 017-portolan-catalog
case_type: positive
modes: [live]
score_types:
live: agent_benchmark
project_dir: project
hard_gate: true
live:
prompt_file: prompt.md
agent_workdir: project
fixtures:
- { source: ../../fixtures/mini-portolan/catalog.json, destination: project/catalog/catalog.json }
- { source: ../../fixtures/mini-portolan/AGENTS.md, destination: project/catalog/AGENTS.md }
- { source: ../../fixtures/mini-portolan/README.md, destination: project/catalog/README.md }
- { source: ../../fixtures/mini-portolan/parcels/collection.json, destination: project/catalog/parcels/collection.json }
- { source: ../../fixtures/mini-portolan/parcels/AGENTS.md, destination: project/catalog/parcels/AGENTS.md }
- { source: ../../fixtures/mini-portolan/parcels/README.md, destination: project/catalog/parcels/README.md }
- { source: ../../fixtures/mini-portolan/parcels/parcels.parquet, destination: project/catalog/parcels/parcels.parquet }
- { source: ../../fixtures/mini-portolan/parcels/parcels-upstream.geojson, destination: project/catalog/parcels/parcels-upstream.geojson }

# Does the shipped guidance actually change what an agent does with a Portolan
# catalog? Two behaviours decide it, and both are graded by consequence rather
# than by inspecting prose:
#
# 1. `parcels/AGENTS.md` states the count rule -- `record_status` carries one
# superseded historical row, and every count must filter it out. Three
# rows match ARIMAA; two are current. An agent that queried without
# reading the collection's agent guide reports 3 and fails row_count.
# 2. The GeoParquet holds the `data` role under an opaque asset key, while
# the pre-conversion GeoJSON is keyed `parcels` and carries `source`.
# Selecting by key rather than by role lands on the upstream extract,
# whose columns are the publisher's originals (`siht1`, `tunnus`), so a
# wrong pick cannot silently produce the right number.
#
# The provenance assertions grade the other half of the guidance: a Portolan
# collection already publishes SPDX license, producer/host providers and a
# version identity, so a project built from one has no excuse for an unpinned
# or unattributed source.
assertions:
- assert: project.conforms_to_schema
- assert: project.parses
- assert: project.graph_resolves
- assert: geodata.row_count
args:
path: data/derived/candidates.parquet
equals: 2
- assert: provenance.every_source_has_provider_and_access
- assert: provenance.every_source_pinned
- assert: provenance.license_present_where_required
- assert: provenance.rationale_present
- assert: validation.required_all_present
- assert: validation.no_implicit_pass
- assert: validation.run_record_matches
14 changes: 14 additions & 0 deletions evals/cases/017-portolan-catalog/prompt.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# Read a Portolan catalog

A Portolan catalog is published under `./catalog/` in this workspace. Its root
is `catalog/catalog.json`.

Compile a reproducible OpenMapStack project that answers: **how many parcels in
that catalog are zoned `ARIMAA`?** Write the qualifying parcels to
`data/derived/candidates.parquet`.

Treat the catalog as the authoritative source: pin it in `project.yaml` with
the provider, license, and version identity it publishes, and carry its
attribution requirement into the project's warnings or license fields. Do not
copy the catalog's files into `data/source/` and re-describe them as your own
extract — cite the collection.
8 changes: 8 additions & 0 deletions evals/fixtures/mini-portolan/AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# Agent guide — mini-tartu-portolan

One collection: `parcels`. Read `parcels/AGENTS.md` before you query it. It
documents a record-status column that changes every count in this catalog.

All geometry is EPSG:3301 (Estonian L-EST97), which is a projected CRS in
metres. Compute lengths and areas directly; do not reproject to EPSG:4326
first.
6 changes: 6 additions & 0 deletions evals/fixtures/mini-portolan/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# mini-tartu-portolan

A deliberately small Portolan catalog: one collection of cadastral parcels
covering a few blocks of Tartu, Estonia. It exists so agent behaviour against
the Portolan conventions can be graded without depending on a third-party
catalog being reachable.
13 changes: 13 additions & 0 deletions evals/fixtures/mini-portolan/catalog.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"type": "Catalog",
"stac_version": "1.0.0",
"id": "mini-tartu-portolan",
"description": "Minimal Portolan catalog used by the OpenMapStack eval suite. Cadastral parcels for a small part of Tartu, converted from an Estonian open-data extract.",
"links": [
{ "rel": "root", "href": "./catalog.json", "type": "application/json" },
{ "rel": "self", "href": "./catalog.json", "type": "application/json" },
{ "rel": "child", "href": "./parcels/collection.json", "type": "application/json", "title": "Cadastral parcels" },
{ "rel": "agents", "href": "./AGENTS.md", "type": "text/markdown" },
{ "rel": "describedby", "href": "./README.md", "type": "text/markdown" }
]
}
34 changes: 34 additions & 0 deletions evals/fixtures/mini-portolan/parcels/AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# Agent guide — parcels

## Count rule, read this before you query

`record_status` is `current` or `superseded`. A `superseded` row is a
historical version of a parcel that a later record replaced; the parcel it
describes still appears in the collection under its current record. **Filter
`record_status = 'current'` in every count, area, and selection.** Including
superseded rows double-counts parcels that were revised, and there is no
warning when you do — the geometry and the land-use value both look valid.

```sql
SELECT count(*) FROM read_parquet('.../parcels.parquet')
WHERE land_use = 'ARIMAA' AND record_status = 'current';
```

## Keys and CRS

- `cadastral_id` is the join key. It is not unique across the file: it is
unique only among `current` rows.
- Geometry is EPSG:3301, a projected CRS in metres. Areas and distances are
computed directly in it. Do not reproject to EPSG:4326 to measure.

## Assets

The GeoParquet is the `data` asset. The GeoJSON carries the `source` role — it
is the upstream extract before conversion, keeps the publisher's original
column names, and is not the file to query. Select on `roles`, not on the
asset key.

## License

CC-BY-4.0, produced by Maa- ja Ruumiamet. Attribution is required in anything
derived from it.
14 changes: 14 additions & 0 deletions evals/fixtures/mini-portolan/parcels/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# parcels

Cadastral parcels with legal land-use designation, for a few blocks of Tartu.

| Column | Meaning |
|---|---|
| `cadastral_id` | Stable cadastral identifier, unique among current records |
| `land_use` | Legal designation: `ARIMAA` (commercial), `ELAMUMAA` (residential), `MAATULUNDUSMAA` (agricultural), `TOOTMISMAA` (industrial) |
| `municipality` | Municipality name |
| `record_status` | `current` or `superseded` — see AGENTS.md |
| `geometry` | Polygon, EPSG:3301 |

Converted from the publisher's extract; the pre-conversion file is published
beside it with the `source` role.
50 changes: 50 additions & 0 deletions evals/fixtures/mini-portolan/parcels/collection.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
{
"type": "Collection",
"stac_version": "1.0.0",
"stac_extensions": [
"https://schemas.portolan-sdi.org/portolan/v0.2.0/schema.json",
"https://stac-extensions.github.io/table/v1.2.0/schema.json"
],
"id": "parcels",
"title": "Cadastral parcels (mini Tartu extract)",
"description": "Cadastral parcels with legal land-use designation, converted to GeoParquet from an Estonian Land and Spatial Development Board extract.",
"license": "CC-BY-4.0",
"updated": "2026-09-01T00:00:00Z",
"extent": {
"spatial": { "bbox": [[26.68, 58.35, 26.76, 58.39]] },
"temporal": { "interval": [["2026-01-01T00:00:00Z", null]] }
},
"providers": [
{ "name": "Maa- ja Ruumiamet", "roles": ["producer", "licensor"], "url": "https://geoportaal.maaruum.ee/" },
{ "name": "OpenMapStack eval fixtures", "roles": ["host"], "url": "https://example.org/mini-portolan" }
],
"table:columns": [
{ "name": "cadastral_id", "type": "string", "description": "Stable cadastral identifier." },
{ "name": "land_use", "type": "string", "description": "Legal land-use designation as published (ARIMAA, ELAMUMAA, MAATULUNDUSMAA, TOOTMISMAA)." },
{ "name": "municipality", "type": "string", "description": "Municipality name." },
{ "name": "record_status", "type": "string", "description": "current or superseded. See AGENTS.md before counting." },
{ "name": "geometry", "type": "geometry", "description": "Parcel polygon, EPSG:3301." }
],
"assets": {
"b7f2": {
"href": "./parcels.parquet",
"type": "application/vnd.apache.parquet",
"title": "Parcels, GeoParquet",
"roles": ["data"]
},
"parcels": {
"href": "./parcels-upstream.geojson",
"type": "application/geo+json",
"title": "Upstream extract before conversion",
"roles": ["source"]
}
},
"links": [
{ "rel": "root", "href": "../catalog.json", "type": "application/json" },
{ "rel": "parent", "href": "../catalog.json", "type": "application/json" },
{ "rel": "self", "href": "./collection.json", "type": "application/json" },
{ "rel": "agents", "href": "./AGENTS.md", "type": "text/markdown" },
{ "rel": "describedby", "href": "./README.md", "type": "text/markdown" },
{ "rel": "via", "href": "https://geoportaal.maaruum.ee/eng/spatial-data/cadastral-data-p310.html", "type": "text/html", "title": "Original publisher page" }
]
}
Loading
Loading