Skip to content

Complete #13: metamorphic checks, source pins, connector pilot, and the OpenMapBench interface - #21

Merged
jaakla merged 8 commits into
mainfrom
feat/13-trustworthy-evaluation
Sep 2, 2026
Merged

Complete #13: metamorphic checks, source pins, connector pilot, and the OpenMapBench interface#21
jaakla merged 8 commits into
mainfrom
feat/13-trustworthy-evaluation

Conversation

@jaakla

@jaakla jaakla commented Sep 2, 2026

Copy link
Copy Markdown
Owner

Completes the remaining workstreams of #13: B4 (conditional metamorphic checks), B5a (source pin contract), B5b (safe connector pilot), C1 (versioned check API), C2 (skill snapshots and arm provenance), and C3 (paired arms and task export), plus the definition-of-done evidence that verify reports stably on projects whose data this repository has never seen.

What changed

B4 — conditional metamorphic checks (openmapstack/metamorphic.py, openmapstack/parameters.py)

  • validation.metamorphic[] declares three relations, each with the precondition that makes it valid: input_permutation_invariance (declared tie-break, unique key), duplicate_resistance (declared dedup key, set semantics only; counts and sums are rejected), positive_buffer_monotonicity (declared inclusion predicate, strictly growing variant).
  • Each relation runs the project's own canonical entrypoint in an isolated copy prepared like a clean rerun, perturbs only the copy, compares with the produced outputs, removes the copy, and fails if the project's immutable inputs were touched. Unmet data preconditions are not_testable with the reason; unknown relations and invalid declarations fail rather than skip.
  • runtime.implementation.parameters (openmapstack-parameters/v1) is the versioned parameter-addressing contract; project.parameters_match_steps catches drift between the declared canonical value and the bound step.
  • openmapstack verify --metamorphic; eval assertion metamorphic.relation_holds; cases 015 and 923–925 prove healthy, deliberate-defect, and isolation paths; unit tests cover positive, defect, and invalid-precondition paths for every relation.

B5a — source pin contract (openmapstack/sources.py)

  • pin.class: local_snapshot (hash-matched file under data/source/) or backend_snapshot (identifier, capture time, retention limit, optional verification). A snapshot that cannot deliver its bytes again is not_reproducible; a mutable alias in version.identifier stays source_unpinned whatever else is declared.
  • Secrets never enter project.yaml: access.connection must be a reference and every source is scanned for embedded credentials (source.credentials, provenance.no_inline_credentials); findings name the path and pattern, never the secret. Cases 926 and 927.

B5b — connector pilot (openmapstack/connectors/, openmapstack source discover|snapshot)

  • DuckDB local files and PostGIS as the reference pair. Credentials by reference; read-only sessions with a statement timeout; single SELECT only; dry run until --approve; row and byte limits; never overwrites; returns the pin block and query/schema digests. DuckDB file access is confined to the source root; PostGIS records pg_current_snapshot() as non-durable retrieval metadata because PostgreSQL has no durable time travel. Unverified backends are refused.
  • references/user-data-sources.md documents the four rules and the manual path for other warehouses. A new scheduled workflow runs the live PostGIS round trip against a service container; the same test ran green locally against postgis/postgis:16-3.4.

C1 — versioned check API (openmapstack/api.py, docs/openmapbench-interop.md, ADR 0004)

  • openmapstack-check-api/v1: api-info / negotiate, checks, check NAME WORKSPACE --arg k=v, packaged check-result and verify-result schemas. Reporting dimensions are owned by openmapstack.api.DIMENSIONS and imported by the eval runner; visual checks now report under visual_judgement, metamorphic under metamorphic_evidence. The consumer fixture test uses only the CLI and schemas.

C2 / C3 — snapshots, arm provenance, paired arms, task export (openmapstack/snapshot.py, evals/run.py)

  • openmapstack skill-snapshot --out DIR | --inspect DIR (openmapstack-skill-snapshot/v1), symlink- and escape-safe; the eval runner records the same snapshot.
  • Every live run records an openmapstack-benchmark-arm/v1 tuple per arm (skill hash, task-set hash, checker and API versions, harness commit, runtime, adapter and agent version, model, sampling, --price-catalog-date).
  • --arms paired runs plain and oms over identical cases, trials, and seeds and reports them side by side with task parity and trajectory diagnostics; no combined score. --export-tasks DIR writes openmapstack-benchmark-task/v1 bundles and marks 070–073 as OpenMapBench-owned.

