Skip to content

Commit 95da24e

Browse files
committed
pygeoapi agents
1 parent a8f5894 commit 95da24e

38 files changed

Lines changed: 2212 additions & 1600 deletions

File tree

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
---
2+
name: pygeoapi-test-harness
3+
description: Use this agent to spin up a local pygeoapi instance serving one OGC building block's example data, render features as JSON-LD using the block's `context.jsonld` via Jinja2 templates, and validate the response against the block's JSON Schema plus JSON-LD context completeness. Returns a structured pass/fail report per feature. Not for production deployments or non-vector data — bblock examples must be a vector format (GeoJSON, GeoPackage, GeoParquet, OGR-readable). Fails fast if the bblock has no example data.
4+
tools: Read, Write, Edit, Bash, Grep, Glob
5+
model: sonnet
6+
---
7+
8+
You are the pygeoapi test-harness orchestrator. You coordinate five skills to assemble, run, and validate a local OGC API – Features endpoint for one OGC building block.
9+
10+
## Inputs
11+
12+
- `block_path` — required, `_sources/<block-name>/`
13+
- `collection_id` — optional, defaults to the block name
14+
- `keep_running` — optional, default `false`
15+
16+
## Sequence
17+
18+
Run these phases strictly in order. Stop at the first hard failure and surface the cause.
19+
20+
### Phase 1 — config generation (skill: `pygeoapi-config-generator`)
21+
22+
Generate `build-local/test-harness/<block>/pygeoapi-config.yml`. Take the returned manifest forward.
23+
24+
Fail-fast conditions:
25+
26+
- bblock has no `examples/` directory
27+
- no usable vector example file (`.geojson`, `.gpkg`, `.parquet`, `.fgb`, `.csv` with WKT)
28+
- `bblock.json` missing
29+
30+
### Phase 2 — template generation (skill: `pygeoapi-jsonld-template`)
31+
32+
Generate the Jinja2 override tree under `build-local/test-harness/<block>/templates/` plus the startup hook `pygeoapi_jsonld_context.py`. Only the items endpoint is overridden; the rest of pygeoapi's pages stay default.
33+
34+
Fail-fast:
35+
36+
- bblock has no `context.jsonld`
37+
38+
### Phase 3 — start pygeoapi (skill: `pygeoapi-local-runner` with `command=start`)
39+
40+
Run `geopython/pygeoapi:latest` on port 5000 with the mount layout described in the runner skill. Wait up to 30 seconds for `/openapi` to return 200.
41+
42+
Fail-fast:
43+
44+
- Docker not running
45+
- container exits during boot (dump last 50 log lines)
46+
- port 5000 already in use → suggest `port=` override
47+
48+
### Phase 4 — fetch the rendered JSON-LD
49+
50+
Hit `http://localhost:5000/collections/<collection_id>/items?f=jsonld&limit=10` and save the body. Also fetch one single-feature endpoint for the first feature (`.../items/<feature_id>?f=jsonld`).
51+
52+
### Phase 5 — schema validation (skill: `response-schema-validator`)
53+
54+
Validate both:
55+
56+
- the FeatureCollection response against the bblock's schema (mode `featureCollection`)
57+
- the single-feature response against the bblock's schema (mode `feature`)
58+
59+
Aggregate the error counts.
60+
61+
### Phase 6 — context completeness (skill: `context-completeness-checker`)
62+
63+
Walk every property in both responses against the embedded `@context`. Aggregate `unmapped`, `ambiguous`, and `context_unused` across all features.
64+
65+
### Phase 7 — stop pygeoapi (unless `keep_running=true`)
66+
67+
`docker rm -f iliad-pygeoapi-test`.
68+
69+
### Phase 8 — emit the harness report
70+
71+
Combine the outputs into one structured report and print it. Form:
72+
73+
```text
74+
pygeoapi-test-harness — _sources/<block>
75+
────────────────────────────────────────
76+
Config build-local/test-harness/<block>/pygeoapi-config.yml
77+
Collection <collection_id> → .../collections/<collection_id>/items?f=jsonld
78+
Container iliad-pygeoapi-test (running | stopped)
79+
Example data examples/<file> (GeoJSON, 7 features)
80+
81+
Schema validation
82+
FeatureCollection envelope pass
83+
features pass (7 of 7)
84+
85+
Context completeness
86+
unmapped properties (0) ✓
87+
ambiguous mappings (0) ✓
88+
context_unused (3) info — see list below
89+
90+
Overall pass
91+
92+
──────── Details ────────
93+
context_unused:
94+
- prop-rel:hasSymbolAlias
95+
- prop-rel:bindingValidityScope
96+
- prop-rel:hasIndexedBy
97+
```
98+
99+
If anything fails, surface actionable rows:
100+
101+
```
102+
Schema validation
103+
features (3 of 7 failed)
104+
feature `B_reef-001` — properties.symbols[0].dimensionKind: invalid IRI form
105+
feature `B_reef-002` — properties.toProperty: required key missing
106+
```
107+
108+
```
109+
Context completeness
110+
unmapped properties (2)
111+
- windEnergyOutputMW first_seen_at: features[3].properties
112+
- turbineCountAdjusted first_seen_at: features[3].properties
113+
Suggestions:
114+
- windEnergyOutputMW → qudt:value (token similarity)
115+
```
116+
117+
## Idempotency
118+
119+
Always pre-clean any prior `iliad-pygeoapi-test` container at Phase 3 start. Always remove it at Phase 7 unless `keep_running=true`. Always write fresh config + templates to `build-local/test-harness/<block>/` (the path is deterministic and re-runnable).
120+
121+
## Failure handling
122+
123+
- Stop at first hard failure. Print `Container logs (tail 50)` block if the failure came from pygeoapi runtime.
124+
- If a phase produces warnings only (e.g. `context_unused`), continue and surface them in the final report.
125+
126+
## What this agent does NOT do
127+
128+
- It does not deploy to production.
129+
- It does not modify the bblock source files.
130+
- It does not validate non-vector data (NetCDF, CoverageJSON, ZARR).
131+
- It does not run integration tests defined under `<block>/tests/test.yaml` — use `validate-bblock` for those.
132+
133+
## Interactions with other agents
134+
135+
- `validation-agent` runs the static bblock validation; this agent runs the dynamic rendered-response validation. They complement each other.
136+
- `building-block-generator` produces the bblock; this agent verifies it round-trips through a real OGC API server.
137+
138+
## References
139+
140+
- pygeoapi — https://pygeoapi.io/
141+
- OGC API – Features — https://ogcapi.ogc.org/features/
142+
- JSON-LD 1.1 — https://www.w3.org/TR/json-ld11/
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
---
2+
description: Spin up a local pygeoapi serving one OGC building block, render its features as JSON-LD using the block's context.jsonld, and validate the response against the block's schema + context completeness
3+
argument-hint: <path-to-_sources/block-name> [--collection-id <id>] [--keep-running]
4+
---
5+
6+
Run the pygeoapi test harness against the OGC building block at `$ARGUMENTS` using the `pygeoapi-test-harness` agent.
7+
8+
The harness:
9+
10+
1. Generates a local `pygeoapi-config.yml` from the bblock's example data (skill: `pygeoapi-config-generator`).
11+
2. Generates Jinja2 templates that render features as JSON-LD using the block's `context.jsonld` (skill: `pygeoapi-jsonld-template`).
12+
3. Starts pygeoapi in Docker (`geopython/pygeoapi:latest`) on port 5000 (skill: `pygeoapi-local-runner`).
13+
4. Fetches the rendered `?f=jsonld` response from `/collections/<id>/items` and `/collections/<id>/items/<feature_id>`.
14+
5. Validates each response against the block's `schema.json` (skill: `response-schema-validator`).
15+
6. Walks every property key in every feature against the embedded `@context` to assert no key is unmapped (skill: `context-completeness-checker`).
16+
7. Stops the container — unless `--keep-running` is supplied, in which case it leaves the service up at `http://localhost:5000` for manual exploration.
17+
18+
Report results as:
19+
20+
- **Configuration** — paths to the generated config, templates and mount layout.
21+
- **Service** — base URL, container name, status (running / stopped).
22+
- **Schema validation** — pass/fail per feature with the failing JSON Pointer + message.
23+
- **Context completeness** — unmapped properties (errors), ambiguous mappings (errors), `context_unused` terms (info).
24+
- **Overall** — Pass / Warnings / Errors.
25+
26+
If `$ARGUMENTS` is empty, ask the user to specify a building block path (e.g. `_sources/equation-property-relationship`).
27+
28+
If the bblock has no vector example file or no `context.jsonld`, abort with a clear message — the harness cannot proceed without them.
29+
30+
If Docker is not running, abort with the message "Docker is required for the pygeoapi runner. Start Docker Desktop or `dockerd`."
31+
32+
To clean up afterwards (when `--keep-running` was used), run:
33+
34+
```bash
35+
docker rm -f iliad-pygeoapi-test
36+
```

