Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/znippy-zoomies

osm-katana/tests/geometry_gate_in_the_chain.rs

//! **The geometry-quality gate, IN the chain** (katana-data lane row 39).
//!
//! # The hole this file was written against
//!
//! The cut chain's post-condition step (korp-collectors `osm_cut_verify::check_extract`,
//! called from its `osm_cut.rs:2568`) asserts *foreign nodes*, *wholly-foreign ways*,
//! *WKB shape* and *presence*. Every one of those asks **where** a geometry is. Not one
//! asks whether it is a **shape at all** — and the census that does ask,
//! `osm-katana verify --geometry-quality`, was reachable only from the CLI. It is what
//! found the 11 510 bowties in the relation rung columns on 2026-09-02; the chain that
//! produced them reported success.
//!
//! So a cut whose ways carry an unclosed ring and a bowtie passes today. The first test
//! here is that sentence, run: it plants exactly those two defects in a fixture cut,
//! shows the chain's own row/null verdict is GREEN on it, and requires the gate to
//! refuse it.
//!
//! # Run
//!
//! ```text
//! cargo test -p osm-katana --test geometry_gate_in_the_chain -- --nocapture
//! ```

use std::path::{Path, PathBuf};
use std::sync::Arc;

use arrow::array::{ArrayRef, BinaryBuilder, Int64Builder};
use arrow::datatypes::{DataType, Field, Schema};
use arrow::record_batch::RecordBatch;
use osm_katana::verify::{
    check_cut_geometry, Budget, GeomQuality, GeometryBaseline, EXTRACT_TABLES,
};
use parquet::arrow::ArrowWriter;

// ── WKB by hand, one defect per row ──────────────────────────────────────────
//
// Hand-built rather than produced by the encoder on purpose: a fixture that goes
// through the writer can only ever carry the defects the writer is capable of, and the
// gate has to hold against the ones that arrive from a re-cut, a rung repack, or an
// encoder that has not been written yet.

/// A WKB POLYGON from `rings`. With `close=false` the ring is left OPEN — the defect
/// `is_closed_ring` turns into an outline where a fill was meant.
fn poly(rings: &[&[(f64, f64)]], close: bool) -> Vec<u8> {
    let mut b = vec![1u8];
    b.extend(3u32.to_le_bytes());
    b.extend((rings.len() as u32).to_le_bytes());
    for ring in rings {
        let mut r: Vec<(f64, f64)> = ring.to_vec();
        if close {
            if let (Some(&f), Some(&l)) = (r.first(), r.last()) {
                if f != l {
                    r.push(f);
                }
            }
        }
        b.extend((r.len() as u32).to_le_bytes());
        for (x, y) in &r {
            b.extend(x.to_le_bytes());
            b.extend(y.to_le_bytes());
        }
    }
    b
}

const SQUARE: &[(f64, f64)] = &[(15.0, 60.0), (15.1, 60.0), (15.1, 60.1), (15.0, 60.1)];
/// Closed, four segments, two of them properly crossing, and ASYMMETRIC so its
/// shoelace area is non-zero — a symmetric bowtie would also land in `zero_area` and
/// blur which counter fired.
const BOWTIE: &[(f64, f64)] = &[(16.0, 60.0), (16.1, 60.1), (16.1, 60.0), (16.0, 60.2)];

fn write_geom_table(path: &Path, rows: &[Option<Vec<u8>>]) {
    let mut gb = BinaryBuilder::new();
    for r in rows {
        match r {
            Some(v) => gb.append_value(v),
            None => gb.append_null(),
        }
    }
    let geom: ArrayRef = Arc::new(gb.finish());
    let schema = Schema::new(vec![Field::new("geometry", DataType::Binary, true)]);
    let batch = RecordBatch::try_new(Arc::new(schema), vec![geom]).unwrap();
    let f = std::fs::File::create(path).unwrap();
    let mut w = ArrowWriter::try_new(f, batch.schema(), None).unwrap();
    w.write(&batch).unwrap();
    w.close().unwrap();
}

fn write_id_table(path: &Path, ids: &[i64]) {
    let mut ib = Int64Builder::new();
    for i in ids {
        ib.append_value(*i);
    }
    let ids: ArrayRef = Arc::new(ib.finish());
    let schema = Schema::new(vec![Field::new("id", DataType::Int64, false)]);
    let batch = RecordBatch::try_new(Arc::new(schema), vec![ids]).unwrap();
    let f = std::fs::File::create(path).unwrap();
    let mut w = ArrowWriter::try_new(f, batch.schema(), None).unwrap();
    w.write(&batch).unwrap();
    w.close().unwrap();
}

