From d8e9b47f28267c08a8013d33fbf936e2eb00c450 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 10 Aug 2026 22:13:49 +0000 Subject: [PATCH 1/4] plan(osm): record the OGAR hot-plug surface and what it means for q2 The OSM substrate is already plugged into OGAR USB-style: socket in lance_graph_contract::hotplug, authority in ogar_vocab::geo_actions, bridge via ogar_osm::plug_in, and the device is osm-soa-bake::capability with a real dispatch arm per capability. Six capabilities are declared on osm_node/osm_way, and two of them are things this endpoint already touches: locate_tile IS slab.tile_range, and project_fields is the masked projection. Three consequences, recorded so a future session does not re-derive them: q2 must not declare its own HOT_PLUG. GEO_EXPECTED_EXECUTORS is ["osm-soa-bake"] and resolve_hotplug checks the consumer name, so a plug from cockpit-server earns UnexpectedConsumer by design. q2 calls an already-activated device; it is not a second device. Phase 1 needs no rework. Calling slab.tile_range directly is explicitly sanctioned by capability.rs's own module doc -- the arms exist to make the registration falsifiable, not to be a mandatory call path. project_fields is a real gap, and it is the authorization one. The endpoint returns decoded values with no mask; the declared capability is surface-intersect-role and fail-closed, meaning an unauthorised position is absent from the response rather than hidden by the caller. Moot for a local cockpit, not moot for a deployment. Filed as a Phase 4 addendum with the honest caveat that it needs a role source first -- until then the endpoint should be described as unauthenticated rather than implying a mask exists. --- .../2026-08-08-osm-soa-cockpit-wiring.md | 68 +++++++++++++++++++ 1 file changed, 68 insertions(+) diff --git a/claude-notes/plans/2026-08-08-osm-soa-cockpit-wiring.md b/claude-notes/plans/2026-08-08-osm-soa-cockpit-wiring.md index c97bce8f84..330d1817e7 100644 --- a/claude-notes/plans/2026-08-08-osm-soa-cockpit-wiring.md +++ b/claude-notes/plans/2026-08-08-osm-soa-cockpit-wiring.md @@ -358,6 +358,74 @@ because it means the slab was *never* V1; only q2's display key is. (`RAILWAY_VOL` is set, the mount is Railway-side only), so this leg is verifiable only on deploy — say so rather than claiming it works. +## The OGAR plug-and-play surface (operator pointer, 2026-08-10) + +**The OSM substrate is already hot-plugged into OGAR, USB-style.** Worth +recording because it changes Phase 4's design and it *validates* Phase 1's +call style rather than condemning it. + +| USB role | Home | Surface | +|---|---|---| +| socket (agnostic, zero-dep) | `lance_graph_contract::hotplug` | `HotPlug { consumer, classids, covered }`, `CapabilityAuthority` | +| authority (host) | `ogar_vocab::geo_actions` | the geo action table + `capability_registry::resolve_hotplug` | +| bridge | `ogar_osm::plug_in` | socket ⇄ authority | +| **device** | `osm-soa-bake::capability` | `Capability` enum, `activate()`, one real dispatch arm each | + +Declared capabilities, and what each already is in code: + +| capability | subject | primitive | +|---|---|---| +| `locate_point` | `osm_node` `0x0F01` | `tms::point_to_tiers` | +| **`locate_tile`** | `osm_node` | **`slab.tile_range(z,x,y)` — what `/api/osm/features/:z/:x/:y` calls** | +| `locate_fragment` | `osm_node` | `BoundaryIndex::locate_range` | +| **`project_fields`** | `osm_node` | **`project::project(row, surface, role)` — the RBAC projection** | +| `street_edges` | `osm_way` `0x0F02` | `street::edge_mask` | +| `polyline_length` | `osm_way` | `geodesy::polyline_metres` | + +### Three consequences for q2 — read before touching the endpoint + +1. **q2 must NOT declare its own `HOT_PLUG`.** + `GEO_EXPECTED_EXECUTORS = ["osm-soa-bake"]`, and `resolve_hotplug` + checks the consumer name — a plug from `cockpit-server` earns + `HotplugDrift::UnexpectedConsumer`, by design. **q2 is a caller of an + already-activated device, not a second device.** The plug is the bake + crate's, and it is already shipped (`capability.rs`, with can-fire + halves proving the port rejects a wrong consumer and short coverage). + +2. **Calling `slab.tile_range(...)` directly is explicitly sanctioned**, so + Phase 1 needs no rework. `capability.rs` says so in its own module doc: + *"These are thin, on purpose … the dispatch surface exists so the + registration is checkable, not to add a layer: a caller that already has + the module in scope should keep calling `tms::point_to_tiers` directly."* + The arms exist to make the registration falsifiable, not to be a + mandatory call path. + +3. **`project_fields` is a real gap in the endpoint, and it is the + authorization one.** `/api/osm/features` currently decodes and returns + `lon/lat/entity_type/ordinal` with no mask applied. The declared + capability is `surface ∩ role` and is **fail-closed** — an unauthorised + position is *absent from the response*, not hidden client-side (the same + projection doctrine a2ui-rs enforces: RBAC happens before framing, and + pixels/JSON can't promise what the wire already leaked). For a + single-user local cockpit that is moot; for anything deployed it is the + difference between a demo and a surface with an access model. + +- [ ] Phase 4 addendum: route the feature response through + `project_fields(row, surface, role)` rather than hand-decoding, so + the endpoint inherits the fail-closed projection instead of + re-implementing (or silently skipping) it. Needs a role source — + until there is one, say plainly that the endpoint is unauthenticated + rather than implying a mask exists. + +**Anti-patterns this pins** (each burned upstream once, per +`OGAR/.claude/knowledge/hotplug-consumer-migration.md`): no bespoke +per-consumer plug mechanism; no shape ordinals in the classid low u16 (low += APP render prefix); no git deps on OGAR (path deps to the sibling — a +git+branch dep writes a rev pin into `Cargo.lock`); no parallel registry +beside `domain_tables()` — `ogar-osm` once shipped its own +`OSM_CAPABILITIES` that verified itself against itself and passed while the +real port answered `NoCapabilitiesFor(0x0F01)`. + ## Notes - Disk: the 1.2 GiB Berlin slab is a local dev artifact under `/tmp`, not From a3fbc9f03246723a671c5c96dc0c85c4152c12d3 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 10 Aug 2026 22:44:37 +0000 Subject: [PATCH 2/4] osm cockpit: migrate the HHTL key from V1 (3-tier z=24) to the V3 substrate The cockpit's displayed address and the key RowSlab sorts rows by are now the same key. They were not before: osm_tiles carried its own 3-tier, z=24, un-flipped Morton key while the slab is keyed 4-tier, z=32, Cesium-TMS-flipped. Every tier differed at every point tested -- Berlin HEEL 0x624b vs 0xc8e1 -- so this was a different key, not a coarser one. Done as a real TDD loop, since the environment can now link the test binary (dropping debuginfo was what unblocked it): RED hhtl_agrees_with_the_v3_substrate_oracle failed 25163 vs 51425 GREEN 14/14 osm tests after the migration The test asserts equality against osm_soa_bake::tms::point_to_tiers -- the implementation the slab is actually keyed on -- rather than asserting the key "has four tiers", which would pass against a wrong key. Implementation is mostly deletion. The local Mercator formula, the 24-bit Morton interleave/deinterleave and the depth constant are now delegations to osm_soa_bake::tms. Two implementations of one projection is precisely how the display address and the row key drifted apart, so the duplicate is gone rather than widened. Two subtleties worth keeping: - The TMS flip must happen at the tile's own zoom and THEN be left-aligned. xyz_to_tms_y is 2^z-1-y, so flipping at z and shifting by 32-z yields the minimum TMS row of the tile's z=32 range, which is the common Morton prefix. Shifting first and flipping at 32 picks the opposite corner and breaks the prefix property. - Reading the bytes LEAF occupies as a tier, rather than as the head of the old family:u24, IS the V3 content-blind reinterpretation. TEST CHANGES -- two pre-existing tests encoded the V1 depth and were re-pinned, not silently widened: - hhtl_roundtrips_to_the_tile: z=24/3-tier/48-bit -> z=32/4-tier/64-bit, and it now un-flips the recovered row before comparing. It also asserts the stored row is genuinely flipped, so the test fails if tile_to_hhtl ever stops applying the flip. - over_depth_zoom_folds_to_its_native_ancestor: over-depth begins at 33, not 25. It now also asserts z=25 is NOT folded, which the V1 form did. - coarse_zoom_lives_in_heel_not_twig: strengthened to cover LEAF. Panel gains a fourth tier cell; the overlay JS was re-checked with node --check. The key-space warning in osm_features.rs was rewritten rather than deleted -- it records why the two keys once differed, which is the reason the equality test exists. Verification: 14/14 osm tests green; 79/80 for the whole crate. The single failure, osint_gotham::dual_use_facets_pack_into_the_value_tenant, was confirmed pre-existing by stashing this change and re-running it on main's state -- it fails identically there and touches nothing in this diff. --- crates/cockpit-server/src/osm.rs | 20 ++- crates/cockpit-server/src/osm_features.rs | 25 ++- crates/cockpit-server/src/osm_tiles.rs | 210 +++++++++++++++------- 3 files changed, 169 insertions(+), 86 deletions(-) diff --git a/crates/cockpit-server/src/osm.rs b/crates/cockpit-server/src/osm.rs index 901f24a98d..e58ee90ba1 100644 --- a/crates/cockpit-server/src/osm.rs +++ b/crates/cockpit-server/src/osm.rs @@ -1,7 +1,7 @@ //! The `/OSM` cockpit — a self-contained slippy-map page over the OSM tile //! material ([`crate::osm_tiles`]). The Geo-domain (`0x0F`) sibling of the FMA //! body-helix cockpit: pan/zoom OSM raster tiles from the standard source, and -//! read each tile's HHTL (HEEL/HIP/TWIG) key live from `/api/osm/locate`. +//! read each tile's HHTL (HEEL/HIP/TWIG/LEAF) key live from `/api/osm/locate`. //! //! The page is a single inline HTML string (the `/mri` cockpit pattern): no //! build step, no external JS. Tiles are `` fetched directly from @@ -36,10 +36,10 @@ const PAGE: &str = r##" .row { display:flex; justify-content:space-between; padding:3px 0; border-bottom:1px solid #161d27; } .k { color:#6b7c93; } .v { color:#e6edf5; } - .tier { display:grid; grid-template-columns:repeat(3,1fr); gap:8px; margin:12px 0; } + .tier { display:grid; grid-template-columns:repeat(4,1fr); gap:6px; margin:12px 0; } .cell { background:#141b24; border:1px solid #223; border-radius:6px; padding:8px; text-align:center; } - .cell b { display:block; font-size:16px; color:#8fd6ff; } + .cell b { display:block; font-size:13px; color:#8fd6ff; } .cell span { color:#6b7c93; font-size:11px; } .ctl { position:absolute; left:12px; top:12px; display:flex; flex-direction:column; gap:6px; z-index:5; } @@ -77,12 +77,14 @@ const PAGE: &str = r##"
—HEEL
—HIP
—TWIG
+
—LEAF
tile source
—

A quadtree is a cascade: - z/x/y Morton-interleaves into the three 16-bit HHTL tiers, so - the map pyramid and the semantic address are one and the same.

+ z/x/y Morton-interleaves into the four 16-bit HHTL tiers + (GEO_V3_FACET rails 0–3), so the map pyramid and the slab's own + row key are one and the same address.