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
1 change: 1 addition & 0 deletions .geodebug.toml.example
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
schema_version = "1"
# GeoDebug project policy.
profile = "default"
fail_on = "error"
Expand Down
32 changes: 32 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,38 @@ jobs:
- name: Integration tests
run: pytest tests/integration

package:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
cache: pip
- name: Install build tooling
run: python -m pip install build twine
- name: Build distributions
run: python -m build
- name: Validate distributions
run: python -m twine check dist/*
- name: Install wheel in clean environment
run: |
python -m venv .wheel-smoke
.wheel-smoke/bin/python -m pip install --upgrade pip
.wheel-smoke/bin/python -m pip install dist/*.whl
.wheel-smoke/bin/geodebug --version
.wheel-smoke/bin/geodebug schema > /tmp/report.schema.json
.wheel-smoke/bin/geodebug schema --kind config > /tmp/config.schema.json
.wheel-smoke/bin/python - <<'PY'
import json
from pathlib import Path

report = json.loads(Path("/tmp/report.schema.json").read_text())
config = json.loads(Path("/tmp/config.schema.json").read_text())
assert report["properties"]["schema_version"]["const"] == "1.0.0"
assert config["properties"]["schema_version"]["const"] == "1"
PY

smoke:
strategy:
matrix:
Expand Down
42 changes: 42 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
name: Release

on:
push:
tags:
- "v*"

permissions:
contents: write

jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Install build tooling
run: python -m pip install build twine
- name: Verify tag matches package version
run: |
VERSION="$(python - <<'PY'
from pathlib import Path
import re

text = Path("src/geodebug/version.py").read_text()
match = re.search(r'__version__ = "([^"]+)"', text)
if match is None:
raise SystemExit("package version not found")
print(match.group(1))
PY
)"
test "${GITHUB_REF_NAME}" = "v${VERSION}"
- name: Build distributions
run: python -m build
- name: Validate distributions
run: python -m twine check dist/*
- name: Create GitHub release
env:
GH_TOKEN: ${{ github.token }}
run: gh release create "${GITHUB_REF_NAME}" dist/* --generate-notes --verify-tag
18 changes: 18 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Changelog

All notable changes to GeoDebug are documented here.

## 0.1.0 - 2026-09-19

### Added

- Deterministic fact, rule, diagnostic, and report kernel.
- GeoJSON, Pyogrio, Rasterio, GeoParquet, and GeoPandas adapters.
- Metadata-first `inspect`, `check`, and `compare` workflows.
- Operation-aware `preflight` checks.
- Project policy through `.geodebug.toml`, including profiles, rule overrides,
disable lists, and reasoned suppressions.
- Canonical report schema `1.0.0` and config schema `1`.
- Eleven built-in high-signal rules across CRS, vector, raster, relations, and
operation semantics.
- Golden false-positive contracts and cross-platform CI.
24 changes: 14 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,20 +10,21 @@ stable integration boundary for CLI, CI, and future agent tooling.

## Install

Core GeoJSON support:
PyPI publication is not enabled for 0.1.0. Install the tagged release directly
from GitHub:

```bash
pip install geodebug
pip install "geodebug @ git+https://github.com/GeoGeekLab/geodebug.git@v0.1.0"
```

Optional format support:
Optional format support can be installed from the same tag:

```bash
pip install "geodebug[vector]" # GPKG, Shapefile, FlatGeobuf
pip install "geodebug[raster]" # GeoTIFF, COG, VRT
pip install "geodebug[parquet]" # GeoParquet
pip install "geodebug[geopandas]" # in-memory GeoDataFrame
pip install "geodebug[all]"
pip install "geodebug[vector] @ git+https://github.com/GeoGeekLab/geodebug.git@v0.1.0"
pip install "geodebug[raster] @ git+https://github.com/GeoGeekLab/geodebug.git@v0.1.0"
pip install "geodebug[parquet] @ git+https://github.com/GeoGeekLab/geodebug.git@v0.1.0"
pip install "geodebug[geopandas] @ git+https://github.com/GeoGeekLab/geodebug.git@v0.1.0"
pip install "geodebug[all] @ git+https://github.com/GeoGeekLab/geodebug.git@v0.1.0"
```

## Usage
Expand Down Expand Up @@ -68,6 +69,7 @@ GeoDebug searches the current directory and its parents for `.geodebug.toml`. An
config path can be supplied with `--config`.

```toml
schema_version = "1"
profile = "default"
fail_on = "error"

Expand Down Expand Up @@ -120,7 +122,7 @@ adapter -> facts -> rules -> diagnostics -> report
Rules never read files directly and adapters never emit diagnostics. Unknown evidence remains
`UNKNOWN`; it is never silently treated as a pass.

See [`docs/architecture.md`](docs/architecture.md) for the kernel contracts.
See [`docs/architecture.md`](docs/architecture.md) for the kernel contracts and [`docs/rules.md`](docs/rules.md) for the released diagnostic contracts.

## Development

Expand All @@ -135,4 +137,6 @@ pytest
```

Golden rule cases live in `tests/golden/cases.toml`. Each case declares both expected
diagnostics and rules that must not be reported.
diagnostics and rules that must not be reported. The release gate also enforces
four-state rule contracts, metamorphic corrections, clean-corpus silence, and
installable wheel validation.
26 changes: 17 additions & 9 deletions docs/architecture.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Kernel architecture
# Architecture

GeoDebug separates observation from diagnosis.

Expand Down Expand Up @@ -27,16 +27,24 @@ Report
- Adapters observe sources and emit facts. They do not diagnose.
- Rules consume normalized facts. They do not perform I/O.
- `UNKNOWN` is distinct from `PASS`.
- Policy can change presentation severity or suppress a finding, but it never
changes a rule truth value.
- Diagnostics carry evidence and a stable fingerprint.
- Rule IDs are stable public identifiers once released.
- The canonical JSON report is the integration boundary for CLI, CI, and future agent tooling.
- Released rule IDs are stable public identifiers.
- The canonical JSON report is the integration boundary for CLI, CI, and agent tooling.
- Report schema `1.0.0` and config schema `1` are independently versioned
from the Python package.

## Sentinel rules
## Rule scopes

The kernel starts with three rules chosen to exercise different scopes:
Dataset rules evaluate each subject independently. Relation rules evaluate a
multi-subject context. Operation rules require an explicit operation context.

- `GEO101`: dataset metadata semantics
- `GEO201`: vector geometry semantics
- `GEO501`: operation-aware measurement semantics
A new rule is not release-ready until its `PASS`, `FAIL`, `UNKNOWN`, and
`NOT_APPLICABLE` states are exercised by the contract suite.

New rules should not be added until their PASS / FAIL / UNKNOWN / NOT_APPLICABLE boundaries are explicit in tests.
## Cost model

Metadata rules may run during ordinary checks. Full-scan rules require explicit
deep inspection. GeoDebug does not silently turn an expensive scan into a
default operation.
13 changes: 13 additions & 0 deletions docs/releasing.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Releasing

GeoDebug releases are tag-driven.

1. Ensure CI is green on `main`.
2. Confirm `CHANGELOG.md` contains the release entry.
3. Confirm `src/geodebug/version.py` contains the release version.
4. Create and push a matching `vX.Y.Z` tag.
5. The release workflow verifies the tag/version match, builds sdist and wheel,
runs `twine check`, and creates the GitHub Release with both artifacts.

PyPI publication is intentionally not automated in 0.1.0. Configure trusted
publishing and add a separate publish job before enabling registry publication.
17 changes: 17 additions & 0 deletions docs/rules.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Diagnostic rules

GeoDebug 0.1.0 ships eleven built-in rules. Rule IDs are stable public identifiers.

| Rule | Scope | Default | Description |
| --- | --- | --- | --- |
| [GEO101](rules/GEO101.md) | dataset | warning | Missing CRS |
| [GEO103](rules/GEO103.md) | dataset | error | Geographic coordinate out of range |
| [GEO105](rules/GEO105.md) | dataset | warning | Projected data outside CRS area of use |
| [GEO201](rules/GEO201.md) | dataset | error | Invalid geometry |
| [GEO301](rules/GEO301.md) | dataset | error | Invalid or singular raster transform |
| [GEO304](rules/GEO304.md) | dataset | warning | NoData value conflicts with valid mask |
| [GEO402](rules/GEO402.md) | relation | error | No spatial overlap |
| [GEO404](rules/GEO404.md) | relation | warning | Raster grid misalignment |
| [GEO501](rules/GEO501.md) | operation | error | Buffer on angular CRS |
| [GEO502](rules/GEO502.md) | operation | error | Planar area on geographic CRS |
| [GEO503](rules/GEO503.md) | operation | error | Planar distance on geographic CRS |
16 changes: 16 additions & 0 deletions docs/rules/GEO101.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# GEO101 — Missing CRS

**Scope:** dataset
**Default severity:** warning
**Certainty:** deterministic

Reports when a dataset explicitly has no declared coordinate reference system.
It does not guess a CRS from coordinate values.

**PASS:** CRS presence is explicitly established.
**FAIL:** CRS metadata is explicitly absent.
**UNKNOWN:** CRS presence cannot be established.
**NOT_APPLICABLE:** no dataset subject is available.

**Remediation:** establish the CRS from authoritative source metadata before
assigning it. Do not infer and write a CRS solely from plausible bounds.
17 changes: 17 additions & 0 deletions docs/rules/GEO103.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# GEO103 — Geographic coordinate out of range

**Scope:** dataset
**Default severity:** error
**Certainty:** deterministic

Checks declared geographic data for implausible angular coordinate ranges.
Latitude must remain within ±90 degrees; longitude is allowed within the
commonly encountered -360 to 360 degree domain.

**PASS:** geographic bounds are within supported angular ranges.
**FAIL:** observed bounds exceed those ranges.
**UNKNOWN:** finite bounds cannot be established.
**NOT_APPLICABLE:** the CRS is not geographic.

This rule diagnoses a mismatch between declared CRS semantics and coordinates;
it does not identify the correct CRS.
17 changes: 17 additions & 0 deletions docs/rules/GEO105.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# GEO105 — Outside CRS area of use

**Scope:** dataset
**Default severity:** warning
**Certainty:** deterministic

Checks projected datasets against the area of use published by the CRS
definition. Dataset bounds are transformed to geographic coordinates before
comparison.

**PASS:** the observed extent overlaps the CRS area of use.
**FAIL:** the observed extent is outside the CRS area of use.
**UNKNOWN:** CRS, area-of-use, or transform evidence is unavailable.
**NOT_APPLICABLE:** the CRS is not projected.

A failure can indicate either an inappropriate projected CRS or incorrect CRS
metadata. GeoDebug does not choose a replacement CRS automatically.
18 changes: 18 additions & 0 deletions docs/rules/GEO201.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# GEO201 — Invalid geometry

**Scope:** dataset
**Default severity:** error
**Certainty:** deterministic
**Cost:** full scan

Reports vector geometries that fail the geometry engine's validity predicate.
File-based checks require deep inspection when validity is not available from
metadata.

**PASS:** the scanned dataset contains no invalid geometries.
**FAIL:** one or more invalid geometries are present.
**UNKNOWN:** geometry validity has not been scanned.
**NOT_APPLICABLE:** the subject is not vector data.

Automatic geometry repair is intentionally not performed because repair can
change geometry type and topology.
13 changes: 13 additions & 0 deletions docs/rules/GEO301.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# GEO301 — Invalid or singular raster transform

**Scope:** dataset
**Default severity:** error
**Certainty:** deterministic

Validates the six affine transform coefficients and checks that the linear
transform is invertible.

**PASS:** coefficients are finite and the determinant is non-zero.
**FAIL:** coefficients are malformed/non-finite or the transform is singular.
**UNKNOWN:** transform metadata is unavailable.
**NOT_APPLICABLE:** the subject is not raster data.
17 changes: 17 additions & 0 deletions docs/rules/GEO304.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# GEO304 — NoData conflicts with valid mask

**Scope:** dataset
**Default severity:** warning
**Certainty:** deterministic
**Cost:** full scan

Reports bands where pixels equal to the declared NoData value are simultaneously
marked valid by the dataset validity mask.

**PASS:** a deep scan finds no conflicts.
**FAIL:** one or more bands contain mask/NoData conflicts.
**UNKNOWN:** a required deep scan has not been performed.
**NOT_APPLICABLE:** the raster has no NoData value or the subject is not raster data.

The rule does not assume that merely observing the NoData numeric value is a
semantic collision.
15 changes: 15 additions & 0 deletions docs/rules/GEO402.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# GEO402 — No spatial overlap

**Scope:** relation
**Default severity:** error
**Certainty:** deterministic

Compares two dataset extents in a common CRS. A failure means their transformed
bounding boxes do not overlap.

**PASS:** transformed extents overlap.
**FAIL:** transformed extents do not overlap.
**UNKNOWN:** comparable bounds cannot be established.
**NOT_APPLICABLE:** the context does not contain exactly two subjects.

Bounding-box overlap is not treated as proof that geometries intersect.
15 changes: 15 additions & 0 deletions docs/rules/GEO404.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# GEO404 — Raster grid misalignment

**Scope:** relation
**Default severity:** warning
**Certainty:** deterministic

Checks whether two rasters with the same CRS and resolution share the same
pixel lattice. Integer-pixel origin offsets are aligned; fractional offsets are
not.

**PASS:** grids share the pixel lattice.
**FAIL:** a fractional-pixel offset is present.
**UNKNOWN:** CRS/resolution is incompatible or rotation/skew prevents the
current deterministic test.
**NOT_APPLICABLE:** the relation is not between raster grids.
16 changes: 16 additions & 0 deletions docs/rules/GEO501.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# GEO501 — Buffer on angular CRS

**Scope:** operation
**Default severity:** error
**Certainty:** deterministic

Reports a planar buffer operation on a geographic CRS when the supplied
distance would be interpreted in angular coordinate units.

**PASS:** the input CRS is projected.
**FAIL:** the input CRS is geographic and a distance is supplied.
**UNKNOWN:** CRS semantics or the distance parameter is unavailable.
**NOT_APPLICABLE:** the operation is not `buffer`.

GeoDebug recommends projecting to an appropriate linear CRS before a metric
buffer; it does not select that CRS automatically.
Loading
Loading