Evidence

  • Unit suite: see the PR checks (locally: all tests pass under the coverage gate, including the live PostGIS test with a local container).
  • python evals/run.py --mode fixture: all contract and mutation cases pass; new mutations 923–927 are detected and isolated.
  • tests/test_verify_foreign.py verifies two projects absent from the fixtures against committed goldens.

Not in this PR

  • Moving cases 070–073 into the OpenMapBench repository itself and running the paired benchmark there; this side now provides the export, the API, and the provenance record it needs.
  • A hosted leaderboard, additional warehouse backends beyond the verified pair, and the other items the epic lists as out of scope.
  • No version bump; openmapstack gains new CLI surface (source, checks, check, api-info, skill-snapshot, verify --metamorphic), so the next release should be 0.3.0.

Closes #13.

🤖 Generated with Claude Code

https://claude.ai/code/session_01TMMy6A73zi7yGjK3wHBhju

jaakla and others added 6 commits September 2, 2026 16:42
…ddressing contract

Three no-golden-answer relations -- input permutation invariance, duplicate
resistance, and positive-buffer monotonicity -- each declare the
precondition that makes them valid, execute the project's own canonical
entrypoint in an isolated variant workspace, and report not_testable with
the reason when the precondition does not hold on the data. Unknown
relations, non-growing variants, and duplicate resistance declared for a
count or sum are declaration failures rather than skipped checks.

runtime.implementation.parameters (openmapstack-parameters/v1) addresses one
knob of the pipeline through an argument or environment binding; verify
fails when the declared canonical value drifts from the bound step.

The fixture generator gains the parameter plus three pipeline-logic break
modes that its copied pipeline.py reproduces on rerun; cases 015 and
923-925 prove the healthy, deliberate-defect, and isolation paths.

Refs #13 (B4).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TMMy6A73zi7yGjK3wHBhju
… sources

A source whose bytes come from a query must say which of two pin classes
froze them: a hash-matched local snapshot under data/source/, or a backend
snapshot with an identifier, capture time, and retention limit. A snapshot
that cannot deliver its bytes again -- missing, edited, expired, or last
verified inaccessible -- is reported as not_reproducible rather than
pinned because a string is present. A mutable alias such as 'latest' in
version.identifier stays unpinned whatever else is declared.

Secrets never enter project.yaml: access.connection must be a reference
(env:, service:, keyring:, or a file outside the project) and every
source block is scanned for password fragments, credentialed URLs, and
well-known key shapes. Findings name the path and pattern, never the
secret.

The fixture generator pins all three sources as local snapshots and gains
two break modes; cases 926 and 927 prove the failure paths.

Refs #13 (B5a).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TMMy6A73zi7yGjK3wHBhju
…ts (pilot)

openmapstack source discover/snapshot implements the connector pilot for
the reference pair, DuckDB local files and PostGIS. Credentials are
resolved from a reference (env:, service:, file:) and never recorded;
sessions are read-only with a statement timeout; only a single SELECT is
accepted; a snapshot is a dry run until --approve, is bounded by row and
byte limits, lands only under data/source/, never overwrites, and returns
the local_snapshot pin plus query and schema digests for the manifest.

The DuckDB connector confines file access to its root and exposes files as
views. PostGIS has no durable time travel, so its pin is the local
snapshot; pg_current_snapshot() is recorded as non-durable retrieval
metadata. Unverified backends are refused rather than approximated.

references/user-data-sources.md documents the four rules and the manual
path for other warehouses; a scheduled workflow runs the live PostGIS
round trip against a service container.

Refs #13 (B5b).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TMMy6A73zi7yGjK3wHBhju
openmapstack-check-api/v1 is the surface OpenMapBench consumes without
vendoring a check: api_info/negotiate for compatibility, list_checks for
the catalogue with parameters, oracle_free flags and reporting dimensions,
run_check returning an openmapstack-check-result/v1 record, and the
packaged verify-result schema. A check that raises is not_testable with
check_error, never a pass or a graded failure. Reporting dimensions are
owned by openmapstack.api and imported by the eval runner; visual checks
now report under their own visual_judgement bucket.

