Skip to content

feat(skill): route Portolan catalogs, without reimplementing them - #30

Merged
jaakla merged 3 commits into
mainfrom
feat/26-portolan-catalogs
Sep 9, 2026
Merged

feat(skill): route Portolan catalogs, without reimplementing them#30
jaakla merged 3 commits into
mainfrom
feat/26-portolan-catalogs

Conversation

@jaakla

@jaakla jaakla commented Sep 9, 2026

Copy link
Copy Markdown
Owner

Closes #26.

What I found first

The issue asks for a skill to consume Portolan datasets, and suggests consulting portolan-sdi/portolan-skills when needed. That suggestion is the right call, and more so than it looks: reading-portolan already exists there and is exactly the consumer skill described — metadata and AGENTS.md, assets by role, DuckDB over GeoParquet, COGs, cross-dataset joins, partitioned collections, PMTiles maps. It is Apache-2.0, built on the Agent Skills open standard, and tracks the spec version in pins.toml with a drift checker.

So this PR does not reimplement it. Writing a second, thinner copy of someone else's maintained skill would drift within a release.

What was actually missing

Reading a Portolan catalog is the access pattern this toolkit already defaults to — static files on object storage, queried in place with DuckDB and httpfs. The gap was recognition and routing: an agent handed a catalog root had no way to know what it was looking at, that the AGENTS.md beside every collection is instruction addressed to it, or that dedicated skills exist.

references/data-sources.md gains a ## Portolan catalogs section:

  • how to recognize one (catalog.json root, AGENTS.md/README.md per collection, the schemas.portolan-sdi.org version URI) and the directory layout;
  • the working rules that are easy to get wrong: read AGENTS.md before writing a query, select assets by roles and never by asset key, use the https href rather than hand-rewriting it to s3://;
  • pointers to portolan-skills (preferred when installed) and portolan-spec as ground truth.

SKILL.md gains the term in the three places an agent routes through — the module table, the discovery triage line, and the frontmatter description — and nothing else. The detail belongs in the reference, per AGENTS.md.

The part that is genuinely ours

Pinning. A Portolan collection already carries an SPDX license, providers, version identity and via links, so the section maps those onto project.yaml fields rather than inventing provenance — and names two traps that would otherwise produce a confident wrong manifest:

  • a catalog whose producer and host differ is a mirror, not the authority. Its top-level updated is the last sync time, so pinning it records the mirror's freshness and attributes the wrong party;
  • file:checksum is multihash-encoded, not a raw sha256 (PORTO-CORE-029). Copying it into a sha256: pin field records a value that can never match the bytes, and local_snapshot pins are verified against real content.

Neutrality

No install command is embedded. portolan-skills ships as Agent Skills and works across tools, but installing a plugin is agent-specific, so the reference names the repo and leaves the mechanism to the host — consistent with AGENTS.md on keeping shared instructions vendor-neutral.

Coverage

SKILL.md is a router whose pointers nothing checked, and fixture evals grade produced projects rather than the routing table. tests/test_skill_routing.py asserts every reference it names exists, every reference is reachable from it, and that Portolan routes to the section documenting it. Both failure paths were confirmed by deliberately breaking them.

464 unit tests pass; fixture evals 16/16 contract_ci and 26/26 mutation.

Not included

A live eval exercising an agent against a real catalog. That needs a public catalog plus model credentials and would be stochastic; the deterministic layers cannot measure whether an agent uses the guidance. Worth a follow-up issue if you want it graded rather than documented.

🤖 Generated with Claude Code

https://claude.ai/code/session_01DqoLGyKY5opHGjNMSerpHg

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: e7028b1135

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread examples/tartu-development/pipeline.py Outdated
cadastre_url = "https://s3.pilw.io/rp-kemit-kataster/ANDMED/Tartu_maakond_KATASTER_GPKG.zip"

if not cadastre_gpkg.exists():
if not _reuse_cached("Cadastral GeoPackage", cadastre_gpkg, cadastre_gpkg.exists(), refresh):

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Capture metadata for the refreshed cadastre

When --refresh downloads a newer mutable cadastre archive here, the manifest still hard-codes download_timestamp and version to 2026-08-25 at lines 390-391, while project.yaml retains the old ETag. finalize_run then copies that timestamp into the source access fields, so a pre-commit refresh can record new bytes and counts as an older snapshot, defeating the example's provenance and reproducibility guarantees. Capture the response/archive timestamp, version, and ETag during this download instead.

AGENTS.md reference: AGENTS.md:L90-L91

Useful? React with 👍 / 👎.

Comment on lines +50 to +53
self.assertIn("Portolan", self.skill)
data_sources = (REPO_ROOT / "references/data-sources.md").read_text(encoding="utf-8")
self.assertIn("## Portolan catalogs", data_sources)
self.assertIn("AGENTS.md", data_sources)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Exercise Portolan behavior in an eval

These assertions only check for three strings, so they remain green if the shipped guidance no longer causes an agent to inspect collection metadata, select assets by role, or produce a correctly pinned result. Because this commit materially changes shipped agent behavior, add an eval case that actually exercises a representative Portolan catalog task rather than treating keyword presence as behavioral coverage.

AGENTS.md reference: AGENTS.md:L87-L90

