Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 14 additions & 8 deletions crates/ogar-class-view/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)`.
Expand Down Expand Up @@ -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()),
Expand Down
123 changes: 122 additions & 1 deletion crates/ogar-vocab/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand All @@ -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,
}
}
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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),
Expand Down Expand Up @@ -2580,6 +2621,11 @@ pub fn all_promoted_classes() -> Vec<Class> {
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(),
Expand Down Expand Up @@ -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]
Expand Down Expand Up @@ -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);
}

Expand Down Expand Up @@ -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);
Expand Down
35 changes: 30 additions & 5 deletions crates/ogar-vocab/src/ports.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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"));
Expand All @@ -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`,
Expand Down Expand Up @@ -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",
);
}
Expand Down
Loading