From f134ea8237f1d8c1874c4c42afd00bf58635362f Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 7 Aug 2026 11:17:29 +0000 Subject: [PATCH 1/7] ogar-vocab: mint the Blocks schema concepts + the blocks_actions table MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The authority half of blockly-rs's hot-plug. Before this, a blockly-rs plug answered `UnknownClassid(0x1701)`: `capability_registry:: resolve_hotplug` joins a consumer's classids against `class_ids::ALL`, and the Blocks domain had zero rows there. MINTED — two SCHEMA concepts, not the opcode palette: - `block_function` (0x1701) — one function body; the classid a stored FunctionNode is addressed by, and the only Blocks concept that binds capabilities. - `block_inventory` (0x1702) — the registry row; minted and addressable but binds nothing, because a registry read never touches a body and no executor has an inventory arm. The 2026-08-04 reserve withheld the OPCODE vocabulary (256 Blockly / Scratch operation bytes) from this codebook, and that still holds: an opcode is an FnIndex byte inside a body, resolved through ogar-loco's vocabulary table, never a classid. The domain-count test is regraded 0 → 2 with that distinction spelled out, plus the tripwire: if it ever counts in the hundreds, the palette leaked into the shared codebook. blocks_actions declares five capabilities on `block_function` — every one a real blockly-abi public function (lower_script / raise_calls / render_text / parse_text / klickweg_address), because resolve_hotplug checks coverage in BOTH directions and an aspirational entry would fail the consumer's own activation rather than quietly describing work nobody did. Registered in `domain_tables()` — the step whose omission was the ogar-osm defect `geo_actions` was written to correct. ogar-blockly's `BlockConcept::concept_id()` now READS `class_ids` instead of re-declaring 0x1701/0x1702. Two constants for one id is exactly the drift the classid join exists to catch. Also bumps the OGAR half of the two-sided COUNT_FUSE (90 → 92); the lance-graph mirror half lands in the paired PR. --- crates/ogar-blockly/src/lib.rs | 16 +- crates/ogar-vocab/src/blocks_actions.rs | 210 +++++++++++++++++++ crates/ogar-vocab/src/capability_registry.rs | 11 + crates/ogar-vocab/src/lib.rs | 95 ++++++++- 4 files changed, 320 insertions(+), 12 deletions(-) create mode 100644 crates/ogar-vocab/src/blocks_actions.rs diff --git a/crates/ogar-blockly/src/lib.rs b/crates/ogar-blockly/src/lib.rs index 49fc605..b03c502 100644 --- a/crates/ogar-blockly/src/lib.rs +++ b/crates/ogar-blockly/src/lib.rs @@ -136,13 +136,21 @@ 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::class_ids`, never re-declared here.** The two + /// schema concepts minted upstream when the hot-plug arc landed: a + /// consumer plugs a classid and + /// `ogar_vocab::capability_registry::resolve_hotplug` joins it against + /// `class_ids::ALL`, so an id that lived only in this crate would answer + /// `UnknownClassid` at the port. Two constants for one id is exactly the + /// drift the join exists to catch — so this reads the codebook. + /// + /// The *opcode palette* stays out of the shared codebook (that reserve is + /// intact): an opcode is an [`FnIndex`] byte inside a body, not a classid. #[must_use] pub const fn concept_id(self) -> u16 { match self { - BlockConcept::Content => 0x1701, - BlockConcept::Inventory => 0x1702, + BlockConcept::Content => ogar_vocab::class_ids::BLOCK_FUNCTION, + BlockConcept::Inventory => ogar_vocab::class_ids::BLOCK_INVENTORY, } } diff --git a/crates/ogar-vocab/src/blocks_actions.rs b/crates/ogar-vocab/src/blocks_actions.rs new file mode 100644 index 0000000..3c4e871 --- /dev/null +++ b/crates/ogar-vocab/src/blocks_actions.rs @@ -0,0 +1,210 @@ +//! Blocks capability surface — the **visual block-programming authoritative +//! action table**. +//! +//! Declares the capabilities a block frontend exposes over the `0x17XX` Blocks +//! concepts, as real [`ActionDef`]s, so +//! [`resolve_hotplug`](crate::capability_registry::resolve_hotplug) can +//! activate `blockly-rs` (and any sibling frontend) when it plugs a Blocks +//! classid in. +//! +//! # Why this lives HERE and not in `ogar-blockly` +//! +//! Same reason `geo_actions` lives here and not in `ogar-osm`, and the same +//! defect avoided: [`domain_tables`](crate::capability_registry::domain_tables) +//! resolves its entries at compile time from modules inside THIS crate, so a +//! table declared in the producer crate is a **parallel registry that verifies +//! itself against itself and passes** while the real port answers +//! `NoCapabilitiesFor`. `ogar-blockly` keeps what it is actually for — the +//! opcode palette, the [`Vocabulary`] implementation, the SoA split — none of +//! which is a capability declaration. +//! +//! # The subject split +//! +//! Only `block_function` binds capabilities. `block_inventory` is minted and +//! addressable but declares none, deliberately: a registry read never touches +//! a body, so no executor has an inventory arm, and a capability with no arm +//! fails `resolve_hotplug`'s both-directions coverage check for every +//! consumer. Declaring surface nobody implements is the fiction that check +//! exists to catch. +//! +//! | subject concept | capabilities | +//! |---|---| +//! | `block_function` (`0x1701`) | `lower_script` / `raise_calls` / `render_text` / `parse_text` / `klickweg_address` | +//! | `block_inventory` (`0x1702`) | *(none — registry rows are not bodies)* | +//! +//! # Every entry exists in the consumer today +//! +//! `lower_script` / `raise_calls` / `render_text` / `parse_text` are +//! `blockly_abi`'s own public functions; `klickweg_address` is +//! `blockly_abi::klickweg::address_of`. `resolve_hotplug` checks coverage in +//! BOTH directions, so an aspirational entry here fails blockly-rs's own +//! activation test rather than quietly describing work nobody did. +//! +//! [`Vocabulary`]: https://docs.rs/ogar-loco + +use crate::{ActionDef, ActionSubject, KausalSpec}; + +/// Every Blocks capability name, in table order — the `const`-evaluable +/// fingerprint of [`blocks_actions`], for a cheap exhaustiveness fuse without +/// paying for the table's allocations. +pub const BLOCKS_ACTION_NAMES: &[&str] = &[ + "lower_script", + "raise_calls", + "render_text", + "parse_text", + "klickweg_address", +]; + +/// One Blocks [`ActionDef`]. `object_class` is `ogit-blocks/{concept}` so +/// `derive_action_rows` recovers the concept from the last `/` segment and +/// resolves it against the codebook — the same fuse shape as +/// `geo_actions::geo_action_def` and `ocr_actions::ocr_action_def`. +fn blocks_action_def(capability: &'static str, subject_concept: &'static str) -> ActionDef { + let object_class = format!("ogit-blocks/{subject_concept}"); + let identity = format!("{object_class}::action_def::{capability}"); + ActionDef { + identity, + predicate: capability.to_owned(), + object_class, + // Lowering a workspace or raising a body is a pure transform the + // editor invokes on its own content — the caller is the substrate + // (an editor cast, a render pass), not an authenticated User. + default_subject: ActionSubject::System, + // Invoked directly by a same-process caller with no OGAR-side + // precondition to guard on — `KausalSpec::External`'s documented case. + kausal: Some(KausalSpec::External), + ..ActionDef::default() + } +} + +/// The Blocks capability surface — one [`ActionDef`] per capability, in +/// [`BLOCKS_ACTION_NAMES`] order. +#[must_use] +pub fn blocks_actions() -> Vec { + const SUBJECT_OF: &[(&str, &str)] = &[ + ("lower_script", "block_function"), + ("raise_calls", "block_function"), + ("render_text", "block_function"), + ("parse_text", "block_function"), + ("klickweg_address", "block_function"), + ]; + SUBJECT_OF + .iter() + .map(|&(capability, subject)| blocks_action_def(capability, subject)) + .collect() +} + +/// The executors the authority EXPECTS to register against this table. +/// +/// `blockly-abi` is the crate that owns every capability above. A sibling +/// frontend (scratch-rs) that grows the same arms is added here in the PR +/// that ships them, never in advance. +pub const BLOCKS_EXPECTED_EXECUTORS: &[&str] = &["blockly-abi"]; + +/// The distinct subject classids this table binds (canon-high concept ids). +/// A registering consumer must activate exactly this set — `block_inventory` +/// is deliberately absent (see the module's subject split). +pub const BLOCKS_SUBJECT_CLASSIDS: &[u16] = &[crate::class_ids::BLOCK_FUNCTION]; + +#[cfg(test)] +mod tests { + use super::*; + use crate::capability_registry::{HotplugDrift, entries_from_actions, resolve_hotplug}; + + #[test] + fn every_subject_concept_resolves_through_the_codebook() { + // The fuse: `derive_action_rows` splits `object_class` on '/' and + // resolves the tail. A typo'd concept name would land in the slag + // ledger as id 0 rather than failing loudly here, so assert the ids. + let rows = entries_from_actions(&blocks_actions()); + assert_eq!(rows.len(), BLOCKS_ACTION_NAMES.len()); + for (capability, id) in &rows { + assert_ne!(*id, 0, "{capability} did not resolve to a minted concept"); + assert_eq!( + id >> 8, + 0x17, + "{capability} resolved outside the Blocks domain" + ); + } + } + + #[test] + fn the_fingerprint_matches_the_table_in_order() { + let defs = blocks_actions(); + assert_eq!(defs.len(), BLOCKS_ACTION_NAMES.len()); + for (def, name) in defs.iter().zip(BLOCKS_ACTION_NAMES) { + assert_eq!(&def.predicate, name, "fingerprint drifted from the table"); + } + } + + #[test] + fn the_declared_subjects_are_exactly_the_subjects_the_table_uses() { + let mut used: Vec = entries_from_actions(&blocks_actions()) + .into_iter() + .map(|(_, id)| id) + .collect(); + used.sort_unstable(); + used.dedup(); + assert_eq!(used, BLOCKS_SUBJECT_CLASSIDS.to_vec()); + } + + #[test] + fn plugging_the_blocks_classid_into_the_port_activates() { + // The activation blockly-rs's own test mirrors. Before this module + // existed the same call returned `Err(UnknownClassid(0x1701))` — the + // Blocks concepts were not in `class_ids::ALL` at all. + let (concepts, capabilities) = + resolve_hotplug("blockly-abi", BLOCKS_SUBJECT_CLASSIDS, BLOCKS_ACTION_NAMES) + .expect("the blocks domain must activate"); + assert_eq!(capabilities.len(), BLOCKS_ACTION_NAMES.len()); + let names: Vec<&str> = concepts.iter().map(|&(n, _)| n).collect(); + assert_eq!(names, vec!["block_function"]); + } + + #[test] + fn the_port_rejects_a_wrong_consumer_and_coverage_gaps_both_ways() { + // Can-fire halves, so the activation above is not "the port says yes + // to everything". + assert!(matches!( + resolve_hotplug( + "some-other-crate", + BLOCKS_SUBJECT_CLASSIDS, + BLOCKS_ACTION_NAMES + ), + Err(HotplugDrift::UnexpectedConsumer(_)) + )); + // Declared-but-uncovered: the executor is missing an arm. + assert!(matches!( + resolve_hotplug("blockly-abi", BLOCKS_SUBJECT_CLASSIDS, &["lower_script"]), + Err(HotplugDrift::Uncovered(_)) + )); + // Covered-but-undeclared: the executor claims surface the authority + // does not declare — the other direction, which a one-way check misses. + let mut over = BLOCKS_ACTION_NAMES.to_vec(); + over.push("compile_to_wasm"); + assert!(matches!( + resolve_hotplug("blockly-abi", BLOCKS_SUBJECT_CLASSIDS, &over), + Err(HotplugDrift::Undeclared(_)) + )); + } + + #[test] + fn the_inventory_concept_is_minted_but_binds_nothing() { + // The deliberate half of the subject split: `block_inventory` is a + // real, addressable classid, and plugging it reports the honest + // "no capability" rather than silently activating an empty set. + assert!( + crate::class_ids::ALL + .iter() + .any(|&(_, id)| id == crate::class_ids::BLOCK_INVENTORY) + ); + assert!(matches!( + resolve_hotplug( + "blockly-abi", + &[crate::class_ids::BLOCK_INVENTORY], + BLOCKS_ACTION_NAMES + ), + Err(HotplugDrift::NoCapabilitiesFor(0x1702)) + )); + } +} diff --git a/crates/ogar-vocab/src/capability_registry.rs b/crates/ogar-vocab/src/capability_registry.rs index 7f11593..93b5f1a 100644 --- a/crates/ogar-vocab/src/capability_registry.rs +++ b/crates/ogar-vocab/src/capability_registry.rs @@ -186,6 +186,11 @@ pub fn domain_tables() -> Vec { expected_executors: crate::healthcare_actions::HEALTHCARE_EXPECTED_EXECUTORS, entries: healthcare_entries, }, + DomainTable { + domain: "blocks", + expected_executors: crate::blocks_actions::BLOCKS_EXPECTED_EXECUTORS, + entries: blocks_entries, + }, ] } @@ -326,6 +331,12 @@ fn geo_entries() -> Vec<(String, u16)> { entries_from_actions(&crate::geo_actions::geo_actions()) } +/// Blocks domain rows ([`crate::blocks_actions`], the blockly-abi table), +/// derived through the same generic [`entries_from_actions`] path as OCR. +fn blocks_entries() -> Vec<(String, u16)> { + entries_from_actions(&crate::blocks_actions::blocks_actions()) +} + /// Healthcare domain rows ([`crate::healthcare_actions`], the medcare-rs /// table — parity-plan P3), derived through the same generic /// [`entries_from_actions`] path as OCR. diff --git a/crates/ogar-vocab/src/lib.rs b/crates/ogar-vocab/src/lib.rs index f69aea7..a5755fa 100644 --- a/crates/ogar-vocab/src/lib.rs +++ b/crates/ogar-vocab/src/lib.rs @@ -34,6 +34,10 @@ use serde::{Deserialize, Serialize}; /// `::()` grammar (`E-GRAMMAR-IS-THE-RECIPE-SHAPE`). pub mod recipe; +/// Healthcare capability surface — the medcare-rs authoritative action +/// table (parity-plan P3; hand-authored, harvest-informed — see the +/// module doc for why the mechanical lift was falsified by the corpus). +pub mod blocks_actions; /// The tesseract-rs OCR capability surface — a hand-authored, non-`lift_*` /// [`ActionDef`] table (tesseract-rs has no source AST to extract from; see /// the module doc for why this is the authoritative action table rather @@ -42,9 +46,6 @@ pub mod recipe; /// `render_tsv` / `render_hocr` / `render_searchable_pdf`, each targeting a /// minted `0x08XX` [`class_ids`] concept. pub mod capability_registry; -/// Healthcare capability surface — the medcare-rs authoritative action -/// table (parity-plan P3; hand-authored, harvest-informed — see the -/// module doc for why the mechanical lift was falsified by the corpus). pub mod geo_actions; pub mod healthcare_actions; pub mod ocr_actions; @@ -1381,6 +1382,21 @@ const CODEBOOK: &[(&str, u16)] = &[ ("osm_note", 0x0F08), ("osm_gpx_trace", 0x0F09), ("osm_user", 0x0F0A), + // ── 0x17XX — Blocks (visual block-programming) ── + // The two SCHEMA concepts a block node's classid carries — NOT the opcode + // palette. The 0x17XX reserve note below withholds the *opcode vocabulary* + // (256 Blockly/Scratch operation bytes) from this codebook, and that still + // holds: opcodes are `FnIndex` bytes inside a function body, resolved + // through `ogar_loco`'s vocabulary table, and minting 256 of them here + // would bloat every consumer's codebook for a palette only block frontends + // read. These two are the different thing the reserve never covered — the + // classids a stored node is addressed BY, exactly as `osm_node` / + // `osm_way` are for geodata — and they must live here because + // `capability_registry::resolve_hotplug` joins a consumer's plug against + // `class_ids::ALL`. Without them blockly-rs cannot hot-plug at all + // (`UnknownClassid(0x1701)`). + ("block_function", 0x1701), + ("block_inventory", 0x1702), ]; /// Codebook **domain** — the high byte of a canonical id (see @@ -1986,6 +2002,22 @@ pub mod class_ids { /// `osm_user` (`0x0F0A`) — a mapper account. Rails `User`. pub const OSM_USER: u16 = 0x0F0A; + // ── 0x17XX — Blocks domain (visual block-programming schema concepts) ── + // + // The two classids a stored block node is addressed BY. The opcode + // palette is deliberately NOT here — see the CODEBOOK 0x17XX note. + + /// `block_function` (`0x1701`) — one **function body**: identity names the + /// function, the value slab carries the call stream. The content classid a + /// `FunctionNode` is stored under, and the only Blocks concept that binds + /// capabilities (`blocks_actions`). + pub const BLOCK_FUNCTION: u16 = 0x1701; + /// `block_inventory` (`0x1702`) — the **registry row**: which functions + /// exist, addressed by identity. A registry read never touches a body, so + /// this concept binds no capability (the mirror of the eight Geo concepts + /// that are minted and addressable but declare none). + pub const BLOCK_INVENTORY: u16 = 0x1702; + // ── 0x07XX — OSINT domain: no concept constants (low byte = APPID, // domain-wise; q2 = 0x01 → `0x0701` is OSINT-for-q2, not a concept — // operator ruling 2026-07-02; see the CODEBOOK section note). ── @@ -2096,6 +2128,9 @@ pub mod class_ids { ("osm_note", OSM_NOTE), ("osm_gpx_trace", OSM_GPX_TRACE), ("osm_user", OSM_USER), + // 0x17XX — Blocks (visual block-programming schema concepts) + ("block_function", BLOCK_FUNCTION), + ("block_inventory", BLOCK_INVENTORY), ]; #[cfg(test)] @@ -2159,7 +2194,7 @@ pub mod class_ids { // lance-graph mirror is rebuilt against it. assert_eq!( ALL.len(), - 90, + 92, "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 +3080,9 @@ pub fn all_promoted_classes() -> Vec { osm_note(), osm_gpx_trace(), osm_user(), + // 0x17XX — Blocks arm (visual block-programming schema concepts) + block_function(), + block_inventory(), ] } @@ -4761,6 +4799,39 @@ pub fn osm_user() -> Class { c } +// ───────────────────────────────────────────────────────────────────── +// 0x17XX — Blocks domain builders (visual block-programming schema). +// The two classids a stored block node is addressed BY — NOT the opcode +// palette, which stays an `FnIndex` byte inside a body (see the CODEBOOK +// 0x17XX note). Shapes grounded in the Apache-2.0 Blockly / scratch-blocks +// block definitions via `ogar-blockly`. +// ───────────────────────────────────────────────────────────────────── + +/// The `block_function` (`0x1701`) — one **function body**: identity names +/// the function, the value slab carries its call stream. The content classid +/// a stored `FunctionNode` carries, and the only Blocks concept that binds +/// capabilities (`blocks_actions`). +#[must_use] +pub fn block_function() -> Class { + let mut c = Class::new("BlockFunction"); + c.language = Language::Unknown; + c.canonical_concept = Some("block_function".to_string()); + c.associations = vec![family_edge("calls", "BlockFunction")]; + c +} + +/// The `block_inventory` (`0x1702`) — the **registry row**: which functions +/// exist, addressed by identity. One function = one owner = its own SoA, so a +/// registry read never touches a body. +#[must_use] +pub fn block_inventory() -> Class { + let mut c = Class::new("BlockInventory"); + c.language = Language::Unknown; + c.canonical_concept = Some("block_inventory".to_string()); + c.associations = vec![family_edge("registers", "BlockFunction")]; + 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 @@ -5749,10 +5820,18 @@ mod tests { // under q2) — reserved, zero concept rows until an operator ruling // mints one — see the CODEBOOK 0x0EXX section note. assert_eq!(concepts_in_domain(ConceptDomain::Genetics).count(), 0); - // Same posture for the Blocks domain (0x17, visual block-programming - // opcodes) — reserved 2026-08-04, zero concept rows until the opcode - // vocabulary is minted from Apache-2.0 / spec sources. - assert_eq!(concepts_in_domain(ConceptDomain::Blocks).count(), 0); + // The Blocks domain (0x17) is the ONE reserve that has since minted — + // and only partly, so the distinction is worth keeping sharp. The + // 2026-08-04 reserve withheld the *opcode vocabulary* (256 Blockly / + // Scratch operation bytes); that still holds and those are still zero + // here, because an opcode is an `FnIndex` inside a function body, not + // a classid. What DID mint are the two SCHEMA concepts a block node is + // addressed by — `block_function` / `block_inventory` — because + // `capability_registry::resolve_hotplug` joins a consumer's plug + // against `class_ids::ALL`, so blockly-rs could not hot-plug without + // them. Two, not 258: if this ever counts in the hundreds, the opcode + // palette leaked into the shared codebook and the reserve was broken. + assert_eq!(concepts_in_domain(ConceptDomain::Blocks).count(), 2); } #[test] From a3974955f57f0627d168e505f4eaad1d6555ba1b Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 7 Aug 2026 11:24:10 +0000 Subject: [PATCH 2/7] ogar-class-view: register the two Blocks concepts (CI fix) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CI caught what my scoped test run missed: `ogar-class-view` keeps a registry that must carry EVERY codebook concept, gated in both directions — every_codebook_id_appears_in_class_ids_all -> block_function (0x1701) in class_ids::ALL but missing from OgarClassView registry known_class_ids_iterates_in_stable_codebook_order -> known_class_ids drifted from codebook order (missing 5889, 5890) Both are the reverse gate working exactly as designed: a CODEBOOK promotion that lands in ogar-vocab without a matching registry entry is supposed to bang here, and it did. Registers `block_function` / `block_inventory` in `all_canonical_classes()`. The opcode palette is deliberately still absent — an opcode is an `FnIndex` byte inside a function body, never a classid, so it has no ObjectView. Process note: the previous commit was verified with `cargo test -p ogar-vocab -p ogar-blockly -p ogar-loco -p ogar-ro` — only the crates I edited. Minting a codebook concept has workspace-wide obligations by construction, so the scoped run could not have caught this. Now verified with `cargo test --workspace`: 80 test binaries, 0 failures; `cargo clippy --workspace --all-targets -- -D warnings` clean; `cargo fmt --all` clean. --- crates/ogar-class-view/src/lib.rs | 9 ++ 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 +- 7 files changed, 226 insertions(+), 92 deletions(-) diff --git a/crates/ogar-class-view/src/lib.rs b/crates/ogar-class-view/src/lib.rs index 077a68a..214a67e 100644 --- a/crates/ogar-class-view/src/lib.rs +++ b/crates/ogar-class-view/src/lib.rs @@ -79,6 +79,9 @@ use ogar_vocab::{ billable_work_entry, billing_party, blob, + // 0x17XX — blocks (visual block-programming schema concepts) + block_function, + block_inventory, bone, canonical_concept_id, charset, @@ -266,6 +269,12 @@ fn all_canonical_classes() -> Vec<(&'static str, Class)> { ("osm_note", osm_note()), ("osm_gpx_trace", osm_gpx_trace()), ("osm_user", osm_user()), + // ── 0x17XX — Blocks (visual block-programming schema concepts) ── + // The two classids a stored block node is addressed BY. The opcode + // palette is deliberately NOT here — an opcode is an `FnIndex` byte + // inside a function body, never a classid, so it has no `ObjectView`. + ("block_function", block_function()), + ("block_inventory", block_inventory()), ] } 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("