/// A four-table fixture cut shaped like the real one: `nodes`, `ways`, `relations`
/// (raw, carrying no geometry — that is CORRECT, see `EXTRACT_TABLES`) and
/// `relations.resolved`. Every table has rows and every geometry table has shapes, so
/// the row/null verdict has nothing to complain about — which is the point.
///
/// `bad` plants ONE unclosed outer ring and ONE bowtie in `ways.parquet`.
fn plant_cut(tag: &str, bad: bool) -> PathBuf {
    let dir = std::env::temp_dir().join(format!("kd39_{tag}_{}", std::process::id()));
    let _ = std::fs::remove_dir_all(&dir);
    std::fs::create_dir_all(&dir).unwrap();

    write_geom_table(
        &dir.join("nodes.parquet"),
        &[Some(osm_katana::shared::geometry::encode_wkb_point(15.0, 60.0))],
    );

    let mut ways: Vec<Option<Vec<u8>>> = vec![
        Some(poly(&[SQUARE], true)),
        Some(poly(&[SQUARE], true)),
        None,
    ];
    if bad {
        ways.push(Some(poly(&[SQUARE], false))); // UNCLOSED outer ring
        ways.push(Some(poly(&[BOWTIE], true))); // SELF-INTERSECTING
    }
    write_geom_table(&dir.join("ways.parquet"), &ways);

    write_id_table(&dir.join("relations.parquet"), &[1, 2, 3]);
    write_geom_table(
        &dir.join("relations.resolved.parquet"),
        &[Some(poly(&[SQUARE], true))],
    );
    dir
}

/// **RED-first: a cut carrying a known-bad ring must not pass the chain's verify.**
///
/// SEEN RED on t14s 2026-09-06 before [`check_cut_geometry`] existed, with the first
/// half of this test asserting a refusal:
///
/// ```text
/// nodes.parquet: 1 rows, fault=None      ways.parquet: 5 rows, fault=None
/// relations.parquet: 3 rows, fault=None  relations.resolved.parquet: 1 rows, fault=None
/// panicked: the chain's verify step must REFUSE a cut carrying 1 unclosed ring +
///           1 bowtie in ways.parquet — it returned 4 tables, all green
/// ```
///
/// That first half stays here on purpose: it is the reason the second half exists, and
/// if someone ever teaches `verify_dir` to count rings, this assertion is what tells
/// them the gate below became a second opinion rather than the only one.
#[test]
fn a_cut_with_a_planted_bad_ring_is_refused_by_the_chains_verify() {
    let dir = plant_cut("bad", true);

    // What the chain judged before this row: rows, nulls, the pack band. All green.
    let rows_verdict = osm_katana::verify::verify_dir(&dir, &EXTRACT_TABLES).unwrap();
    assert!(
        rows_verdict.iter().all(|t| t.ok()),
        "the row/null verdict is GREEN on a cut with an open ring and a bowtie in it — \
         that is the hole this gate closes, not a bug in verify_dir",
    );

    // What it must judge as well: the shapes.
    let g = check_cut_geometry(&dir, &EXTRACT_TABLES, &GeometryBaseline::ZERO).unwrap();
    assert!(
        !g.ok(),
        "the geometry gate MUST refuse a cut whose ways carry an unclosed ring and a \
         bowtie — it passed: {g:#?}",
    );
    let reason = g.reason().expect("a refusal MUST carry a reason");
    assert!(
        reason.contains("ways.parquet::geometry"),
        "the refusal names the column that carries the defect: {reason}",
    );
    assert!(
        reason.contains("unclosed_ring") && reason.contains("self_intersecting"),
        "both planted defects are named, not just the first: {reason}",
    );
    assert!(
        reason.contains("ZERO"),
        "…and WHICH baseline refused it, so a green under one baseline can never be \
         read as a green under another: {reason}",
    );
    // The numbers travel with the refusal — one of each, against a budget of 0.
    for f in &g.findings {
        assert_eq!((f.counted, f.allowed), (1, 0), "{f}");
    }
    println!("green: {reason}");

    let _ = std::fs::remove_dir_all(&dir);
}

/// The clean twin: the same writer, the same tables, no planted defect — green at
/// ZERO. A gate that cannot pass is a gate somebody switches off.
#[test]
fn a_clean_cut_passes_the_gate_at_zero() {
    let dir = plant_cut("clean", false);
    let g = check_cut_geometry(&dir, &EXTRACT_TABLES, &GeometryBaseline::ZERO).unwrap();
    assert!(g.ok(), "a clean fixture cut must pass at ZERO: {g:#?}");
    assert!(g.reason().is_none());
    // …and it really looked: three geometry tables censused, books closed on each.
    // `relations.parquet` carries no geometry column and contributes none — correct.
    assert_eq!(g.census.len(), 3, "nodes + ways + relations.resolved: {g:#?}");
    for (name, q) in &g.census {
        assert_eq!(q.unaccounted(), 0, "books close for {name}");
    }
    let _ = std::fs::remove_dir_all(&dir);
}

