diff --git a/crates/ogar-class-view/src/lib.rs b/crates/ogar-class-view/src/lib.rs index b40c0f8..75defa2 100644 --- a/crates/ogar-class-view/src/lib.rs +++ b/crates/ogar-class-view/src/lib.rs @@ -69,14 +69,15 @@ use ogar_vocab::{ accounting_account, action_handler, anatomical_structure, auth_ory_keto, auth_store, auth_zanzibar, auth_zitadel, automation_trigger, billable_work_entry, billing_party, bone, canonical_concept_id, commercial_document, commercial_line_item, currency_policy, diagnosis, - joint, knowledge_item, lab_value, mars_application, mars_machine, mars_node_template, - mars_resource, mars_software, medication, patient, payment_record, pricelist, pricelist_rule, - priority, product, project, project_actor, project_attachment, project_changeset, - project_comment, project_custom_field, project_custom_value, project_enabled_module, - project_forum, project_journal, project_member_role, project_membership, project_message, - project_news, project_query, project_relation, project_repository, project_role, - project_status, project_type, project_version, project_watcher, project_wiki_page, - project_work_item, skeleton, tax_policy, treatment, unit_of_measure, visit, vital_sign, + hr_department, hr_employee, hr_employment_contract, hr_job, joint, knowledge_item, lab_value, + mars_application, mars_machine, mars_node_template, mars_resource, mars_software, medication, + patient, payment_record, pricelist, pricelist_rule, priority, product, project, project_actor, + project_attachment, project_changeset, project_comment, project_custom_field, + project_custom_value, project_enabled_module, project_forum, project_journal, + project_member_role, project_membership, project_message, project_news, project_query, + project_relation, project_repository, project_role, project_status, project_type, + project_version, project_watcher, project_wiki_page, project_work_item, skeleton, tax_policy, + treatment, unit_of_measure, visit, vital_sign, }; /// All promoted canonical concepts: `(canonical_concept_name, Class)`. @@ -144,6 +145,11 @@ fn all_canonical_classes() -> Vec<(&'static str, Class)> { ("auth_zitadel", auth_zitadel()), ("auth_zanzibar", auth_zanzibar()), ("auth_ory_keto", auth_ory_keto()), + // ── 0x0DXX — HR cluster (closes the final 4-of-11 odoo-rs #14 gap) ── + ("hr_employee", hr_employee()), + ("hr_department", hr_department()), + ("hr_job", hr_job()), + ("hr_employment_contract", hr_employment_contract()), // ── 0x0CXX — automation (HIRO MARS CMDB + DO-arm actuators) ── ("mars_application", mars_application()), ("mars_resource", mars_resource()), diff --git a/crates/ogar-vocab/src/lib.rs b/crates/ogar-vocab/src/lib.rs index 04f967b..3ddb02a 100644 --- a/crates/ogar-vocab/src/lib.rs +++ b/crates/ogar-vocab/src/lib.rs @@ -1235,6 +1235,15 @@ const CODEBOOK: &[(&str, u16)] = &[ ("action_handler", 0x0C07), ("action_applicability", 0x0C08), ("automation_trigger", 0x0C09), + // ── 0x0DXX — HR domain (employment / org / contracts) ── + // Public HR master-data: person + organizational-unit + role + employment- + // contract entities. Distinct from Auth (IdP→classid bridge) and Health + // (PHI). Closes the final 4-of-11 cross-axis identity gap surfaced by + // odoo-rs PR #14: hr.employee / hr.department / hr.job / hr.contract. + ("hr_employee", 0x0D01), + ("hr_department", 0x0D02), + ("hr_job", 0x0D03), + ("hr_employment_contract", 0x0D04), ]; /// Codebook **domain** — the high byte of a canonical id (see @@ -1282,6 +1291,13 @@ pub enum ConceptDomain { /// public-reference posture as [`Anatomy`](Self::Anatomy). See /// `docs/MARS-TRANSCODING.md` + `docs/HIRO-DO-ARM-LIFT.md`. Automation, + /// `0x0DXX` — HR (employment / org / contracts). Public master-data for + /// person + organizational-unit + role + employment-contract entities; + /// distinct from `Auth` identity (which is the IdP→classid bridge) and + /// from `Health` PHI. Mirrors arago HIRO HR semantics + Odoo `hr.*` + + /// `vcard:Individual` / `org:OrganizationalUnit` / `org:Role` / + /// `fibo:Contract` alignment. + HR, /// Any high-byte slot not yet assigned a domain (`0x03XX`–`0x06XX`, /// `0x0DXX`+). Unassigned, @@ -1301,6 +1317,7 @@ pub fn canonical_concept_domain(id: u16) -> ConceptDomain { 0x0A => ConceptDomain::Anatomy, 0x0B => ConceptDomain::Auth, 0x0C => ConceptDomain::Automation, + 0x0D => ConceptDomain::HR, _ => ConceptDomain::Unassigned, } } @@ -1612,6 +1629,24 @@ pub mod class_ids { /// `auth_ory_keto` (`0x0B04`) — Ory Keto provider profile. pub const AUTH_ORY_KETO: u16 = 0x0B04; + // ── 0x0DXX — HR domain (employment / org / contracts) ── + + /// `hr_employee` (`0x0D01`) — person record. OSB `Employee`, Odoo + /// `hr.employee` (`vcard:Individual`). + pub const HR_EMPLOYEE: u16 = 0x0D01; + /// `hr_department` (`0x0D02`) — organizational unit (sub-tree of an + /// organization). OSB `Department`, Odoo `hr.department` + /// (`org:OrganizationalUnit`). + pub const HR_DEPARTMENT: u16 = 0x0D02; + /// `hr_job` (`0x0D03`) — role / position. OSB `Job`, Odoo `hr.job` + /// (`org:Role`). + pub const HR_JOB: u16 = 0x0D03; + /// `hr_employment_contract` (`0x0D04`) — base employment contract. + /// OSB `Contract`, Odoo `hr.contract` (`fibo:Contract`). Payroll + /// computation stays outside the codebook (Odoo Enterprise / OSB + /// add-on territory). + pub const HR_EMPLOYMENT_CONTRACT: u16 = 0x0D04; + // ── 0x0CXX — Automation domain (HIRO IT-automation stack) ── /// `mars_application` (`0x0C01`) — a MARS Application CMDB entity; head of @@ -1707,6 +1742,12 @@ pub mod class_ids { ("auth_zitadel", AUTH_ZITADEL), ("auth_zanzibar", AUTH_ZANZIBAR), ("auth_ory_keto", AUTH_ORY_KETO), + // 0x0DXX — HR (employment / org / contracts; closes the final + // 4-of-11 cross-axis gap from odoo-rs PR #14) + ("hr_employee", HR_EMPLOYEE), + ("hr_department", HR_DEPARTMENT), + ("hr_job", HR_JOB), + ("hr_employment_contract", HR_EMPLOYMENT_CONTRACT), // 0x0CXX — automation (HIRO IT-automation: MARS CMDB + actuators) ("mars_application", MARS_APPLICATION), ("mars_resource", MARS_RESOURCE), @@ -2580,6 +2621,11 @@ pub fn all_promoted_classes() -> Vec { auth_zitadel(), auth_zanzibar(), auth_ory_keto(), + // 0x0DXX — HR arm + hr_employee(), + hr_department(), + hr_job(), + hr_employment_contract(), // 0x0CXX — automation arm (HIRO MARS CMDB + DO-arm actuators), // in class_ids::ALL order. mars_application(), @@ -3804,6 +3850,80 @@ pub fn anatomical_structure() -> Class { c } +// ───────────────────────────────────────────────────────────────────── +// 0x0DXX — HR domain (employment / org / contracts). The reusable +// Active-Record shape for HR master-data per arago HIRO + Odoo `hr.*` + +// vcard/org/fibo alignment. Field names are English schema labels. + +/// `hr_employee` (`0x0D01`) — person record (vcard:Individual). +pub fn hr_employee() -> Class { + let mut c = Class::new("HrEmployee"); + c.language = Language::Unknown; + c.canonical_concept = Some("hr_employee".to_string()); + c.associations = Vec::new(); + let mut full_name = Attribute::new("full_name"); + full_name.type_name = Some("string".to_string()); + let mut email = Attribute::new("email"); + email.type_name = Some("string".to_string()); + let mut phone = Attribute::new("phone"); + phone.type_name = Some("string".to_string()); + let mut employee_id = Attribute::new("employee_id"); + employee_id.type_name = Some("string".to_string()); + c.attributes = vec![full_name, email, phone, employee_id]; + c +} + +/// `hr_department` (`0x0D02`) — organizational unit (org:OrganizationalUnit). +pub fn hr_department() -> Class { + let mut c = Class::new("HrDepartment"); + c.language = Language::Unknown; + c.canonical_concept = Some("hr_department".to_string()); + c.associations = Vec::new(); + let mut name = Attribute::new("name"); + name.type_name = Some("string".to_string()); + let mut manager_ref = Attribute::new("manager_ref"); + manager_ref.type_name = Some("string".to_string()); + let mut parent_ref = Attribute::new("parent_ref"); + parent_ref.type_name = Some("string".to_string()); + c.attributes = vec![name, manager_ref, parent_ref]; + c +} + +/// `hr_job` (`0x0D03`) — role / position (org:Role). +pub fn hr_job() -> Class { + let mut c = Class::new("HrJob"); + c.language = Language::Unknown; + c.canonical_concept = Some("hr_job".to_string()); + c.associations = Vec::new(); + let mut title = Attribute::new("title"); + title.type_name = Some("string".to_string()); + let mut description = Attribute::new("description"); + description.type_name = Some("string".to_string()); + let mut department_ref = Attribute::new("department_ref"); + department_ref.type_name = Some("string".to_string()); + c.attributes = vec![title, description, department_ref]; + c +} + +/// `hr_employment_contract` (`0x0D04`) — base employment contract +/// (fibo:Contract). Payroll computation stays out of the codebook. +pub fn hr_employment_contract() -> Class { + let mut c = Class::new("HrEmploymentContract"); + c.language = Language::Unknown; + c.canonical_concept = Some("hr_employment_contract".to_string()); + c.associations = Vec::new(); + let mut start_date = Attribute::new("start_date"); + start_date.type_name = Some("date".to_string()); + let mut end_date = Attribute::new("end_date"); + end_date.type_name = Some("date".to_string()); + let mut contract_type = Attribute::new("contract_type"); + contract_type.type_name = Some("string".to_string()); + let mut salary = Attribute::new("salary"); + salary.type_name = Some("decimal".to_string()); + c.attributes = vec![start_date, end_date, contract_type, salary]; + c +} + /// The `skeleton` (`0x0A02`) — the whole-body skeletal system; the root of /// the bone partonomy (`crates/ogar-fma-skeleton`). #[must_use] @@ -4611,7 +4731,7 @@ mod tests { // Unassigned blocks (3-6, D+). assert_eq!(canonical_concept_domain(0x0300), ConceptDomain::Unassigned); assert_eq!(canonical_concept_domain(0x0600), ConceptDomain::Unassigned); - assert_eq!(canonical_concept_domain(0x0D00), ConceptDomain::Unassigned); + assert_eq!(canonical_concept_domain(0x0D00), ConceptDomain::HR); assert_eq!(canonical_concept_domain(0xFFFF), ConceptDomain::Unassigned); } @@ -4710,6 +4830,7 @@ mod tests { } // Counts line up with the codebook blocks. assert_eq!(concepts_in_domain(ConceptDomain::Health).count(), 7); + assert_eq!(concepts_in_domain(ConceptDomain::HR).count(), 4); 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); diff --git a/crates/ogar-vocab/src/ports.rs b/crates/ogar-vocab/src/ports.rs index 74d7775..ccc3888 100644 --- a/crates/ogar-vocab/src/ports.rs +++ b/crates/ogar-vocab/src/ports.rs @@ -501,6 +501,12 @@ pub const ODOO_ALIASES: &[(&str, u16)] = &[ ("product.pricelist", class_ids::PRICELIST), ("product.pricelist.item", class_ids::PRICELIST_RULE), ("uom.uom", class_ids::UNIT_OF_MEASURE), + // HR cluster — closes the final 4-of-11 cross-axis identity gap surfaced + // by odoo-rs PR #14. New 0x0DXX concept domain (HR). + ("hr.employee", class_ids::HR_EMPLOYEE), + ("hr.department", class_ids::HR_DEPARTMENT), + ("hr.job", class_ids::HR_JOB), + ("hr.contract", class_ids::HR_EMPLOYMENT_CONTRACT), // Cross-arm bridge: the timesheet / cost line converges on the // project-arm `billable_work_entry` (0x0103) — the SAME id // OpenProject `TimeEntry` and Redmine `TimeEntry` resolve to. @@ -958,10 +964,13 @@ mod tests { fn odoo_commerce_models_resolve_into_the_commerce_domain() { use crate::{ConceptDomain, canonical_concept_domain}; // Every commerce-arm alias lands in the Commerce (0x02XX) domain. - // `account.analytic.line` is the deliberate exception — it's the - // cross-arm bridge into the project domain (asserted separately). + // Two deliberate exceptions: `account.analytic.line` is the cross-arm + // bridge into the project domain, and the `hr.*` cluster lives in + // the HR domain (0x0DXX) — both are asserted in their own tests + // (`account_analytic_line_resolves_into_the_project_domain` / + // `odoo_hr_models_resolve_into_the_hr_domain`). for &(name, _) in OdooPort::aliases() { - if name == "account.analytic.line" { + if name == "account.analytic.line" || name.starts_with("hr.") { continue; } let id = OdooPort::class_id(name).unwrap_or_else(|| panic!("`{name}` must resolve")); @@ -973,6 +982,22 @@ mod tests { } } + #[test] + fn odoo_hr_models_resolve_into_the_hr_domain() { + use crate::{ConceptDomain, canonical_concept_domain}; + // The hr.* cluster (HR_EMPLOYEE / HR_DEPARTMENT / HR_JOB / + // HR_EMPLOYMENT_CONTRACT) lands in the HR (0x0DXX) domain — closes + // the final 4-of-11 cross-axis identity gap from odoo-rs PR #14. + for name in ["hr.employee", "hr.department", "hr.job", "hr.contract"] { + let id = OdooPort::class_id(name).unwrap_or_else(|| panic!("`{name}` must resolve")); + assert_eq!( + canonical_concept_domain(id), + ConceptDomain::HR, + "`{name}` -> 0x{id:04X} must live in the HR (0x0DXX) domain", + ); + } + } + /// **The planning ↔ ERP convergence pin.** A logged unit of work is /// one canonical concept — `billable_work_entry` (0x0103) — whether /// it arrives as an OpenProject `TimeEntry`, a Redmine `TimeEntry`, @@ -1005,13 +1030,13 @@ mod tests { // 9 Odoo model aliases = 8 commerce-arm (account.move, // sale.order, account.move.line, sale.order.line, account.tax, // res.partner, account.payment, res.currency) + 4 product/accounting - // master-record aliases + 3 ProductCatalog cluster aliases (product.template, product.product, + // master-record aliases + 3 ProductCatalog cluster aliases + 4 HR cluster aliases (product.template, product.product, // account.account, account.account.template — Phase-3 mints per // odoo-rs PR #14 + #16) + 1 cross-arm bridge // (account.analytic.line → billable_work_entry). Re-count on drift. assert_eq!( OdooPort::aliases().len(), - 16, + 20, "Odoo alias count drift — re-count the ODOO_ALIASES table", ); }