diff --git a/.claude/board/LATEST_STATE.md b/.claude/board/LATEST_STATE.md index 92831d9b..e94cb52f 100644 --- a/.claude/board/LATEST_STATE.md +++ b/.claude/board/LATEST_STATE.md @@ -1,5 +1,11 @@ # LATEST_STATE — What Just Shipped (read this FIRST) +## 2026-07-11 — branch `claude/medcare-ruff-codebook-handover-5ulx0i` — `ClassView::menu_address` — the runtime Klickwege-menu radix projection + +### Current Contract Inventory — new entry + +- **`class_view::ClassView::menu_address(class) -> Vec`** (NEW default trait method). The RUNTIME projection of the harvest-side ruff `nav_digest` `[menu-quad]` `loc=` field: walks [`is_a_parent`] root-first to lower a class's menu LOCATION into the existing classid ontology as a radix-trie path `[root, …, parent, class]` — the concept ontology **is** the radix trie; the menu address is a path through it, never a stored ordinal (V3 LE-contract §3). A renderer lays out the menu by prefix from the path alone, zero value decode. Same 16-hop cap + on-stack visited cycle guard as the sibling `resolve_render_class` (never loops/panics; only alloc is the returned path). Default method → every existing `ClassView` impl gains it for free. Tests: `menu_address_walks_is_a_root_first` (30 is_a 20 is_a 7 → `[7,20,30]`; root → `[7]`), `menu_address_cycle_terminates` (2-cycle + self-loop bounded). Completes the "digest-now-ClassView-after" plan (ruff #82 = digest lowering; this = runtime projection). No new type/module; pure additive trait surface. + ## 2026-07-10 — branch `claude/review-claude-board-files-nhqgx1` — `contract::style_family::StyleFamily` — M9 ThinkingStyle dedup shipped (D-TSC-1, first 5+3 council run) ### Current Contract Inventory — new entry diff --git a/crates/lance-graph-contract/src/class_view.rs b/crates/lance-graph-contract/src/class_view.rs index 70f3056e..c0a285f5 100644 --- a/crates/lance-graph-contract/src/class_view.rs +++ b/crates/lance-graph-contract/src/class_view.rs @@ -915,6 +915,48 @@ pub trait ClassView { } } + /// The **menu address** of `class` — the radix-trie path lowered from the + /// `is_a` rail, root-first: `[root_ancestor, …, parent, class]`. + /// + /// This is the runtime projection of the harvest-side `nav_digest` + /// `[menu-quad]` `loc=` field (ruff `ruff_spo_triplet::nav_digest`): the + /// existing classid ontology **is** the radix trie, and a node's menu + /// LOCATION is a path through it — walking [`is_a_parent`](ClassView::is_a_parent), + /// never a stored ordinal (V3 LE-contract §3 forbids a position slot). A + /// renderer lays out the menu by prefix from this path alone, with zero + /// value decode (OGAR "the key prerenders nodes"). A root class (no + /// `is_a_parent`) returns `[class]` — its own single-element address. + /// + /// **Cycle- and depth-safe, zero-dep:** the SAME 16-hop cap + on-stack + /// visited guard as [`resolve_render_class`](ClassView::resolve_render_class). + /// A `subClassOf` cycle terminates at the first repeat (never loops, never + /// panics); the only allocation is the returned path. + fn menu_address(&self, class: ClassId) -> Vec { + const MAX_HOPS: usize = 16; + let mut path: Vec = Vec::new(); + let mut visited: [ClassId; MAX_HOPS] = [class; MAX_HOPS]; + let mut current = class; + let mut depth = 0usize; + loop { + path.push(current); + if depth >= MAX_HOPS { + break; // depth cap → truncated (still root-first, monotonic) + } + visited[depth] = current; + depth += 1; + let parent = match self.is_a_parent(current) { + Some(p) => p, + None => break, // taxonomy root + }; + if visited[..depth].contains(&parent) { + break; // subClassOf cycle → stop at the first repeat + } + current = parent; + } + path.reverse(); // root-first: the radix-trie address + path + } + /// Which edge-codec flavor this class reads its node edge block with. /// /// Default is @@ -1596,6 +1638,34 @@ mod tests { assert_eq!(classes.resolve_render_class(7), 7); } + /// `menu_address`: the radix-trie path lowered from the `is_a` rail, + /// root-first — the runtime twin of the nav_digest `[menu-quad]` `loc=`. + #[test] + fn menu_address_walks_is_a_root_first() { + // 30 is_a 20 is_a 7 → the menu address is [7, 20, 30] (root-first). + let classes = FakeClasses::new().with_isa(30, 20).with_isa(20, 7); + assert_eq!( + classes.menu_address(30), + vec![7, 20, 30], + "the address is the is_a ancestor chain, broadest-first" + ); + // A root class (no is_a parent) is its own single-element address. + assert_eq!(classes.menu_address(7), vec![7]); + } + + /// A `subClassOf` cycle terminates: the path is the bounded chain up to the + /// first repeat, never an infinite loop (same guard as the render ladder). + #[test] + fn menu_address_cycle_terminates() { + let classes = FakeClasses::new().with_isa(40, 41).with_isa(41, 40); + let addr = classes.menu_address(40); + // 40 → 41 → (41's parent is 40, already visited) stop: path = [41, 40]. + assert_eq!(addr, vec![41, 40], "cycle stops at the first repeat"); + // A self-loop likewise terminates with a single element. + let selfloop = FakeClasses::new().with_isa(50, 50); + assert_eq!(selfloop.menu_address(50), vec![50]); + } + /// An orphan (no is_a parent, no fields) resolves to ITSELF — the monotonic /// bottom rung: the caller renders the generic facet dump. #[test]