osm-katana/tests/facett_ladder_fixture.rs
//! **The other half of facett's cross-repo taxonomy guard.**
//!
//! `facett_map::style::classify_with` / `classify_label` and this crate's
//! [`osm_katana::shared::class::RULES`] are two TEXTS of one taxonomy — an arm-for-arm
//! transcription, in two repos, with nothing enforcing that they stay in step. facett
//! checks itself against a CHECKED-IN copy of this crate's ladder
//! (`facett-map/osm-katana-ladder.txt`, produced by `examples/ladder_spelling.rs`), and
//! its own guard is `facett_map::style::osm_katana_ladder`.
//!
//! A copy can go stale, and facett cannot see into this repo. **This file is what makes
//! the stale side notice.** It pins the generator's output against a literal, so the one
//! edit that would strand facett's copy — a change to `RULES`, `RUNGS`,
//! `UNCLASSIFIED_MIN_ZOOM` or `TIER_CAMERAS` — turns a test red HERE, in the repo where
//! the edit happened, with the regeneration command in the failure message.
//!
//! # This is a change detector, and that is the whole job
//!
//! It is not a correctness check and it does not pretend to be one: it says nothing
//! about whether a rung is RIGHT. It says that the ladder moved and that a copy of it
//! exists elsewhere. The literal below is not derived from `rules_spelling()` at
//! runtime — that would be the identity-value trap LAW 2 names, a value compared with
//! itself through one function, and it could never go red.
//!
//! Run: `cargo test -p osm-katana --test facett_ladder_fixture`
use osm_katana::shared::class::{RULES, RUNGS, Tier, UNCLASSIFIED_MIN_ZOOM, rules_spelling};
/// The regeneration command, spelled once, quoted into every failure message.
const REGENERATE: &str = "cargo run -p osm-katana --example ladder_spelling \
> ../facett/facett-map/osm-katana-ladder.txt";
/// **The tier ladder facett holds a copy of.** A literal, one line per tier.
const TIERS: &[&str] = &[
"Country@0>6",
"Region@6>6",
"District@9>9",
"City@12>12",
"Detail@15>15",
];
/// **The rules facett holds a copy of.** A literal, one line per rule, in order.
const RULE_LINES: &[&str] = &[
// FIRST since `islet-1` (2026-08-31): `RULES` is order-sensitive and the `place`
// block ends in a catch-all, so a shoreline way tagged `place=islet` matched `place`
// and was written min_zoom 12 while facett paints coastline at 6 — 32 049 of 79 084
// coastline ways on the Sweden cut carry a `place` tag.
"natural∈[coastline]>6",
// The boundary block (2026-09-01): `admin_level` decides the rung, asked before the
// `boundary` key so a way segment tagged with the level alone lands with its relation.
"admin_level∈[1|2]>6",
"admin_level∈[3|4]>6",
"admin_level∈[5|6|7]>9",
"admin_level?>12",
"boundary∈[national_park|protected_area]>6",
"boundary?>12",
"place∈[country|state|province|region]>6",
"place∈[city]>6",
"place∈[town]>6",
"place∈[village]>9",
"place∈[municipality|borough]>9",
"place?>12",
"piste:type?!highway∈[motorway|motorway_link|trunk|trunk_link|primary|primary_link|secondary|secondary_link|tertiary|tertiary_link]>9",
"snowmobile?!highway∈[motorway|motorway_link|trunk|trunk_link|primary|primary_link|secondary|secondary_link|tertiary|tertiary_link]>12",
"route?!highway∈[motorway|motorway_link|trunk|trunk_link|primary|primary_link|secondary|secondary_link|tertiary|tertiary_link]>9",
"sac_scale?!highway∈[motorway|motorway_link|trunk|trunk_link|primary|primary_link|secondary|secondary_link|tertiary|tertiary_link]>9",
"trail_visibility?!highway∈[motorway|motorway_link|trunk|trunk_link|primary|primary_link|secondary|secondary_link|tertiary|tertiary_link]>9",
"mtb:scale?!highway∈[motorway|motorway_link|trunk|trunk_link|primary|primary_link|secondary|secondary_link|tertiary|tertiary_link]>12",
"highway∈[motorway|trunk|primary]>6",
"highway∈[motorway_link|trunk_link|primary_link|secondary]>6",
"highway∈[secondary_link|tertiary|tertiary_link]>9",
"highway∈[residential|unclassified|living_street]>12",
"highway∈[service|track]>15",
"highway∈[footway|cycleway|path|pedestrian|steps]>15",
"highway∈[road|busway|bus_guideway]>9",
"highway?>12",
"railway∈[rail|narrow_gauge]>6",
"railway∈[light_rail|subway|tram|funicular|preserved|miniature]>9",
"railway?>12",
"natural∈[water|lake|bay|wetland]>6",
"water?>6",
"waterway∈[river|riverbank|dock|basin]>6",
"waterway∈[canal]>9",
"waterway∈[ditch|drain]>15",
"waterway?>12",
"natural∈[wood|forest|scrub|grassland|heath]>6",
"leisure∈[park|garden|nature_reserve|recreation_ground]>6",
"landuse?>6",
"building?>12",
"amenity?>12",
];
/// ★ **RED-when-broken: the ladder moved and facett's copy did not.**
///
/// **RED ARM** (seen): moved `place=town` from `Region` to `District` in `RULES` ⇒
/// *"rule 3 is now `place∈[town]>9`, the copy facett holds says `place∈[town]>6`"*.
#[test]
fn the_ladder_facett_copied_is_still_this_ladder() {
assert_eq!(
RUNGS.to_vec(),
vec![6u8, 9, 12, 15],
"the RUNGS moved. facett projects its `Lod` onto this list; regenerate its copy:\n {REGENERATE}"
);
assert_eq!(
UNCLASSIFIED_MIN_ZOOM, 12,
"the unclassified rung moved. Regenerate facett's copy:\n {REGENERATE}"
);
let tiers: Vec<String> = Tier::ladder_spelling().split(',').map(str::to_string).collect();
assert_eq!(
tiers.len(),
TIERS.len(),
"the tier ladder has {} entries and facett's copy has {}. Regenerate it:\n {REGENERATE}",
tiers.len(),
TIERS.len()
);
for (i, (got, want)) in tiers.iter().zip(TIERS).enumerate() {
assert_eq!(
got, want,
"tier {i} is now `{got}`, the copy facett holds says `{want}`. Regenerate \
it:\n {REGENERATE}"
);
}
let rules: Vec<String> = rules_spelling().split(',').map(str::to_string).collect();
assert_eq!(
rules.len(),
RULE_LINES.len(),
"the ladder has {} rules and facett's copy has {}. A rule was added or removed; \
regenerate it:\n {REGENERATE}",
rules.len(),
RULE_LINES.len()
);
for (i, (got, want)) in rules.iter().zip(RULE_LINES).enumerate() {
assert_eq!(
got, want,
"rule {i} is now `{got}`, the copy facett holds says `{want}`. Regenerate \
it:\n {REGENERATE}"
);
}
}
/// ★ **RED-when-broken: a pin that pins nothing.**
///
/// The literals above are only worth anything if they are the whole ladder and if they
/// could disagree with it. Three arms: the pin covers every rule in `RULES`, the
/// spelling is non-trivial (a table of empty strings would compare equal to itself), and
/// a DELIBERATELY wrong expectation is detected — the arm that proves the comparison has
/// teeth without needing a source edit to see red.
#[test]
fn the_pin_could_actually_disagree() {
assert_eq!(
RULE_LINES.len(),
RULES.len(),
"the pin covers {} of {} rules — the uncovered ones can drift silently",
RULE_LINES.len(),
RULES.len()
);
assert!(
RULE_LINES.iter().all(|r| r.contains('>') && r.len() > 4),
"a rule spelling with no `>rung` is not a spelling; the pin would be comparing \
placeholders with placeholders"
);
let real: Vec<String> = rules_spelling().split(',').map(str::to_string).collect();
let mut wrong = RULE_LINES.to_vec();
wrong[0] = "place∈[country|state|province|region]>15";
assert_ne!(
real,
wrong.iter().map(|s| (*s).to_string()).collect::<Vec<String>>(),
"a deliberately wrong expectation compares EQUAL to the real spelling — the \
comparison has no teeth"
);
}