CLI: openmapstack checks, check NAME WORKSPACE --arg k=v, api-info
[--require-api --min-version --require-check]. The consumer fixture test
proves the contract from the CLI and JSON schemas alone.

Refs #13 (C1).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TMMy6A73zi7yGjK3wHBhju
…nd task export

openmapstack skill-snapshot --out DIR creates the controlled copy of
SKILL.md, references/, and templates/ with a per-file inventory and content
hash (openmapstack-skill-snapshot/v1); --inspect re-verifies one. Symlinks
and escaping inventory paths are refused. The eval runner now records the
same snapshot for the oms arm.

A live run records one openmapstack-benchmark-arm/v1 record per arm: skill
hash and commit, task-set hash, checker and check-API versions, harness
commit, runtime, adapter and agent version, model, sampling, and
--price-catalog-date; unknown values are null, never omitted.

--arms paired runs plain and oms over identical cases, trials, and seeds
and reports them side by side with task parity, per-arm quality with a
Wilson interval, per-arm cost, and trajectory diagnostics; no combined
score is published. --export-tasks writes vendor-neutral
openmapstack-benchmark-task/v1 bundles, marking cases 070-073 as
OpenMapBench-owned.

Refs #13 (C2, C3).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TMMy6A73zi7yGjK3wHBhju
Two synthetic projects built by pure-Python pipelines over invented
geodata (trail/river crossings with a metamorphic relation and a declared
parameter; facilities per district with an override, an unverified
expectation, and a warning) are verified with --metamorphic, and their
text and JSON reports are compared with committed goldens after path and
digest normalisation. The JSON also validates against the packaged
verify-result schema.

Writing the first project surfaced a genuine finding: GeoJSON without a
crs member reads as WGS84, so declaring EPSG:3301 for it fails
geodata.dataset_crs_is correctly. The project now writes the crs member;
the debugging notes record that trap and the pipeline-copy requirement
for metamorphic mutations.

Refs #13 (definition of done).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TMMy6A73zi7yGjK3wHBhju

@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: 1f55cea52f

