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,
);
}
}