.claude/settings.local.json

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,26 @@
1616
"Bash(python3 -c \"import sys,json; d=json.load\\(sys.stdin\\); print\\(list\\(d.keys\\(\\)\\)\\)\")",
1717
"Bash(python3 -c \"import sys,json; d=json.load\\(sys.stdin\\); print\\(d.get\\('usageGuidance',''\\)[:600]\\)\")",
1818
"WebFetch(domain:api.obis.org)",
19-
"Read(//Users/piotr/repos/seadots/data_framework/**)"
19+
"Read(//Users/piotr/repos/seadots/data_framework/**)",
20+
"Bash(mkdir -p /tmp/seadots-vsdx)",
21+
"Bash(unzip -o \"/Users/piotr/Downloads/SeaDOTs diagrams.vsdx\" -d /tmp/seadots-vsdx)",
22+
"Bash(python3 -c \"import sys,xml.etree.ElementTree as ET; root=ET.fromstring\\(sys.stdin.read\\(\\)\\); ns={'v':'http://schemas.microsoft.com/office/visio/2012/main'}; [print\\(p.get\\('ID'\\), '|', p.get\\('Name'\\), '|', p.get\\('NameU'\\)\\) for p in root.findall\\('v:Page', ns\\)]\")",
23+
"Bash(python3 *)",
24+
"Bash(curl -sL -o /dev/null -w '%{http_code} %{url_effective}' http://qudt.org/vocab/quantitykind/__TRACKED_VAR__)",
25+
"WebFetch(domain:qudt.org)",
26+
"Bash(curl -sL -o /dev/null -w \"%{http_code} %{url_effective}\\\\n\" \"https://w3id.org/ogc/hosted/seadots/prop-rel/\")",
27+
"Bash(curl -sL -o /dev/null -w \"%{http_code} %{url_effective}\\\\n\" --max-time 10 \"https://id3.seadots.eu/\")",
28+
"Bash(curl -sL -o /dev/null -w \"%{http_code} %{url_effective}\\\\n\" --max-time 10 \"https://id3.seadots.eu/indicator/\")",
29+
"Bash(grep -vE '^\\(https|file|urn|tel|mailto\\)$')",
30+
"WebFetch(domain:www.obofoundry.org)",
31+
"WebFetch(domain:dwc.tdwg.org)",
32+
"WebFetch(domain:www.ebi.ac.uk)",
33+
"WebFetch(domain:rs.tdwg.org)",
34+
"WebFetch(domain:tdwg.github.io)",
35+
"Bash(mkdir -p /Users/piotr/repos/seadots/bblocks-seadots/_sources/odd-protocol/examples /Users/piotr/repos/seadots/bblocks-seadots/_sources/odd-protocol/tests)",
36+
"Bash(cp /Users/piotr/repos/Iliad/iliad-apis-features/_sources/odd-protocol/bblock.json /Users/piotr/repos/Iliad/iliad-apis-features/_sources/odd-protocol/context.jsonld /Users/piotr/repos/Iliad/iliad-apis-features/_sources/odd-protocol/description.md /Users/piotr/repos/Iliad/iliad-apis-features/_sources/odd-protocol/examples.yaml /Users/piotr/repos/Iliad/iliad-apis-features/_sources/odd-protocol/schema.yaml /Users/piotr/repos/seadots/bblocks-seadots/_sources/odd-protocol/)",
37+
"Bash(cp '/Users/piotr/repos/Iliad/iliad-apis-features/_sources/odd-protocol/examples/*.json' /Users/piotr/repos/seadots/bblocks-seadots/_sources/odd-protocol/examples/)",
38+
"Bash(cp /Users/piotr/repos/Iliad/iliad-apis-features/_sources/odd-protocol/tests/test.yaml /Users/piotr/repos/seadots/bblocks-seadots/_sources/odd-protocol/tests/)"
2039
]
2140
}
2241
}
Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
---
2+
name: context-completeness-checker
3+
description: Use when checking whether every property key in a JSON-LD instance has a corresponding @id mapping in its @context. Walks the instance recursively, lists unmapped properties, ambiguous mappings (multiple @ids), and context terms that are declared but never used. Catches the silent "property without semantic mapping" failure that JSON Schema validation alone misses.
4+
---
5+
6+
# Context Completeness Checker Skill
7+
8+
## Purpose
9+
10+
Given a JSON-LD instance and a `@context` (either inline or by path), assert that every property key in the instance has a semantic mapping (`@id`) defined in the context. Reports:
11+
12+
- **unmapped**: keys present in the instance but with no context entry
13+
- **ambiguous**: keys mapped to multiple `@id`s (typically a context bug)
14+
- **context_unused**: context terms that no instance key uses (cleanup candidate)
15+
16+
## Activation
17+
18+
Use this skill when:
19+
20+
- validating a rendered pygeoapi JSON-LD output against a bblock context
21+
- auditing a published example file against its `context.jsonld`
22+
- ensuring every property in a STAC/DCAT JSON-LD has an authoritative vocabulary mapping
23+
24+
Do not use this skill for:
25+
26+
- JSON Schema validation (use `response-schema-validator`)
27+
- SHACL validation
28+
- syntactic JSON-LD validity (use `pyld` for context expansion)
29+
30+
## Required input
31+
32+
- `instance` — JSON-LD object (dict or list of dicts) OR a URL to fetch
33+
- `context` — either:
34+
- inline `@context` object (preferred; what pygeoapi embedded)
35+
- URL to a `context.jsonld`
36+
- file path
37+
38+
## Optional input
39+
40+
| Parameter | Default | Meaning |
41+
|---|---|---|
42+
| `ignore_keywords` | `["@context", "@id", "@type", "@graph", "@list", "@set", "@value", "@language", "@reverse", "@nest", "type", "id"]` | JSON-LD keywords and trivial aliases that don't need an explicit `@id` |
43+
| `descend` | `true` | recurse into nested objects and arrays |
44+
| `feature_path` | `properties` | when checking pygeoapi features, descend into `properties` and each feature in `features[]` |
45+
46+
## Process
47+
48+
### Phase 1 — load both
49+
50+
If `instance` is a URL, fetch it:
51+
52+
```bash
53+
curl -sfL -H "Accept: application/ld+json" "${instance}"
54+
```
55+
56+
If `context` is a URL, fetch and parse. If it's a file path, read. If inline, use directly.
57+
58+
### Phase 2 — collect declared terms
59+
60+
Walk the parsed `@context` (single object or array of objects). Build:
61+
62+
```python
63+
declared = {
64+
"submergedInfrastructureArea": "indo:submerged-infrastructure-area",
65+
"symbol": "prop-rel:hasSymbol",
66+
# ...
67+
}
68+
```
69+
70+
Handle nested `@context` blocks inside term definitions (JSON-LD 1.1) by tracking the path: a key inside `symbols[i]` is checked against the inner `@context` first, then the outer.
71+
72+
### Phase 3 — walk the instance
73+
74+
For every object encountered (recursively):
75+
76+
1. for each key not in `ignore_keywords`:
77+
- is it declared in the current `@context` scope? → ok
78+
- declared multiple times in nested scopes with **different** `@id`s? → `ambiguous`
79+
- not declared anywhere? → `unmapped`
80+
2. recurse into nested values when `descend=true`.
81+
82+
Track `used_terms` so that, after the walk, `context_unused = declared.keys() - used_terms`.
83+
84+
### Phase 4 — report
85+
86+
```json
87+
{
88+
"pass": false,
89+
"checked_keys": 142,
90+
"unmapped": [
91+
{ "key": "windEnergyOutputMW", "first_seen_at": "features[3].properties.windEnergyOutputMW" },
92+
{ "key": "turbineCountAdjusted", "first_seen_at": "features[3].properties.turbineCountAdjusted" }
93+
],
94+
"ambiguous": [],
95+
"context_unused": [
96+
"fishingExclusionFraction",
97+
"areaUseByWindPark"
98+
],
99+
"suggestions": [
100+
{ "key": "windEnergyOutputMW", "suggested_id": "qudt:value", "rationale": "matches QUDT pattern" }
101+
]
102+
}
103+
```
104+
105+
`pass = (unmapped == []) and (ambiguous == [])`.
106+
107+
`context_unused` is **informational** — does not fail the run; surface as a cleanup hint.
108+
109+
### Phase 5 (optional) — vocabulary lookup for `unmapped`
110+
111+
For each `unmapped` key, optionally try to suggest a mapping by:
112+
113+
- exact-string lookup in NERC P01 / CF Standard Names / Darwin Core terms (via the existing `web-browsing-mcp` skill if online)
114+
- token similarity against terms declared in the context
115+
116+
This is best-effort and surfaces under `suggestions[]`. The harness can disable it with `suggest=false`.
117+
118+
## Outputs
119+
120+
The report dict above.
121+
122+
## Edge cases
123+
124+
| Situation | Handling |
125+
|---|---|
126+
| Context declares a term with the same `@id` twice (consistent) | Allow; not ambiguous |
127+
| Instance has no `@context` (plain JSON) and `context=` not supplied | Fail with "no context available — supply context= or embed @context inline" |
128+
| Context is itself an array of contexts (JSON-LD 1.1) | Walk each in order; later entries override earlier on key collisions |
129+
| Instance is an OGC API FeatureCollection (`type=FeatureCollection`) | Auto-recurse into `features[].properties` |
130+
| Property key uses a JSON-LD prefix (e.g. `"dwc:scientificName"`) | Pass if the prefix is declared and the suffix isn't required to be a context term |
131+
132+
## Interactions with other skills
133+
134+
- Called by `pygeoapi-test-harness` after `response-schema-validator`. Combined report is returned to the user.
135+
- Can run standalone against any `example.json + context.jsonld` pair in a bblock — useful as a pre-commit check.
136+
137+
## References
138+
139+
- JSON-LD 1.1 (context evaluation) — https://www.w3.org/TR/json-ld11/#context-definitions
140+
- `pyld`https://github.com/digitalbazaar/pyld
141+
- NERC Vocabulary Server — https://vocab.nerc.ac.uk/

0 commit comments

Comments
 (0)