From c5994175ca5fda37eab04a188712e238f6ca12c1 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 22 Jun 2026 18:42:04 +0000 Subject: [PATCH] =?UTF-8?q?feat(ogar-vocab):=20canonical=5Fconcept=5Fname?= =?UTF-8?q?=20=E2=80=94=20the=20id=20=E2=86=92=20concept-name=20reverse=20?= =?UTF-8?q?map?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The inverse of `canonical_concept_id`: `canonical_concept_name(u16) -> Option<&'static str>`, a linear scan over the existing CODEBOOK. Ids are unique in the codebook (now asserted by a round-trip test over all 39 entries), so the mapping is 1:1 and round-trips both ways. This is the capability odoo-rs filed as PROBE-OGAR-ID-TO-CONCEPT-NAME (its specs/UPSTREAM_WISHLIST.md): a consumer that resolved a classid via a port alias (name → id) needs id → concept-name to (a) stamp a readable `COMMENT 'commercial_document (classid:0x00020202)'` into emitted DDL and (b) populate `Class::canonical_concept` from an id — WITHOUT copying the codebook locally (which the Core-First doctrine forbids). The forward step plus this reverse step also resolves the lexical asymmetry: both `sale.order` and `account.move` resolve to 0x0202 → `commercial_document`, where neither name lexically IS the concept. Additive, zero behaviour change. 2 new tests (round-trip over CODEBOOK + known-ids/None); full ogar-vocab suite 85 passing, clippy clean on the new code. Co-Authored-By: Claude Opus 4.8 --- crates/ogar-vocab/src/lib.rs | 52 ++++++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/crates/ogar-vocab/src/lib.rs b/crates/ogar-vocab/src/lib.rs index 5862d45..d023318 100644 --- a/crates/ogar-vocab/src/lib.rs +++ b/crates/ogar-vocab/src/lib.rs @@ -1264,6 +1264,30 @@ pub fn canonical_concept_id(concept: &str) -> Option { .find_map(|(name, id)| if *name == concept { Some(*id) } else { None }) } +/// Inverse of [`canonical_concept_id`]: the canonical concept name for a +/// codebook `id`, or `None` if the id is not a promoted concept. +/// +/// Ids are unique in [`CODEBOOK`] (a concept arrives once and never moves — +/// asserted by [`tests::canonical_concept_name_round_trips`]), so the mapping +/// is 1:1 and round-trips both ways: +/// `canonical_concept_id(canonical_concept_name(id)?) == Some(id)`. +/// +/// This is the reverse lookup a consumer needs to turn a *resolved* classid +/// back into its human-readable concept without re-deriving (or copying) the +/// codebook locally — e.g. to stamp `COMMENT 'commercial_document +/// (classid:0x00020202)'` into emitted DDL, or to populate a `Class`'s +/// `canonical_concept` from an id. The forward `name -> id` step +/// ([`canonical_concept_id`] via a port alias) followed by this `id -> name` +/// step resolves the alias asymmetry that lexical canonicalisation cannot +/// (e.g. both `sale.order` and `account.move` resolve to `0x0202` -> +/// `commercial_document`, where neither name lexically *is* the concept). +#[must_use] +pub fn canonical_concept_name(id: u16) -> Option<&'static str> { + CODEBOOK + .iter() + .find_map(|(name, cid)| if *cid == id { Some(*name) } else { None }) +} + /// **Compile-time class-id constants** — every promoted concept's id /// exposed as a named `pub const u16` so downstream consumers can dispatch /// on canonical identity at compile time without a [`canonical_concept_id`] @@ -3508,6 +3532,34 @@ mod tests { assert_eq!(canonical_concept_id("user"), None); } + #[test] + fn canonical_concept_name_round_trips() { + // Every codebook id reverses to its name, and that name maps forward + // to the same id — proving the reverse lookup is total over the + // codebook AND that ids are unique (a collision would break one + // direction). This is the gate `PROBE-OGAR-ID-TO-CONCEPT-NAME` (odoo-rs + // UPSTREAM_WISHLIST) asks for before the consumer fusion can land. + for &(name, id) in CODEBOOK { + assert_eq!( + canonical_concept_name(id), + Some(name), + "id 0x{id:04X} must reverse to `{name}`", + ); + assert_eq!(canonical_concept_id(name), Some(id)); + } + } + + #[test] + fn canonical_concept_name_known_ids_and_none_for_unknown() { + assert_eq!(canonical_concept_name(0x0202), Some("commercial_document")); + assert_eq!(canonical_concept_name(0x0103), Some("billable_work_entry")); + assert_eq!(canonical_concept_name(0x0204), Some("billing_party")); + // Ids outside the codebook (the 0x0000 default sentinel, the 0xFFFF + // max) have no canonical concept — None, never a synthesised name. + assert_eq!(canonical_concept_name(0x0000), None); + assert_eq!(canonical_concept_name(0xFFFF), None); + } + #[test] fn ogar_codebook_maps_curator_labels_to_canonical_id() { // The load-bearing insight: leave the curator name shape intact;