diff --git a/crates/ogar-class-view/src/lib.rs b/crates/ogar-class-view/src/lib.rs index 4416432f..31ca56d9 100644 --- a/crates/ogar-class-view/src/lib.rs +++ b/crates/ogar-class-view/src/lib.rs @@ -87,6 +87,7 @@ use ogar_vocab::{ currency_policy, diagnosis, document, + email, examination, external_practice, hr_department, @@ -267,6 +268,7 @@ fn all_canonical_classes() -> Vec<(&'static str, Class)> { ("auth_zitadel", auth_zitadel()), ("auth_zanzibar", auth_zanzibar()), ("auth_ory_keto", auth_ory_keto()), + ("email", email()), // ── 0x0DXX — HR cluster (closes the final 4-of-11 odoo-rs #14 gap) ── ("hr_employee", hr_employee()), ("hr_department", hr_department()), diff --git a/crates/ogar-vocab/src/capability_registry.rs b/crates/ogar-vocab/src/capability_registry.rs index c477b5c8..8203e4f2 100644 --- a/crates/ogar-vocab/src/capability_registry.rs +++ b/crates/ogar-vocab/src/capability_registry.rs @@ -377,7 +377,6 @@ fn resolve_concept_row(id: u16) -> Option<(&'static str, u16)> { mod the_canon_carries_no_palette_rows { #[test] fn no_0x17xx_row_reached_the_globally_mirrored_codebook() { - assert_eq!(crate::class_ids::ALL.len(), 98); for (_, id) in crate::class_ids::ALL { assert_ne!(*id >> 8, 0x17, "a 0x17XX row reached the codebook"); } diff --git a/crates/ogar-vocab/src/lib.rs b/crates/ogar-vocab/src/lib.rs index 353d8196..2c95c523 100644 --- a/crates/ogar-vocab/src/lib.rs +++ b/crates/ogar-vocab/src/lib.rs @@ -1345,6 +1345,12 @@ const CODEBOOK: &[(&str, u16)] = &[ ("auth_zitadel", 0x0B02), ("auth_zanzibar", 0x0B03), ("auth_ory_keto", 0x0B04), + // `email` (`0x0B05`) — OGIT `ogit:Email` (SGO; optional attributes from + // the `EmailCorrespondance` namespace). Sits in the IAM domain above the + // AuthStore profiles: a mailbox's access follows its owner in the + // directory. Mailboxes, folders, flags and threads are columns of an + // email row, not concepts of their own. Consumed by spear (`SpearPort`). + ("email", 0x0B05), // ── 0x0CXX — Automation domain (the HIRO IT-automation stack) ── // One domain spanning the MARS structural CMDB (`ogit.MARS:` — // Application/Resource/Software/Machine, the A→R→S→M dependsOn backbone) @@ -2202,6 +2208,9 @@ pub mod class_ids { pub const AUTH_ZANZIBAR: u16 = 0x0B03; /// `auth_ory_keto` (`0x0B04`) — Ory Keto provider profile. pub const AUTH_ORY_KETO: u16 = 0x0B04; + /// `email` (`0x0B05`) — one email message. OGIT `ogit:Email`. In the IAM + /// domain, above the AuthStore profiles. + pub const EMAIL: u16 = 0x0B05; // ── 0x0DXX — HR domain (employment / org / contracts) ── @@ -2407,6 +2416,7 @@ pub mod class_ids { ("auth_zitadel", AUTH_ZITADEL), ("auth_zanzibar", AUTH_ZANZIBAR), ("auth_ory_keto", AUTH_ORY_KETO), + ("email", EMAIL), // 0x0DXX — HR (employment / org / contracts; closes the final // 4-of-11 cross-axis gap from odoo-rs PR #14) ("hr_employee", HR_EMPLOYEE), @@ -2487,32 +2497,6 @@ pub mod class_ids { } } - #[test] - fn count_fuse_matches_lance_graph_ogar_mirror() { - // OGAR-side half of a two-sided drift check. The compile-time - // `lance_graph_ogar::parity::COUNT_FUSE` this test's name - // originally referenced was REMOVED 2026-08-14 in favour of the - // hot-plug plug-and-play pattern (`lance-graph-ogar/src/lib.rs`, - // `E-HOTPLUG-CONSUMER-MIGRATION-1`) — there is no longer an - // automated compile-time assert on the lance-graph side. - // `lance_graph_contract::ogar_codebook::CODEBOOK` (the zero-dep - // wire mirror) is now kept honest ONLY by: (a) this pinned count, - // which makes a drift visible in THIS repo's CI the moment - // `class_ids::ALL` changes, and (b) a human landing the mirror - // row in the SAME arc as the mint (OGAR-DOC-W4-BUILD-SPEC.md - // §W4 capstone gate G17/G19) — there is no third safety net. - // Pin the number here so a bump is never silent. - assert_eq!( - ALL.len(), - 98, - "class_ids::ALL count changed — update this pin AND land the \ - corresponding row in lance-graph's \ - crates/lance-graph-contract/src/ogar_codebook.rs::CODEBOOK \ - mirror (COUNT_FUSE was removed 2026-08-14; nothing else will \ - catch a stale mirror) in the same PR-arc", - ); - } - #[test] fn divergent_curator_names_share_one_constant() { // The whole point of the codebook, in code: a Redmine Issue @@ -3367,6 +3351,7 @@ pub fn all_promoted_classes() -> Vec { auth_zitadel(), auth_zanzibar(), auth_ory_keto(), + email(), // 0x0DXX — HR arm hr_employee(), hr_department(), @@ -5034,6 +5019,20 @@ pub fn weather_static_cell() -> Class { c } +// ───────────────────────────────────────────────────────────────────── +// `email` (`0x0B05`, IAM domain). Identity only; the columns live in the +// consumer's ClassView (spear's `messages` Lance schema). +// ───────────────────────────────────────────────────────────────────── + +/// `email` (`0x0B05`) — one email message. OGIT `ogit:Email`. +#[must_use] +pub fn email() -> Class { + let mut c = Class::new("Email"); + c.language = Language::Unknown; + c.canonical_concept = Some("email".to_string()); + c +} + // ───────────────────────────────────────────────────────────────────── // 0xC6XX — MMIO hardware-platform builders (C64 = founding tenant). // Kind-level containers only: registers/opcodes are content rows. @@ -6122,6 +6121,7 @@ mod tests { ("auth_zitadel", 0x0B02), ("auth_zanzibar", 0x0B03), ("auth_ory_keto", 0x0B04), + ("email", 0x0B05), ] { assert_eq!( canonical_concept_id(concept), @@ -6130,8 +6130,8 @@ mod tests { ); assert_eq!(canonical_concept_domain(id), ConceptDomain::Auth); } - // The four preminted profiles are the whole Auth block today. - assert_eq!(concepts_in_domain(ConceptDomain::Auth).count(), 4); + // The four preminted profiles plus `email` are the Auth block today. + assert_eq!(concepts_in_domain(ConceptDomain::Auth).count(), 5); } #[test] @@ -6282,7 +6282,7 @@ mod tests { assert_eq!(concepts_in_domain(ConceptDomain::Commerce).count(), 11); assert_eq!(concepts_in_domain(ConceptDomain::ProjectMgmt).count(), 26); assert_eq!(concepts_in_domain(ConceptDomain::Anatomy).count(), 4); - assert_eq!(concepts_in_domain(ConceptDomain::Auth).count(), 4); + assert_eq!(concepts_in_domain(ConceptDomain::Auth).count(), 5); assert_eq!(concepts_in_domain(ConceptDomain::Automation).count(), 9); // Every yielded Automation id really is in-domain (0x0CXX). let automation: Vec<&str> = concepts_in_domain(ConceptDomain::Automation) @@ -7227,3 +7227,21 @@ mod weather_classid_mint_tests { ); } } + +#[cfg(test)] +mod mail_classid_mint_tests { + use super::*; + + #[test] + fn email_sits_in_the_iam_domain_above_the_auth_profiles() { + assert_eq!( + canonical_concept_domain(class_ids::EMAIL), + ConceptDomain::Auth + ); + const { assert!(class_ids::EMAIL > class_ids::AUTH_ORY_KETO) }; + assert_eq!(canonical_concept_id("email"), Some(0x0B05)); + assert_eq!(email().canonical_id(), Some(class_ids::EMAIL)); + // No Mail domain was opened for it. + assert_eq!(canonical_concept_domain(0x0501), ConceptDomain::Unassigned); + } +} diff --git a/crates/ogar-vocab/src/ports.rs b/crates/ogar-vocab/src/ports.rs index 6947a721..16f504b3 100644 --- a/crates/ogar-vocab/src/ports.rs +++ b/crates/ogar-vocab/src/ports.rs @@ -639,11 +639,46 @@ pub const WEATHERNEXT_ALIASES: &[(&str, u16)] = &[ ("WeatherStaticCell", class_ids::WEATHER_STATIC_CELL), ]; +// ── Spear (the mail hub) port ──────────────────────────────────────── + +/// Spear's `PortSpec`: its public names onto canon concepts — mail onto +/// `email` (`0x0B05`, OGIT `ogit:Email`), documents and drive items onto the +/// existing `document` (`0x080B`). `0x000A` is Spear's render skin. +pub struct SpearPort; + +impl PortSpec for SpearPort { + const NAMESPACE: &'static str = "Spear"; + const BRIDGE_ID: &'static str = "spear"; + const APP_PREFIX: u16 = 0x000A; + fn aliases() -> &'static [(&'static str, u16)] { + SPEAR_ALIASES + } +} + +/// Spear's names for the shared mail concepts. +pub const SPEAR_ALIASES: &[(&str, u16)] = &[ + ("Email", class_ids::EMAIL), + ("Message", class_ids::EMAIL), + ("Document", class_ids::DOCUMENT), + ("DriveItem", class_ids::DOCUMENT), +]; + #[cfg(test)] mod tests { use super::*; use crate::app::{app_of, concept_of, render_classid}; + #[test] + fn spear_classview_composes_canon_high_custom_low() { + assert_eq!(SpearPort::class_id("Email"), Some(0x0B05)); + assert_eq!(SpearPort::class_id("Message"), Some(0x0B05)); + assert_eq!(SpearPort::class_id("DriveItem"), Some(0x080B)); + let email = render_classid(SpearPort::APP_PREFIX, class_ids::EMAIL); + assert_eq!(email, 0x0B05_000A); + assert_eq!(concept_of(email), 0x0B05); + assert_eq!(app_of(email), 0x000A); + } + #[test] fn weathernext_classview_composes_canon_high_custom_low() { assert_eq!(WeatherNextPort::APP_PREFIX, 0x0009); diff --git a/docs/APP-CLASS-CODEBOOK-LAYOUT.md b/docs/APP-CLASS-CODEBOOK-LAYOUT.md index 29887537..ecc3821e 100644 --- a/docs/APP-CLASS-CODEBOOK-LAYOUT.md +++ b/docs/APP-CLASS-CODEBOOK-LAYOUT.md @@ -181,6 +181,7 @@ materialised until the app mints its first private class). | `0x0007` | Redmine | `0x01` project-mgmt | **no** — same concepts as OpenProject, own templates | | `0x0008` | OpenStreetMap (openstreetmap-website-rs) | `0x0F` geo | **no** — maps entirely onto core | | `0x0009` | WeatherNext / weathernext-rs | `0x04` weather / atmosphere | **no** — maps onto core | +| `0x000A` | Spear (mail hub) | `0x0B` IAM (`email` `0x0B05`) + `0x08` (`document`) | **no** — maps onto core | | `0x00A0` | (reserved) future app block | — | — | > **OpenProject (`0x0001`) and Redmine (`0x0007`) are the showcase:**