/// **The recorded baseline tolerates exactly what was MEASURED and not one more.**
///
/// The honest threshold: `ways::geometry` on the live cut carries defects that came out
/// of CONVERT, and a gate that fails on today's real cut is a gate that gets switched
/// off. So the record holds them, with provenance, and the gate fails on anything ABOVE
/// the record — a defect budget that can only be spent down.
#[test]
fn the_recorded_baseline_tolerates_the_record_and_refuses_one_more() {
    let base = GeometryBaseline::RECORDED;
    assert!(
        base.provenance.contains("verify --geometry-quality"),
        "a record without its provenance is a magic number: {}",
        base.provenance,
    );
    let a = base.allowance("ways.parquet", "geometry");
    let (unclosed, selfx) = (
        a.unclosed_ring.recorded().expect("recorded"),
        a.self_intersecting.recorded().expect("recorded"),
    );
    assert_eq!(
        (unclosed, selfx),
        (99, 599),
        "the record is the census of 2026-09-02, not a round number",
    );
    assert_eq!(
        a.decode_fail,
        Budget::At(0),
        "a blob no reader can decode is NEVER budgeted",
    );

    // Exactly the record: green.
    let at_record = GeomQuality {
        column: "geometry".into(),
        rows: 1_000_000,
        polys: 1_000_000,
        unclosed_ring: unclosed,
        self_intersecting: selfx,
        ..Default::default()
    };
    let f = base.findings("ways.parquet", &at_record);
    assert!(f.is_empty(), "the recorded defects are the budget: {f:#?}");

    // One more of either: red, and the finding says by how much.
    let mut worse = at_record.clone();
    worse.self_intersecting += 1;
    let f = base.findings("ways.parquet", &worse);
    assert_eq!(f.len(), 1, "{f:#?}");
    assert_eq!(f[0].defect, "self_intersecting");
    assert_eq!((f[0].counted, f[0].allowed), (selfx + 1, selfx));

    // Fewer than the record: still green — the budget shrinks with the data, and a
    // re-cut that fixes the 99 must not have to touch this file to land.
    let mut better = at_record.clone();
    better.unclosed_ring = 0;
    assert!(base.findings("ways.parquet", &better).is_empty());

    // A column the record does not name gets NO budget — a new rung column that
    // arrives dirty is a NEW defect, which is what "no new defects" has to mean.
    let newcol = GeomQuality {
        column: "geometry_z15".into(),
        rows: 10,
        polys: 10,
        self_intersecting: 1,
        ..Default::default()
    };
    let f = base.findings("ways.parquet", &newcol);
    assert_eq!(f.len(), 1, "an unrecorded column is budgeted at ZERO: {f:#?}");

    // decode_fail is 0 in EVERY row of the record — no entry can quietly buy one.
    for a in base.allow {
        assert_eq!(a.decode_fail, Budget::At(0), "{}::{}", a.table, a.column);
    }
}

/// **An UNASSERTED count is reported, never silently passed.**
///
/// `relations.resolved::geometry`'s selfx was never written down by the V21 census, so
/// the record says `Unmeasured` rather than guessing 0. It must not fail a cut — and it
/// must not vanish either.
#[test]
fn an_unmeasured_column_is_reported_rather_than_passed_or_failed() {
    let base = GeometryBaseline::RECORDED;
    let a = base.allowance("relations.resolved.parquet", "geometry");
    assert_eq!(a.self_intersecting, Budget::Unmeasured);
    assert_eq!(a.self_intersecting.recorded(), None);

    let q = GeomQuality {
        column: "geometry".into(),
        rows: 100,
        polys: 100,
        self_intersecting: 7,
        ..Default::default()
    };
    assert!(
        base.findings("relations.resolved.parquet", &q).is_empty(),
        "an unmeasured count fails nothing",
    );
    let notes = base.notes("relations.resolved.parquet", &q);
    assert_eq!(notes.len(), 1, "…and is REPORTED: {notes:#?}");
    assert_eq!(notes[0].defect, "self_intersecting");
    assert_eq!(notes[0].counted, 7);
    assert!(notes[0].to_string().contains("UNASSERTED"), "{}", notes[0]);

    // Its unclosed/decode_fail ARE recorded at 0 — the census fails on those, and it
    // reported only `ways::geometry` red, so 0 there is a measurement.
    let mut open = q.clone();
    open.unclosed_ring = 1;
    let f = base.findings("relations.resolved.parquet", &open);
    assert_eq!(f.len(), 1, "{f:#?}");
    assert_eq!(f[0].defect, "unclosed_ring");
}

/// **Books that do not close fail under EVERY baseline.** A budget is a licence to
/// carry a known number of bad shapes; it is never a licence for a census that cannot
/// account for its own rows — that would let a gate report a small number because it
/// stopped counting.
#[test]
fn unbalanced_books_are_not_budgetable() {
    let q = GeomQuality {
        column: "geometry".into(),
        rows: 100,
        polys: 40, // 60 rows unaccounted for
        ..Default::default()
    };
    for base in [&GeometryBaseline::ZERO, &GeometryBaseline::RECORDED] {
        let f = base.findings("ways.parquet", &q);
        assert!(
            f.iter()
                .any(|x| x.defect == "unaccounted" && x.counted == 60),
            "baseline {} must refuse books that do not close: {f:#?}",
            base.name,
        );
    }
}