ℹ️ 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 openmapstack/connectors/postgis.py Outdated
def _write_parquet(duck, destination: Path, columns, geometry_columns, srids, rows) -> None:
duck_types = {
"int2": "SMALLINT", "int4": "INTEGER", "int8": "BIGINT", "float4": "FLOAT", "float8": "DOUBLE",
"numeric": "DOUBLE", "bool": "BOOLEAN", "date": "DATE", "timestamp": "TIMESTAMP", "timestamptz": "TIMESTAMPTZ",

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 Preserve exact NUMERIC values in PostGIS snapshots

When a PostGIS query returns a numeric column with more precision than an IEEE-754 double can represent—common for identifiers and high-precision measurements—this mapping silently rounds the value while materializing the supposedly immutable snapshot. The resulting Parquet file is then hashed and accepted as reproducible even though it no longer contains the source value; map numeric to a lossless decimal representation instead.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Fixed in 5af4cda: NUMERIC is now materialised as DECIMAL(p, s) with the widest precision and scale present in the result, or as exact text beyond DECIMAL(38). The live PostGIS test seeds numeric(24, 9) and asserts an exact round trip; a unit test covers the beyond-38 fallback.

Comment thread evals/run.py
Comment on lines +646 to +648
for case_dir in sorted(case_dirs):
case_def = _load_case(case_dir)
ids.append(case_def.get("id", case_dir.name))

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 Hash only tasks actually selected for live execution

When live mode uses the default unfiltered case selection, case_dirs includes fixture-only cases that run_case later marks skipped, but this loop still adds all of their IDs and files to the arm's task_set. Published arm provenance therefore claims an exact task set containing many tasks that were never run, so comparisons cannot reliably identify the workload; filter this input to live-capable cases before building the IDs and digest.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Fixed in 5af4cda: the task-set hash now covers only live-capable cases, so fixture-only cases that live mode skips no longer enter an arm's workload identity. The paired-arm test includes a fixture-only neighbour and asserts it is absent.

Comment thread evals/run.py
Comment on lines +717 to +718
"tool_surface": {"adapter": agent_name, "agent_version": agent_version},
"model": {"provider": provider, "id": model, "revision": metadata.get("model_revision")},

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 Record the resolved adapter and model in arm provenance

When --agent is omitted, the runner still resolves a per-case adapter, but this record emits a null adapter/provider because it uses the CLI override rather than agent_run.agent. It also records the requested model instead of agent_run.model, even though the Claude adapter deliberately reports the model actually observed. Thus otherwise successful benchmark arms can publish missing or incorrect execution identity despite that information being present in the normalized trial records.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Fixed in 5af4cda: arm provenance takes the adapter and model from the normalized trial records (what actually ran), falling back to the CLI values only when no trial reported them. Test runs without --agent and with a requested alias, and asserts the resolved adapter and observed model.

Comment thread openmapstack/snapshot.py Outdated
Comment on lines +72 to +76
for entry in sorted(entries, key=lambda item: item["path"]):
digest.update(entry["path"].encode("utf-8"))
digest.update(b"\0")
digest.update(bytes.fromhex(entry["sha256"].removeprefix("sha256:")))
digest.update(b"\0")

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 Keep the historical skill content-hash algorithm

The previous benchmark runner hashed each relative path followed by the raw file bytes, whereas this implementation hashes the path followed by the file's SHA-256 digest bytes. Consequently, an unchanged skill receives a different content_sha256 after upgrading to this commit, contradicting the stated historical-comparability guarantee and making old and new benchmark arms appear to use different skill content. Preserve the prior raw-byte algorithm or explicitly version the digest scheme.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Fixed in 5af4cda: the content hash is the historical raw-byte algorithm again (path, NUL, bytes, NUL per file in path order), so an unchanged skill keeps the hash recorded by earlier arms. A test pins the algorithm inline.

Comment thread openmapstack/sources.py Outdated
Comment on lines +227 to +230
if scheme == "file":
target = project_path(root, remainder.strip())
if target is not None:
return "access.connection file references must point outside the project directory"

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 Reject relative file connection references during validation

For a reference such as file:../secrets/postgis.dsn, project_path returns None because the path resolves outside the project, so this validator accepts it. The connector subsequently rejects the same reference because resolve_connection_reference requires file: paths to be absolute, meaning preflight and no_inline_credentials can pass a manifest that every source operation will reject. Enforce the same absolute-path rule here.

AGENTS.md reference: AGENTS.md:L139-L142

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Fixed in 5af4cda: validation now applies the connector's rule (absolute path, outside the project) to file: references, so preflight and provenance.no_inline_credentials reject file:../secrets/postgis.dsn the same way source discover/snapshot would.

jaakla and others added 2 commits September 2, 2026 17:32
… references

- PostGIS NUMERIC is materialised as DECIMAL with the widest precision and
  scale present, or as exact text beyond DECIMAL(38); a DOUBLE mapping
  rounded identifiers and high-precision measurements inside a snapshot
  that was then hashed and pinned as immutable. Live and unit tests round
  trip numeric(24, 9) exactly.
- The arm's task-set hash covers only live-capable cases; fixture-only
  cases in the selection are skipped in live mode and no longer inflate
  the workload identity.
- Arm provenance records the adapter and model that actually ran, taken
  from the normalized trial records, rather than the CLI override and the
  requested alias.
- The skill snapshot content hash is the historical raw-byte algorithm
  (path, NUL, bytes, NUL), so an unchanged skill keeps the hash recorded
  in earlier benchmark arms; a test pins the algorithm.
- file: connection references must be absolute and outside the project at
  validation time, matching the connector, so preflight cannot accept a
  reference every source operation refuses.

Refs #13, PR #21 review.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TMMy6A73zi7yGjK3wHBhju
The package gains substantial new CLI surface in this release: openmapstack
source discover/snapshot, checks, check, api-info, skill-snapshot, and
verify --metamorphic, plus the openmapstack-check-api/v1 interface external
harnesses consume.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TMMy6A73zi7yGjK3wHBhju
@jaakla
jaakla merged commit d4e1bd3 into main Sep 2, 2026
4 checks passed
@jaakla
jaakla deleted the feat/13-trustworthy-evaluation branch September 2, 2026 18:17
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.

Epic: trustworthy evaluation across CI, user projects, and public benchmarks

1 participant