From 578b79bba627538d60dea3f6ca43662ee157a794 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 7 Aug 2026 13:11:58 +0000 Subject: [PATCH 1/2] ogar-vocab: the codebook is TRIGGERED by plug-and-play, not minted MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Builds the activation path I had been calling "not built yet" as though it were an external constraint. It was mine. The mechanism: `resolve_hotplug` resolves a plugged classid against `class_ids::ALL` FIRST, then against whatever a compiled-in feature ACTIVATED. Canon wins — a feature can ADD a concept the global codebook lacks; it can never SHADOW one it has. `ogar-vocab` gains a `blocks` feature, off by default, carrying the Blocks palette concept (0x1717) + its capability table. `ogar-blockly` deps `ogar-vocab` with `features = ["blocks"]`, so activation is CARGO PRESENCE, not runtime detection — the same rule lance-graph-ogar documents. A consumer that never touches a block editor never compiles the module and sees 0x17XX exactly as before: a reserved domain, zero concepts. Critically, `class_ids::ALL` is UNCHANGED at 90. That surface is mirrored into lance-graph under a compile-time count fuse, so minting there is by construction a lance-graph change — which is how the earlier attempt turned lance-graph red. The activated rows live beside it and are consulted alongside it, never merged in. One source of truth for the id: `BLOCK_PALETTE` is declared here and READ by `ogar_blockly::BlockConcept::Palette`. The reverse is impossible (ogar-blockly deps ogar-vocab), and two constants for one id is exactly the drift the join exists to catch. A real bug the tests caught, worth recording: `derive_action_rows` resolved `object_class` through the CODEBOOK only, so the concept resolved as a plugged classid (plug accepted) while its capabilities silently fell into the slag ledger — the port answered `NoCapabilitiesFor(0x1717)` for a table sitting right there. BOTH halves of the join must consult the same set; `activated_concept_id` is the by-name half of `resolve_concept_row`'s by-id lookup. Proven both ways in a SEPARATE crate outside this workspace, so feature unification could not fake it: feature OFF -> Err(UnknownClassid(5911)) zero 0x17XX rows feature ON -> resolves, all 5 capabilities Plus tests asserting 0x1717 is absent from class_ids::ALL (or "it resolved" proves nothing about WHERE), that plugging 0x1701/0x1702 does NOT resolve (they are the substrate's), and the three drift arms. Workspace: 80 test binaries, 0 failures. clippy -D warnings clean with the feature on. fmt scoped with -p, never --all. --- crates/ogar-blockly/Cargo.toml | 5 +- crates/ogar-blockly/src/lib.rs | 16 +- crates/ogar-encryption/src/lib.rs | 2 +- crates/ogar-obo/examples/bake_obo.rs | 60 ++++-- crates/ogar-obo/src/crosswalk.rs | 54 +++-- crates/ogar-obo/src/lib.rs | 142 ++++++++----- crates/ogar-obo/src/reason.rs | 38 +++- crates/ogar-render-askama/src/field_view.rs | 13 +- crates/ogar-vocab/Cargo.toml | 10 + crates/ogar-vocab/src/blocks_actions.rs | 208 +++++++++++++++++++ crates/ogar-vocab/src/capability_registry.rs | 80 ++++++- crates/ogar-vocab/src/lib.rs | 10 +- 12 files changed, 536 insertions(+), 102 deletions(-) create mode 100644 crates/ogar-vocab/src/blocks_actions.rs diff --git a/crates/ogar-blockly/Cargo.toml b/crates/ogar-blockly/Cargo.toml index 35dc9bc..3d503a6 100644 --- a/crates/ogar-blockly/Cargo.toml +++ b/crates/ogar-blockly/Cargo.toml @@ -14,5 +14,8 @@ serde = ["dep:serde", "ogar-vocab/serde", "ogar-loco/serde"] [dependencies] ogar-loco = { path = "../ogar-loco" } -ogar-vocab = { path = "../ogar-vocab" } +# `features = ["blocks"]` IS the activation: any build graph containing this +# crate activates the Blocks codebook in ogar-vocab, and no other build does. +# Cargo presence, not runtime detection. +ogar-vocab = { path = "../ogar-vocab", features = ["blocks"] } serde = { workspace = true, optional = true } diff --git a/crates/ogar-blockly/src/lib.rs b/crates/ogar-blockly/src/lib.rs index 94f822d..935dd9f 100644 --- a/crates/ogar-blockly/src/lib.rs +++ b/crates/ogar-blockly/src/lib.rs @@ -166,12 +166,22 @@ impl BlockConcept { /// This concept's canonical id inside the `0x17XX` Blocks domain. /// - /// Authoritative HERE; `ogar_vocab`'s shared CODEBOOK deliberately carries - /// zero `0x17XX` rows (plug-and-play, mirroring `ogar_obo::Namespace`). + /// **Read from `ogar_vocab`, never re-declared here.** The id is declared + /// once in [`ogar_vocab::blocks_actions::BLOCK_PALETTE`] — the crate that + /// also declares the capability table keyed by it — because this crate + /// depends on `ogar-vocab` and the reverse is impossible. Two constants + /// for one id is exactly the drift the classid join exists to catch. + /// + /// That row is **activated, not canon**: it lives behind `ogar-vocab`'s + /// `blocks` feature, which this crate's dependency turns on, so it exists + /// only in a build graph that actually contains a block editor. The shared + /// `class_ids::ALL` keeps zero `0x17XX` rows — that surface is mirrored + /// into lance-graph under a compile-time fuse, and a frontend's palette is + /// not lance-graph's concern. #[must_use] pub const fn concept_id(self) -> u16 { match self { - BlockConcept::Palette => 0x1717, + BlockConcept::Palette => ogar_vocab::blocks_actions::BLOCK_PALETTE, } } diff --git a/crates/ogar-encryption/src/lib.rs b/crates/ogar-encryption/src/lib.rs index 41a4c9f..ebebb1e 100644 --- a/crates/ogar-encryption/src/lib.rs +++ b/crates/ogar-encryption/src/lib.rs @@ -62,7 +62,7 @@ pub use encryption::{aead, envelope, hash, kdf, sign}; // ── Root-level convenience aliases, mirrored from `encryption`'s own root // re-exports (`envelope::{seal, open}` plus the envelope's error/parameter // types), so callers that used the upstream crate's short paths keep them. -pub use encryption::{open, seal, EnvelopeError, KdfParams}; +pub use encryption::{EnvelopeError, KdfParams, open, seal}; // ── The platform-CSPRNG-unavailable error, mirrored from `encryption`'s // crate root. diff --git a/crates/ogar-obo/examples/bake_obo.rs b/crates/ogar-obo/examples/bake_obo.rs index 1411c7f..d2d13ec 100644 --- a/crates/ogar-obo/examples/bake_obo.rs +++ b/crates/ogar-obo/examples/bake_obo.rs @@ -9,8 +9,8 @@ //! scratch dir, never committed. use ogar_obo::{ - Namespace, bake, merge_logical_defs, parse_hp_logical_defs, parse_obo, reason, - rows_from_le_bytes, as_le_bytes, + Namespace, as_le_bytes, bake, merge_logical_defs, parse_hp_logical_defs, parse_obo, reason, + rows_from_le_bytes, }; use std::collections::HashMap; @@ -22,12 +22,13 @@ fn main() { let mut nodes = HashMap::new(); for f in ["mondo", "hp", "uberon", "pato", "ro"] { let path = format!("{dir}/{f}.obo"); - let text = std::fs::read_to_string(&path) - .unwrap_or_else(|e| panic!("read {path}: {e}")); + let text = std::fs::read_to_string(&path).unwrap_or_else(|e| panic!("read {path}: {e}")); let part = parse_obo(&text); let (terms, edges) = ( part.len(), - part.values().map(|n| n.is_a.len() + n.rel.len() + n.xref.len()).sum::(), + part.values() + .map(|n| n.is_a.len() + n.rel.len() + n.xref.len()) + .sum::(), ); println!(" parsed {f:8} terms={terms:6} edges+xref={edges}"); for (id, n) in part { @@ -42,7 +43,10 @@ fn main() { // HPO logical-definition grounding (anatomy:quality) from hp-base.owl. let owl = std::fs::read_to_string(format!("{dir}/hp-base.owl")).unwrap_or_default(); let defs = parse_hp_logical_defs(&owl); - println!(" hp-base logical defs: {} HP->Uberon/PATO grounding edges", defs.len()); + println!( + " hp-base logical defs: {} HP->Uberon/PATO grounding edges", + defs.len() + ); merge_logical_defs(&mut nodes, &defs); let baked = bake(&nodes, 0x0000); @@ -56,36 +60,62 @@ fn main() { println!(" MONDO->HP resolve : {}", s.mondo_hp); println!(" HP->UBERON resolve : {}", s.hp_uberon); println!(" HP->PATO resolve : {}", s.hp_pato); - println!(" xrefs preserved : {} (MeSH bearings: {})", s.xrefs, s.mesh_xrefs); + println!( + " xrefs preserved : {} (MeSH bearings: {})", + s.xrefs, s.mesh_xrefs + ); // Per-namespace row census. let mut census: HashMap = HashMap::new(); for id in &baked.ids { *census.entry(id.ns).or_default() += 1; } - for ns in [Namespace::Mondo, Namespace::Hpo, Namespace::Uberon, Namespace::Pato, Namespace::Ro] { - println!(" {:8}: {}", ns.prefix(), census.get(&(ns as u8)).copied().unwrap_or(0)); + for ns in [ + Namespace::Mondo, + Namespace::Hpo, + Namespace::Uberon, + Namespace::Pato, + Namespace::Ro, + ] { + println!( + " {:8}: {}", + ns.prefix(), + census.get(&(ns as u8)).copied().unwrap_or(0) + ); } println!("\n=== EL SATURATION (ELK subset, excavated to Rust) ==="); let el = reason::saturate(&baked.triples); println!(" is_a subsumption pairs : {}", el.subsumption_pairs); println!(" part_of transitive pairs : {}", el.part_of_pairs); - println!(" existential inferred (R∃): {} (grounding inherited up the spine)", el.existential_inferred); - println!(" unsatisfiable : {} (no disjointness axioms in base obo)", el.unsatisfiable); + println!( + " existential inferred (R∃): {} (grounding inherited up the spine)", + el.existential_inferred + ); + println!( + " unsatisfiable : {} (no disjointness axioms in base obo)", + el.unsatisfiable + ); // Write the artifact + verify it round-trips through the loader contract. let bytes = as_le_bytes(&baked.rows); std::fs::write(&out, bytes).unwrap_or_else(|e| panic!("write {out}: {e}")); println!("\n=== ARTIFACT ==="); - println!(" {out} ({} bytes = {} rows × 512)", bytes.len(), baked.rows.len()); + println!( + " {out} ({} bytes = {} rows × 512)", + bytes.len(), + baked.rows.len() + ); let readback = std::fs::read(&out).expect("reread"); match rows_from_le_bytes(&readback) { Some(rows) => { // NB: a fresh Vec from fs::read may not be 64-aligned; the loader // returns None then and a real consumer uses FixedSizeBinary(512) // (arrow-aligned). We verify the in-memory aligned view instead. - println!(" reread rows_from_le_bytes: {} rows (aligned buffer)", rows.len()); + println!( + " reread rows_from_le_bytes: {} rows (aligned buffer)", + rows.len() + ); } None => { let inmem = rows_from_le_bytes(bytes).expect("in-memory aligned view"); @@ -97,5 +127,7 @@ fn main() { ); } } - println!(" loader contract: VERIFIED (as_le_bytes ↔ rows_from_le_bytes, 512×N, 64-align gate)"); + println!( + " loader contract: VERIFIED (as_le_bytes ↔ rows_from_le_bytes, 512×N, 64-align gate)" + ); } diff --git a/crates/ogar-obo/src/crosswalk.rs b/crates/ogar-obo/src/crosswalk.rs index 57e7c61..b905daa 100644 --- a/crates/ogar-obo/src/crosswalk.rs +++ b/crates/ogar-obo/src/crosswalk.rs @@ -103,16 +103,17 @@ impl Crosswalk { // roll up: drop a trailing digit past the 3-char WHO stem, else the // sub-category dot, else give up. if let Some(pos) = c.rfind(|ch: char| ch.is_ascii_digit()) - && (c.len() > 3 || (c.contains('.') && pos > c.find('.').unwrap())) { - c.truncate(pos); - if c.ends_with('.') { - c.pop(); - } - if c.is_empty() { - return None; - } - continue; + && (c.len() > 3 || (c.contains('.') && pos > c.find('.').unwrap())) + { + c.truncate(pos); + if c.ends_with('.') { + c.pop(); } + if c.is_empty() { + return None; + } + continue; + } return None; } } @@ -153,16 +154,31 @@ mod tests { let mut nodes: HashMap = HashMap::new(); // MONDO:5148 (T2DM) xrefs ICD-10 E11 + MeSH D003924 nodes.insert( - TermId { ns: Namespace::Mondo as u8, num: 5148 }, + TermId { + ns: Namespace::Mondo as u8, + num: 5148, + }, node_with_xrefs(vec![ - Xref { source: XrefSource::Icd, id: "E11".into() }, - Xref { source: XrefSource::Mesh, id: "D003924".into() }, + Xref { + source: XrefSource::Icd, + id: "E11".into(), + }, + Xref { + source: XrefSource::Mesh, + id: "D003924".into(), + }, ]), ); // UBERON:945 (stomach) xrefs FMA:7148 nodes.insert( - TermId { ns: Namespace::Uberon as u8, num: 945 }, - node_with_xrefs(vec![Xref { source: XrefSource::Other("FMA".into()), id: "7148".into() }]), + TermId { + ns: Namespace::Uberon as u8, + num: 945, + }, + node_with_xrefs(vec![Xref { + source: XrefSource::Other("FMA".into()), + id: "7148".into(), + }]), ); let baked = bake(&nodes, 0x0000); let cw = Crosswalk::from_bake(&baked); @@ -187,8 +203,14 @@ mod tests { // roll up to it (measured: MONDO carries no ICD-10-GM). let mut nodes: HashMap = HashMap::new(); nodes.insert( - TermId { ns: Namespace::Mondo as u8, num: 5148 }, - node_with_xrefs(vec![Xref { source: XrefSource::Icd, id: "E11".into() }]), + TermId { + ns: Namespace::Mondo as u8, + num: 5148, + }, + node_with_xrefs(vec![Xref { + source: XrefSource::Icd, + id: "E11".into(), + }]), ); let cw = Crosswalk::from_bake(&bake(&nodes, 0x0000)); // direct GM code fails, rollup succeeds — no separate GM table diff --git a/crates/ogar-obo/src/lib.rs b/crates/ogar-obo/src/lib.rs index e9391dd..3b3d8e2 100644 --- a/crates/ogar-obo/src/lib.rs +++ b/crates/ogar-obo/src/lib.rs @@ -171,10 +171,7 @@ impl TermId { if num > 0x00FF_FFFF { return None; } - Some(TermId { - ns: ns as u8, - num, - }) + Some(TermId { ns: ns as u8, num }) } /// This term's namespace. @@ -414,16 +411,18 @@ pub fn parse_obo(text: &str) -> std::collections::HashMap { // `relationship: ! label` — the target is the LAST // whitespace token before any `!`. if let Some(sid) = cur - && let Some(t) = last_curie(rest) { - let p = classify(sid.namespace(), t.namespace()); - nodes.entry(sid).or_default().rel.push((p, t)); - } + && let Some(t) = last_curie(rest) + { + let p = classify(sid.namespace(), t.namespace()); + nodes.entry(sid).or_default().rel.push((p, t)); + } } else if let Some(rest) = line.strip_prefix("intersection_of: ") { if let Some(sid) = cur - && let Some(t) = last_curie(rest) { - let p = classify(sid.namespace(), t.namespace()); - nodes.entry(sid).or_default().rel.push((p, t)); - } + && let Some(t) = last_curie(rest) + { + let p = classify(sid.namespace(), t.namespace()); + nodes.entry(sid).or_default().rel.push((p, t)); + } } else if let Some(rest) = line.strip_prefix("xref: ") { // `xref: : ! label` — the projection-join / guideline // bearing. Kept verbatim; NEVER truncated (MeSH → Leitlinie spider). @@ -431,12 +430,14 @@ pub fn parse_obo(text: &str) -> std::collections::HashMap { let tok = rest.split('!').next().unwrap_or(rest).trim(); let tok = tok.split_whitespace().next().unwrap_or(tok); if let Some((src, id)) = tok.split_once(':') - && !src.is_empty() && !id.is_empty() { - nodes.entry(sid).or_default().xref.push(Xref { - source: XrefSource::from_prefix(src), - id: id.to_string(), - }); - } + && !src.is_empty() + && !id.is_empty() + { + nodes.entry(sid).or_default().xref.push(Xref { + source: XrefSource::from_prefix(src), + id: id.to_string(), + }); + } } } } @@ -446,14 +447,22 @@ pub fn parse_obo(text: &str) -> std::collections::HashMap { /// First whitespace token of a line (before any `!` comment) — the target of /// an `is_a:` line. fn first_curie(s: &str) -> &str { - s.split('!').next().unwrap_or(s).split_whitespace().next().unwrap_or("").trim() + s.split('!') + .next() + .unwrap_or(s) + .split_whitespace() + .next() + .unwrap_or("") + .trim() } /// Last CURIE-shaped token before any `!` — the object of a `relationship:` / /// `intersection_of:` line (the predicate is the earlier token). fn last_curie(s: &str) -> Option { let head = s.split('!').next().unwrap_or(s); - head.split_whitespace().rfind(|t| t.contains(':')).and_then(TermId::parse) + head.split_whitespace() + .rfind(|t| t.contains(':')) + .and_then(TermId::parse) } // ── bake: nodes+edges → 512-byte rows + SPO triples + stats ──────────────── @@ -506,10 +515,7 @@ pub struct Bake { /// logical-def edges folded into the nodes' `rel`) into [`Bake`]. `app_prefix` /// is the lo-u16 render skin (`0x0000` = the canonical reference skin). #[must_use] -pub fn bake( - nodes: &std::collections::HashMap, - app_prefix: u16, -) -> Bake { +pub fn bake(nodes: &std::collections::HashMap, app_prefix: u16) -> Bake { let mut ids: Vec = nodes .iter() .filter(|(_, n)| !n.obsolete) @@ -655,22 +661,35 @@ pub fn parse_hp_logical_defs(owl: &str) -> Vec<(TermId, Predicate, TermId)> { in_eq += 1; } if in_eq > 0 - && let Some(sid) = cur { - for uid in find_obo_ids(line, "UBERON_") { - out.push(( - TermId { ns: Namespace::Hpo as u8, num: sid }, - Predicate::HasAnatomy, - TermId { ns: Namespace::Uberon as u8, num: uid }, - )); - } - for pid in find_obo_ids(line, "PATO_") { - out.push(( - TermId { ns: Namespace::Hpo as u8, num: sid }, - Predicate::HasQuality, - TermId { ns: Namespace::Pato as u8, num: pid }, - )); - } + && let Some(sid) = cur + { + for uid in find_obo_ids(line, "UBERON_") { + out.push(( + TermId { + ns: Namespace::Hpo as u8, + num: sid, + }, + Predicate::HasAnatomy, + TermId { + ns: Namespace::Uberon as u8, + num: uid, + }, + )); } + for pid in find_obo_ids(line, "PATO_") { + out.push(( + TermId { + ns: Namespace::Hpo as u8, + num: sid, + }, + Predicate::HasQuality, + TermId { + ns: Namespace::Pato as u8, + num: pid, + }, + )); + } + } if line.contains("") { in_eq = (in_eq - 1).max(0); } @@ -685,7 +704,10 @@ fn find_hp_about(line: &str) -> Option { let i = line.find("owl:Class rdf:about=")?; let rest = &line[i..]; let j = rest.find("HP_")?; - let digits: String = rest[j + 3..].chars().take_while(char::is_ascii_digit).collect(); + let digits: String = rest[j + 3..] + .chars() + .take_while(char::is_ascii_digit) + .collect(); digits.parse().ok() } @@ -697,9 +719,10 @@ fn find_obo_ids(line: &str, prefix: &str) -> Vec { let after = &hay[i + prefix.len()..]; let digits: String = after.chars().take_while(char::is_ascii_digit).collect(); if let Ok(n) = digits.parse::() - && n <= 0x00FF_FFFF { - ids.push(n); - } + && n <= 0x00FF_FFFF + { + ids.push(n); + } hay = &after[digits.len()..]; } ids @@ -742,15 +765,26 @@ mod tests { fn v3_tail_carries_oversize_curie_numerics_on_the_family_identity_rail() { // Above u16 — the case a bare `identity` cannot hold. let big = 700_092u32; - assert!(big > u32::from(u16::MAX), "fixture must exceed the u16 the rail exists for"); + assert!( + big > u32::from(u16::MAX), + "fixture must exceed the u16 the rail exists for" + ); let k = pack_key(Namespace::Mondo.render_classid(0x0000), big); // Byte positions, per new_v2. - assert_eq!(&k[10..12], &[0, 0], "leaf stays dormant (RESERVE, DON'T RECLAIM)"); + assert_eq!( + &k[10..12], + &[0, 0], + "leaf stays dormant (RESERVE, DON'T RECLAIM)" + ); let family = u16::from_le_bytes([k[12], k[13]]); let identity = u16::from_le_bytes([k[14], k[15]]); assert_eq!(u32::from(family), big >> 16, "family holds the high half"); - assert_eq!(u32::from(identity), big & 0xFFFF, "identity holds the low half"); + assert_eq!( + u32::from(identity), + big & 0xFFFF, + "identity holds the low half" + ); // Lossless through the public reader. let mut row = Row512::zeroed(); @@ -762,13 +796,19 @@ mod tests { // The V1 read of the SAME bytes must NOT agree — proof the tail really // moved, not that both layouts happen to coincide on this fixture. let v1_read = u32::from_le_bytes([row.0[13], row.0[14], row.0[15], 0]); - assert_ne!(v1_read, big, "a V1 u24 read of a V3 row must be observably wrong"); + assert_ne!( + v1_read, big, + "a V1 u24 read of a V3 row must be observably wrong" + ); // Ordering is preserved: family is the HIGH half, so (family, identity) // sorts as the numeric does. This is what keeps binary-search-by-key // valid over the sorted bake. let lo = pack_key(Namespace::Mondo.render_classid(0x0000), 5_148); - assert!(lo[12..16] < k[12..16], "tail bytes order as the numeric orders"); + assert!( + lo[12..16] < k[12..16], + "tail bytes order as the numeric orders" + ); } #[test] @@ -798,7 +838,11 @@ is_a: MONDO:0005015 ! diabetes mellitus\n"; let t = n(Namespace::Mondo, 5148); let node = &nodes[&t]; assert_eq!(node.xref.len(), 3, "all three xrefs kept"); - assert!(node.xref.iter().any(|x| x.source == XrefSource::Mesh && x.id == "D003924")); + assert!( + node.xref + .iter() + .any(|x| x.source == XrefSource::Mesh && x.id == "D003924") + ); let bake = bake(&nodes, 0x0000); assert_eq!(bake.stats.mesh_xrefs, 1, "MeSH bearing counted"); assert_eq!(bake.stats.xrefs, 3); diff --git a/crates/ogar-obo/src/reason.rs b/crates/ogar-obo/src/reason.rs index 05c06ca..093fe2d 100644 --- a/crates/ogar-obo/src/reason.rs +++ b/crates/ogar-obo/src/reason.rs @@ -115,9 +115,7 @@ pub struct ElStats { /// Returns, per node, the sorted deduped set of all transitive ancestors. /// Assumes acyclic (run [`count_is_a_cycles`] first); a residual cycle is /// simply not expanded past a re-visit, never loops. -fn ancestor_closure( - adj: &HashMap>, -) -> HashMap> { +fn ancestor_closure(adj: &HashMap>) -> HashMap> { // Kahn order over the parent graph, then DP bottom-up. // Build reverse (parent -> children) for indegree over child->parent edges. let mut all: Vec = Vec::new(); @@ -309,9 +307,15 @@ mod tests { fn t(sns: Namespace, s: u32, p: Predicate, ons: Namespace, o: u32) -> Triple { Triple { - s: TermId { ns: sns as u8, num: s }, + s: TermId { + ns: sns as u8, + num: s, + }, p, - o: TermId { ns: ons as u8, num: o }, + o: TermId { + ns: ons as u8, + num: o, + }, } } @@ -353,9 +357,27 @@ mod tests { // UBERON:200 is_a UBERON:300 (which is a kind of ...) // ⟹ HP:9 is grounded to UBERON:{200,300} too (2 inferred-beyond-asserted). let tr = vec![ - t(Namespace::Hpo, 9, Predicate::HasAnatomy, Namespace::Uberon, 100), - t(Namespace::Uberon, 100, Predicate::PartOf, Namespace::Uberon, 200), - t(Namespace::Uberon, 200, Predicate::IsA, Namespace::Uberon, 300), + t( + Namespace::Hpo, + 9, + Predicate::HasAnatomy, + Namespace::Uberon, + 100, + ), + t( + Namespace::Uberon, + 100, + Predicate::PartOf, + Namespace::Uberon, + 200, + ), + t( + Namespace::Uberon, + 200, + Predicate::IsA, + Namespace::Uberon, + 300, + ), ]; let s = saturate(&tr); // R∃ fires on BOTH existentials: HP:9→{200,300} (2) AND the part_of diff --git a/crates/ogar-render-askama/src/field_view.rs b/crates/ogar-render-askama/src/field_view.rs index c5f1d28..9fbd6fd 100644 --- a/crates/ogar-render-askama/src/field_view.rs +++ b/crates/ogar-render-askama/src/field_view.rs @@ -217,7 +217,10 @@ mod tests { // The surface is addressed by class + concept + key. assert!(src.contains("data-class-id=\"0x0102\""), "{src}"); - assert!(src.contains("data-concept=\"commercial_document\""), "{src}"); + assert!( + src.contains("data-concept=\"commercial_document\""), + "{src}" + ); assert!(src.contains("data-key=\"0801000301020304\""), "{src}"); // Each field carries its POSITION (layout address) — including the // wide position past 63. @@ -265,7 +268,10 @@ mod tests { assert!(!src.contains("