Useful? React with 👍 / 👎.

and roads_meta.get("matched") == roads_meta.get("returned")
and roads_meta.get("returned") == len(roads_raw.get("features", []))
)
roads_cache_valid = _reuse_cached("ETAK main roads", roads_geojson, roads_cache_valid, refresh)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Bypass cache parsing when refresh is requested

If an existing roads cache or its metadata is truncated or invalid JSON, execution fails in the preceding json.loads calls before this new refresh check runs; the education cache follows the same pattern at lines 282-294. Thus pipeline.py --refresh cannot recover from a damaged cache despite promising to discard cached sources. Skip reading cached files when refresh is true, or remove them before validating them.

Useful? React with 👍 / 👎.

jaakla and others added 2 commits September 9, 2026 13:49
Portolan publishes geospatial data as a static STAC catalog on object
storage -- GeoParquet plus PMTiles for vector, COG for raster, plain
Parquet for tabular -- with no API server. Reading one is the access
pattern this toolkit already defaults to, so almost nothing new is needed:
DuckDB with httpfs over remote GeoParquet already works.

What was missing is recognition and routing. An agent handed a catalog
root had no way to know what it was looking at, that `AGENTS.md` beside
every collection is instruction meant for it, or that dedicated skills
exist. So `data-sources.md` gains a Portolan section covering the layout,
the working rules that are easy to get wrong (read AGENTS.md before
querying, select assets by `roles` rather than key, use the https href),
and pointers to `portolan-sdi/portolan-skills` and the spec as ground
truth. `SKILL.md` gains the term in three places an agent routes through,
and nothing more -- the detail belongs in the reference.

The additive part is pinning, which is this toolkit's concern and not the
reading skill's. A collection already carries an SPDX license, providers,
version identity and `via` links, so the section maps them onto the
manifest fields rather than inventing provenance, and names two traps:

- a catalog whose producer and host differ is a mirror, so its `updated`
  is a sync time and the authority is behind the `via` link;
- `file:checksum` is multihash-encoded (PORTO-CORE-029), so copying it
  into a `sha256:` pin records a value that can never match the bytes.

Coverage: SKILL.md is a router whose pointers nothing checked. The new
tests assert every reference it names exists, every reference is reachable
from it, and that Portolan routes to the section documenting it. Both
failure paths were confirmed by breaking them.

Closes #26

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DqoLGyKY5opHGjNMSerpHg
Review was right that the routing test proves only that three strings are
present. It stays green if the guidance stops changing what an agent does,
which is the thing the change is for.

So add `017-portolan-catalog`, a live case over a fixture catalog that
grades two behaviours by consequence:

- `parcels/AGENTS.md` carries the count rule. `record_status` holds one
  superseded historical row, and every count must exclude it. Three rows
  match ARIMAA; two are current. An agent that queried the GeoParquet
  without reading the collection's agent guide answers 3 and fails
  `geodata.row_count`;
- the GeoParquet holds the `data` role under an opaque asset key, while
  the pre-conversion GeoJSON is keyed `parcels` and carries `source`.
  Picking by key lands on the upstream extract, whose columns are the
  publisher's originals, so a wrong pick surfaces rather than quietly
  returning the right number.

The provenance assertions grade the rest: a collection publishes an SPDX
license, producer/host providers and a version identity, so a project
built from one has no excuse for an unpinned or unattributed source.

The fixture is local, so the case needs no third-party catalog to be
reachable and cannot fail on someone else's outage. It runs in live mode
only -- an agent is the thing under test -- so fixture CI skips it, which
is honest rather than free: `not_testable` is not `passed`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DqoLGyKY5opHGjNMSerpHg
@jaakla
jaakla force-pushed the feat/26-portolan-catalogs branch from c1f1278 to ec7f314 Compare September 9, 2026 13:50
The fixture catalog cannot falsify the guidance -- we wrote both, so they
agree by construction. These tests check the same claims against the
published Fields of the World catalog instead, the one the Portolan
browser links to.

It immediately earned its place. `predictions/vectors` carries no `data`
asset at all: its roles are visual, style, thumbnail, documentation,
metadata, and the GeoParquet sits behind a `partition:glob` and 210 item
links, with the `data` role on each item. An agent following the reference
as written -- "select assets by roles, `data` is the primary Parquet" --
would have found no data and concluded the collection was empty. The
reference now says a collection without a `data` asset is partitioned
rather than empty, and where to look instead.

The multihash note is confirmed rather than corrected: every `file:checksum`
on the Estonia item begins `1220`, sha2-256 in multihash, so the warning
against copying one into a `sha256:` pin field describes something real.

Opt-in, because a third-party outage must never redden this repository:
the tests skip unless OPENMAPSTACK_NETWORK_TESTS=1, and skip rather than
fail when the service cannot be reached. A skip is `not_testable`, not a
pass. Wiring them into a scheduled job would give them recurring value, but
that is a separate decision from having them.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DqoLGyKY5opHGjNMSerpHg
@jaakla
jaakla merged commit a9cf23d into main Sep 9, 2026
2 checks passed
@jaakla
jaakla deleted the feat/26-portolan-catalogs branch September 9, 2026 14:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add a skill to consume Portolan datasets

1 participant