From 811861cc95fba06ccba7724292ead99cbe62cf40 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 1 Jul 2026 01:31:41 +0000 Subject: [PATCH] feat(op-canon): Snapshot::concept_by_class_id reverse record lookup MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add `Snapshot::concept_by_class_id(id: u16) -> Option<&Concept>` — the reverse lookup returning the full snapshot record for a codebook id, complementing #62's `canonical_concept_name` (which returns only the name via the OGAR map). Where the name-only map suffices for display, this yields the whole Concept — curator classes + class_id_le wire bytes — that corpus-evidence consumers need. Snapshot-backed (covers the OpenProject promoted concepts); the name it carries agrees with the OGAR reverse map, pinned by canonical_concept_name_agrees_with_the_snapshot. - 1 unit test: 0x0102 -> project_work_item record (WorkPackage among the curator classes); every concept round-trips by its own id; unknown -> None. cargo +1.95 fmt / clippy -D warnings / test -p op-canon: 24 unit + 8 doctests green. --- crates/op-canon/src/lib.rs | 39 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/crates/op-canon/src/lib.rs b/crates/op-canon/src/lib.rs index 247d89a..722e932 100644 --- a/crates/op-canon/src/lib.rs +++ b/crates/op-canon/src/lib.rs @@ -157,6 +157,21 @@ impl Snapshot { .iter() .find(|c| c.curator_classes.iter().any(|n| n == curator_class)) } + + /// Reverse lookup by codebook id: the full [`Concept`] record whose + /// `class_id` equals `id` (`0x0102` → the `project_work_item` concept, + /// with its curator classes + wire bytes). + /// + /// Complements [`crate::app::canonical_concept_name`] (which returns only + /// the name, via the OGAR reverse map): this returns the whole snapshot + /// record — curator classes, `class_id_le` — that the name-only map + /// cannot. Snapshot-backed, so it covers exactly the OpenProject promoted + /// concepts, and the name it carries agrees with the OGAR map (pinned by + /// [`crate::app::tests::canonical_concept_name_agrees_with_the_snapshot`]). + #[must_use] + pub fn concept_by_class_id(&self, id: u16) -> Option<&Concept> { + self.concepts.iter().find(|c| c.class_id_u16() == id) + } } #[cfg(test)] @@ -201,6 +216,30 @@ mod tests { } } + #[test] + fn concept_by_class_id_finds_the_full_record_and_round_trips() { + let s = Snapshot::load(); + // Known id → the project_work_item concept, with its curator classes. + let wp = s + .concept_by_class_id(class_ids::PROJECT_WORK_ITEM) + .expect("0x0102 is a promoted OpenProject concept"); + assert_eq!(wp.canonical_concept, "project_work_item"); + assert!( + wp.curator_classes.iter().any(|c| c == "WorkPackage"), + "expected WorkPackage among curator classes, saw {:?}", + wp.curator_classes, + ); + // Every concept round-trips: look up by its own id, get the same record. + for c in &s.concepts { + let found = s + .concept_by_class_id(c.class_id_u16()) + .expect("a concept's own id must resolve"); + assert_eq!(found.canonical_concept, c.canonical_concept); + } + // Unknown id → None (not a panic). + assert!(s.concept_by_class_id(0xFFFF).is_none()); + } + #[test] fn codebook_ids_are_unique() { use std::collections::HashSet;