From 7c986da0fbb352b6de8e9cd6e2246f25d7dc3573 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 13 Aug 2026 13:47:02 +0000 Subject: [PATCH 1/2] ogar-vocab: mint osm_street_node (0x0F0B), the first SYNTHESIZED Geo concept MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A junction in the routable-way graph earns its own concept id. Not because the Geo domain was short of ids, but because the substrate has no other way to say which reading a row's value slots carry. Junction rows exist today (Berlin 233,053; Brandenburg 590,437) and bake as plain `osm_node`. That is the problem: nothing on disk distinguishes "a junction using its edge slots" from "a tagged node using its first tag slot". osm-soa-bake's street.rs documents what that costs — an earlier draft placed the edge-name lane at value-local slot 2 and measured 1,084,213 FALSE HITS on real Berlin data, one per tagged node, each reinterpreting a node's first tag bytes as eight raw name ordinals. It survives today only by hiding at slot 1, the last gap the tag path never writes. Adjacency, bearing, turn matrix and speed need several more slots and there is no second gap. A distinct classid makes the layout class-resolved, which is the canon's own answer ("Tetris it across the slots"), and retires the workaround instead of stacking on it. ── The invariant this breaks, deliberately ── `OSM_CONCEPTS` asserted every Geo concept has a Rails source class. That held while the domain was a mirror of OSM's schema. It stopped holding when the bake began SYNTHESIZING junctions: OSM has no junction element, so there is nothing to ground against, and inventing a Rails name to keep the assertion green would put a falsehood in the Core. So the exemption is an explicit `SYNTHESIZED` list, not a relaxed assertion — membership is a reviewable claim that OSM genuinely has no source element. A paired check asserts every exempted name IS a concept, so the exemption cannot silently cover a typo and re-open the hole it was cut for. ── Cross-repo, because the fuses said so ── Adding a concept moved class_ids::ALL 90 -> 91 and tripped four drift guards, one of which names its own remedy: "update this pin AND the lance-graph mirror COUNT_FUSE in the same PR". Both pins, all_promoted_classes(), and the lance_graph_contract::ogar_codebook mirror move together. Verified: ogar-osm 18/18, ogar-vocab 155/155, and lance-graph-ogar compiles — its COUNT_FUSE is a compile-time assert, so a half-done mint fails the BUILD, not just tests. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01NMeiLmtDKhomJNSo2ecbJw --- crates/ogar-osm/src/lib.rs | 43 +++++++++++++++++-- crates/ogar-vocab/src/capability_registry.rs | 2 +- crates/ogar-vocab/src/lib.rs | 45 +++++++++++++++++++- 3 files changed, 84 insertions(+), 6 deletions(-) diff --git a/crates/ogar-osm/src/lib.rs b/crates/ogar-osm/src/lib.rs index 33413d95..e7f8a2f3 100644 --- a/crates/ogar-osm/src/lib.rs +++ b/crates/ogar-osm/src/lib.rs @@ -92,13 +92,34 @@ pub const fn classid(concept: u16, classview: u16) -> u32 { render_classid(classview, concept) } +/// Concepts DERIVED by a bake rather than mirrored from a Rails class. +/// +/// The Geo domain began as a mirror of OSM's own schema, so "every concept +/// grounds to a Rails class" held by construction. It stopped holding when the +/// bake started synthesizing junctions from the routable-way graph: OSM has no +/// junction element, and inventing a Rails name to satisfy the check would put +/// a falsehood in the Core to keep a test green. +/// +/// Membership is therefore an explicit, reviewable list — not a relaxed +/// assertion. Adding a name here is a claim that OSM genuinely has no source +/// element for it. +pub const SYNTHESIZED: &[&str] = &["osm_street_node"]; + /// Every Geo concept, in codebook order, with its Rails source class(es). /// /// The `Old*` versioned Rails classes ground to their base concept — the /// temporal axis is a Lance version, not a new id — which is why `Node` and /// `OldNode` share `0x0F01`. +/// +/// One entry has NO Rails source: see [`SYNTHESIZED`]. pub const OSM_CONCEPTS: &[(&str, u16, &[&str])] = &[ ("osm_node", class_ids::OSM_NODE, &["Node", "OldNode"]), + // The one DERIVED concept: OSM has no junction element, so there is no + // Rails class to ground against. The bake synthesizes it from the + // routable-way graph. Its rails list is deliberately empty — see + // `tests::every_concept_sits_in_the_geo_domain`, which allows exactly + // this concept to have none rather than accepting a fabricated source. + ("osm_street_node", class_ids::OSM_STREET_NODE, &[]), ("osm_way", class_ids::OSM_WAY, &["Way", "OldWay"]), ( "osm_relation", @@ -556,15 +577,29 @@ mod tests { GEO_DOMAIN, "{name} ({id:#06x}) is outside the Geo domain" ); - assert!(!rails.is_empty(), "{name} has no Rails source class"); + // Empty rails is allowed for EXACTLY the concepts OSM does not + // have an element for — a derived concept has nothing to ground + // against, and a fabricated source class would be worse than an + // honest absence. Anything else with no rails is a mistake. + if !SYNTHESIZED.contains(name) { + assert!(!rails.is_empty(), "{name} has no Rails source class"); + } + } + // Every synthesized name must actually BE a concept — otherwise the + // exemption above silently covers a typo and re-opens the hole. + for s in SYNTHESIZED { + assert!( + OSM_CONCEPTS.iter().any(|(n, _, _)| n == s), + "{s} is exempted but is not a concept" + ); } - // Ten concepts, and the ids are distinct — a copy-paste that reused + // Eleven concepts, and the ids are distinct — a copy-paste that reused // one constant twice would pass a domain check but fail here. - assert_eq!(OSM_CONCEPTS.len(), 10); + assert_eq!(OSM_CONCEPTS.len(), 11); let mut ids: Vec = OSM_CONCEPTS.iter().map(|(_, id, _)| *id).collect(); ids.sort_unstable(); ids.dedup(); - assert_eq!(ids.len(), 10, "two concepts share an id"); + assert_eq!(ids.len(), 11, "two concepts share an id"); } #[test] diff --git a/crates/ogar-vocab/src/capability_registry.rs b/crates/ogar-vocab/src/capability_registry.rs index 44df86bf..3dff13fa 100644 --- a/crates/ogar-vocab/src/capability_registry.rs +++ b/crates/ogar-vocab/src/capability_registry.rs @@ -372,7 +372,7 @@ fn resolve_concept_row(id: u16) -> Option<(&'static str, u16)> { mod the_canon_carries_no_palette_rows { #[test] fn no_0x17xx_row_reached_the_globally_mirrored_codebook() { - assert_eq!(crate::class_ids::ALL.len(), 90); + assert_eq!(crate::class_ids::ALL.len(), 91); for (_, id) in crate::class_ids::ALL { assert_ne!(*id >> 8, 0x17, "a 0x17XX row reached the codebook"); } diff --git a/crates/ogar-vocab/src/lib.rs b/crates/ogar-vocab/src/lib.rs index d83b2c9e..cdc8f54f 100644 --- a/crates/ogar-vocab/src/lib.rs +++ b/crates/ogar-vocab/src/lib.rs @@ -1381,6 +1381,14 @@ const CODEBOOK: &[(&str, u16)] = &[ ("osm_note", 0x0F08), ("osm_gpx_trace", 0x0F09), ("osm_user", 0x0F0A), + // SYNTHESIZED — the one Geo concept with no Rails source. OSM has no + // "junction" element: a street node is DERIVED by the bake from the + // routable-way graph (Berlin 619,470 junction nodes → 233,053 rows; + // Brandenburg 590,437). It earns its own id because its VALUE SLOTS mean + // something different from an `osm_node`'s — per-edge names, adjacency, + // bearing, and a turn matrix, resolved by ClassView — and there is + // otherwise no byte on disk distinguishing the two readings. + ("osm_street_node", 0x0F0B), ]; /// Codebook **domain** — the high byte of a canonical id (see @@ -1985,6 +1993,20 @@ pub mod class_ids { pub const OSM_GPX_TRACE: u16 = 0x0F09; /// `osm_user` (`0x0F0A`) — a mapper account. Rails `User`. pub const OSM_USER: u16 = 0x0F0A; + /// `osm_street_node` (`0x0F0B`) — a junction in the routable-way graph. + /// + /// **No Rails source, by nature**: OSM has no junction element. This is + /// DERIVED by the bake wherever routable ways meet, and it exists as its + /// own concept because its value slots carry a different schema from an + /// ordinary `osm_node` — per-edge street names, adjacency, bearing and a + /// turn matrix, all resolved through its ClassView. + /// + /// Without a distinct classid there is no byte on disk separating "a + /// junction row using its edge slots" from "a tagged node using its first + /// tag slot" — a reader that guessed measured **1,084,213 false hits** on + /// real Berlin data (see `osm-soa-bake`'s `street.rs`). The id IS the + /// disambiguator. + pub const OSM_STREET_NODE: u16 = 0x0F0B; // ── 0x07XX — OSINT domain: no concept constants (low byte = APPID, // domain-wise; q2 = 0x01 → `0x0701` is OSINT-for-q2, not a concept — @@ -2096,6 +2118,7 @@ pub mod class_ids { ("osm_note", OSM_NOTE), ("osm_gpx_trace", OSM_GPX_TRACE), ("osm_user", OSM_USER), + ("osm_street_node", OSM_STREET_NODE), ]; #[cfg(test)] @@ -2159,7 +2182,7 @@ pub mod class_ids { // lance-graph mirror is rebuilt against it. assert_eq!( ALL.len(), - 90, + 91, "class_ids::ALL count changed — update this pin AND the \ lance-graph mirror COUNT_FUSE (crates/lance-graph-ogar/src/lib.rs) \ in the same PR", @@ -3045,6 +3068,7 @@ pub fn all_promoted_classes() -> Vec { osm_note(), osm_gpx_trace(), osm_user(), + osm_street_node(), ] } @@ -4761,6 +4785,25 @@ pub fn osm_user() -> Class { c } +/// The `osm_street_node` (`0x0F0B`) — a junction in the routable-way graph. +/// +/// The one Geo class with **no Rails source**: OSM has no junction element, so +/// `Class::new` names the derived concept rather than a mirrored table. It is +/// synthesized by the bake wherever routable ways meet, and it exists as its +/// own class because its value slots carry a schema an `osm_node`'s do not — +/// per-edge street names, adjacency, bearing and a turn matrix. +/// +/// Its association is to `Way`, not to `Node`: a junction is defined BY the +/// ways that meet at it, which is also how the bake derives it. +#[must_use] +pub fn osm_street_node() -> Class { + let mut c = Class::new("StreetNode"); + c.language = Language::Unknown; + c.canonical_concept = Some("osm_street_node".to_string()); + c.associations = vec![family_edge("ways", "Way")]; + c +} + // ── 0x0CXX — Automation domain builders (HIRO IT-automation stack) ── // The MARS structural CMDB (A→R→S→M `dependsOn` backbone) + the Automation // DO-arm actuators. Shapes grounded in the vendored OGIT TTL attributes From 2299e3dc9f8ed54fb5a6375ee68aa566fb3f8612 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 14 Aug 2026 11:13:21 +0000 Subject: [PATCH 2/2] ogar-class-view: register osm_street_node, the ClassView the mint promised MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CI caught the mint landing half-done: `osm_street_node` (0x0F0B) went into `class_ids::ALL` without a matching `OgarClassView` registry entry, so both paired invariants failed — every_codebook_id_appears_in_class_ids_all osm_street_node (0x0f0b) in class_ids::ALL but missing from registry known_class_ids_iterates_in_stable_codebook_order known_class_ids drifted from codebook order (missing 3851) which is exactly what that test's own comment says it exists to catch: "a CODEBOOK promotion landing in OGAR without being added here." The failure is pointed, given what the mint is FOR. The whole argument for 0x0F0B is that a junction's value slots need a class-resolved reading rather than a lucky gap in the tag path — and the reading IS the ClassView. An id in the codebook with no registry entry is an address that resolves to nothing: the precise defect the mint was meant to remove, reintroduced one layer up. `osm_street_node()` already existed in ogar-vocab; only the lift was missing. Added to the import list and to the registry after `osm_user`, matching the codebook order the second test asserts. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01NMeiLmtDKhomJNSo2ecbJw --- crates/ogar-class-view/src/lib.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/crates/ogar-class-view/src/lib.rs b/crates/ogar-class-view/src/lib.rs index 077a68a9..20061af7 100644 --- a/crates/ogar-class-view/src/lib.rs +++ b/crates/ogar-class-view/src/lib.rs @@ -112,6 +112,7 @@ use ogar_vocab::{ osm_note, osm_relation, osm_relation_member, + osm_street_node, osm_user, osm_way, osm_way_node, @@ -266,6 +267,7 @@ fn all_canonical_classes() -> Vec<(&'static str, Class)> { ("osm_note", osm_note()), ("osm_gpx_trace", osm_gpx_trace()), ("osm_user", osm_user()), + ("osm_street_node", osm_street_node()), ] }