diff --git a/.claude/board/INTEGRATION_PLANS.md b/.claude/board/INTEGRATION_PLANS.md index c0056d41..f1550011 100644 --- a/.claude/board/INTEGRATION_PLANS.md +++ b/.claude/board/INTEGRATION_PLANS.md @@ -1,3 +1,25 @@ +## 2026-05-27 — odoo-savant-reasoners-v1 (lance-graph side of the Odoo richness harvest: 2 new OGIT families + Layer-2 axioms + StyleCluster wiring + 5 Reasoner impls) + +**Status:** PROPOSAL (picks up the cross-repo handover boundary in `.claude/odoo/SAVANTS.md` §"lance-graph handover boundary"). woa-rs defined the 25-Savant roster + delegation tuples; lance-graph implements (a) Reasoner impls, (b) 2 new families + Layer-2 alignment axioms for the `None` classes, (c) StyleCluster wiring. +**Confidence:** HIGH on (b)/(c) — additive extensions of `odoo_alignment.rs` seed + alignment TTLs. MED on (a) — Reasoner dispatch shape (one impl per ReasoningKind vs savant-config registry) pinned but needs a review pass. +**Plan file:** `.claude/plans/odoo-savant-reasoners-v1.md` +**Predecessors:** PR #412 (odoo hydrator + dolce_odoo classifier + ODOO slot 50), PR #413 (briefing pack). +**Anchored iron rules:** I-VSA-IDENTITIES (savant = Layer-2 role catalogue), AGI-as-glove, board-hygiene, Iron Rule 1 (no brain-crate in customer binary), Iron Rule 7 (verhaltens-bewahrend — reasoner output is suggestion-only). + +### Scope +Group B — `0x63 ProductCatalog` (Analytical) + `0x90 HRFoundation` (Empathic) families + Layer-2 alignment axioms for `stock.*` / `account.analytic.distribution.model` / `account.account.tag` (land on existing pivot where honest, else documented `None`). Group C — `StyleCluster` per family (field-or-sidecar). Group A — `SavantConclusion` + 5 `Reasoner` impls (one per `ReasoningKind`) in lance-graph-callcenter, dispatching on evidence + family style, `InferenceType::default_strategy()` → QueryStrategy, NarsTruth evidence fusion. + +### Deliverables +D-ODOO-SAV-1 two new families + seed rows + family_registry.ttl · D-ODOO-SAV-2 Layer-2 alignment axioms TTL · D-ODOO-SAV-3 StyleCluster per family · D-ODOO-SAV-4 5 Reasoner impls (gated on dispatch-shape review, own PR). + +### Execution +D-ODOO-SAV-1/2/3 additive + low-risk → first PR (this session). D-ODOO-SAV-4 → follow-up PR after `/code-review` on dispatch shape. Plan + INTEGRATION_PLANS prepend land with D-ODOO-SAV-1. + +### Invariants +Option B (inherit existing slots; new families are genuine basins not per-class mints; `None` stays `None` w/o honest pivot) · public OWL pristine (axioms are NEW TTL) · savant = Layer-2 catalogue · reasoner output = suggestion (guard stays in woa-rs) · impls in callcenter behind contract `Reasoner` trait. + +--- + ## 2026-05-27 — atom-mailbox-substrate-v1 (ladder-serves-mailbox: atoms→styles→personas, quorum projection, counterfactual mantissa, AriGraph hot/cold/tombstone) **Status:** PROPOSAL (implements `EPIPHANIES.md` E-LADDER-SERVES-MAILBOX; extends `rung-persona-orchestration-v1` D-PERSONA-1 downward into the atom layer and outward into the mailbox lifecycle). diff --git a/.claude/plans/odoo-savant-reasoners-v1.md b/.claude/plans/odoo-savant-reasoners-v1.md new file mode 100644 index 00000000..91600e9e --- /dev/null +++ b/.claude/plans/odoo-savant-reasoners-v1.md @@ -0,0 +1,123 @@ +# odoo-savant-reasoners-v1 — lance-graph side of the Odoo richness harvest + +> **Status:** PROPOSAL. Picks up the explicit cross-repo handover boundary +> declared in `lance-graph/.claude/odoo/SAVANTS.md` §"lance-graph handover +> boundary": the woa-rs session defined the 25-Savant roster + delegation +> tuples + call sites + evidence schemas (all contract-level, BBB-allowed); +> the **lance-graph side** must now implement (a) the `Reasoner` impls, (b) +> the two new OGIT families + Layer-2 alignment axioms for the `None` +> classes, (c) the `StyleCluster` wiring. +> +> **Confidence:** HIGH on (b)/(c) — concrete additive extensions of the +> existing `odoo_alignment.rs` seed table + alignment TTLs. MED on (a) — the +> `Reasoner` dispatch shape is an architectural decision (one impl per +> `ReasoningKind` vs a savant-config registry) that this plan pins but which +> needs a review pass before the larger build. +> +> **Predecessors:** PR #412 (odoo hydrator + `dolce_odoo` classifier + ODOO +> slot 50), PR #413 (briefing pack import). Reads on: +> `lance-graph-contract::reasoning` (Reasoner / ReasoningKind / ReasoningContext), +> `lance-graph-callcenter::{family_table, odoo_alignment, unified_bridge}`, +> `lance-graph-contract::thinking::StyleCluster`. +> +> **Anchored iron rules:** I-VSA-IDENTITIES (savant = Layer-2 role catalogue, +> content in tables not bundles), the AGI-as-glove doctrine (savant dispatch +> rides `MetaColumn`/`EdgeColumn`, no new service), board-hygiene. + +## Scope + +Three deliverable groups, matching SAVANTS.md (a)/(b)/(c): + +### Group B — two new OGIT families + Layer-2 alignment axioms (lowest risk, first) + +The roster needs homes for classes that resolve to `None` today: + +- **`0x63 ProductCatalog`** (basin: product catalogue + pricelist + UoM; + default style **Analytical**). Lands `product.template`, + `product.pricelist`, `product.pricelist.item`, `uom.uom`, + `product.category`. Currently `product.*` inherits `0x61 BillingCore` + slot 1 via the prefix fallback — ProductCatalog promotes the + catalogue/pricing concepts to their own basin so L8's + `PricelistAssignmentAgent` has a real family instead of `None`. +- **`0x90 HRFoundation`** (basin: employee / org; default style + **Empathic**). Lands `hr.employee`→`vcard:Individual`, + `hr.department`→`org:OrganizationalUnit`, `hr.job`, `hr.contract` (base + only — payroll engine is Enterprise/absent, flagged). All `hr.*` + resolve `None` today. +- **Layer-2 alignment axioms** for the remaining `None` classes that do + NOT get a new family (they stay cross-cutting): `stock.*` (stock.move, + stock.rule, stock.warehouse.orderpoint), `account.analytic.distribution.model`, + `account.account.tag`. These get `owl:equivalentClass` / `rdfs:subClassOf` + rows in `data/ontologies/odoo/alignment/` pointing at existing pivots + (e.g. `stock.move`→`schema:MoveAction`-style, analytic→`fibo:CostCenter`) + so `resolve_odoo` can land them on an existing family rather than minting + one. Where no honest pivot exists, the class stays `None` and the plan + records WHY (genuinely cross-cutting, needs a runtime SPO-G edge not a + static family). + +### Group C — StyleCluster wiring + +`FamilyEntry` carries `dolce_marker` + `owl_characteristics` but NOT the +default `StyleCluster`. SAVANTS.md inherits the style from the family. Add a +`default_style: StyleCluster` field to `FamilyEntry` (or a sidecar +`family_style(OgitFamily) -> StyleCluster` map if adding a field churns too +many call sites — decide in review). Seed: +0x60→Direct, 0x61→Analytical, 0x62→Analytical, 0x63→Analytical, +0x80→Empathic, 0x81→Direct, 0x90→Empathic. + +### Group A — Reasoner impls (largest, scoped here, built in a follow-up PR) + +The 25 savants collapse onto **5 `ReasoningKind` discriminants** × +**5 `InferenceType` strategies**. Decision to pin: implement **one +`Reasoner` per `ReasoningKind`** (CustomerCategory, PostingAnomaly, +NextBestAction, InvoiceCompleteness, Other) that dispatches on +`ReasoningContext.evidence` + the resolved family's style, rather than 25 +separate impls. Each `reason()` selects its `QueryStrategy` from +`InferenceType::default_strategy()` (already mapped: +Deduction→CamExact, Induction→CamWide, Abduction→DnTreeFull, +Revision→BundleInto, Synthesis→BundleAcross) and fuses evidence via the +savant's `SemiringChoice` (NarsTruth = the common case). Conclusion type: +a truth-weighted `SavantConclusion { suggestion, confidence: NarsTruth, +rationale }` that woa-rs applies as a **suggestion only**, never an +un-guarded write (Iron Rule 7, verhaltens-bewahrend). Home crate: +`lance-graph-callcenter` (BBB-allowed, already owns `odoo_alignment.rs`). + +## Deliverables + +- **D-ODOO-SAV-1** (Group B): `0x63 ProductCatalog` + `0x90 HRFoundation` + family constants + seed rows in `odoo_alignment.rs`; `data/family_registry.ttl` + family declarations; product.*/hr.* seed rows. Tests: resolve + product.template→0x63, hr.employee→0x90, end-to-end against live table. +- **D-ODOO-SAV-2** (Group B): Layer-2 alignment axioms TTL for stock.*, + analytic.distribution.model, account.account.tag — land on an existing + pivot where honest, else documented `None`-with-rationale. Tests: the + newly-aligned classes resolve; the genuinely-cross-cutting ones still + return `None` with a recorded reason. +- **D-ODOO-SAV-3** (Group C): `StyleCluster` per family. Field-or-sidecar + decision in review; seed the 7 families; test each family→cluster. +- **D-ODOO-SAV-4** (Group A): `SavantConclusion` + 5 `Reasoner` impls in + `lance-graph-callcenter`, dispatching on `ReasoningKind` + evidence + + family style. Per-impl tests with synthetic `EvidenceRef` batches. + **Gated on a review pass of the dispatch shape.** Its own PR. + +## Execution + +D-ODOO-SAV-1/2/3 are additive + low-risk → ship together in the first PR +(this session). D-ODOO-SAV-4 (Reasoner impls) is the architectural piece → +scoped here, built in a follow-up PR after a `/code-review` pass on the +dispatch shape. Board-hygiene: this plan + INTEGRATION_PLANS prepend land +in the same commit as D-ODOO-SAV-1. + +## Invariants + +- Option B holds: odoo classes INHERIT existing slots via OWL pivot; new + families 0x63/0x90 are genuine new basins (product catalogue, HR), not + per-class mints. `None` stays `None` when no honest pivot exists. +- Public OWL/RDF sources stay pristine — alignment axioms are NEW TTL in + `data/ontologies/odoo/alignment/`, not edits to upstream vocabs. +- Savant = Layer-2 role catalogue (I-VSA-IDENTITIES): identity in the + family table, content (evidence/rules) in Arrow/SPO, never bundled. +- Reasoner output is a suggestion; the deterministic guard stays in woa-rs + (verhaltens-bewahrend). lance-graph implements the ambiguous core only. +- No brain-crate in the customer binary (Iron Rule 1): impls live in + `lance-graph-callcenter` behind the contract `Reasoner` trait. diff --git a/crates/lance-graph-callcenter/data/family_registry.ttl b/crates/lance-graph-callcenter/data/family_registry.ttl index bed158b5..ff12c458 100644 --- a/crates/lance-graph-callcenter/data/family_registry.ttl +++ b/crates/lance-graph-callcenter/data/family_registry.ttl @@ -161,6 +161,16 @@ ogit:MRORepair ogit.meta:superDomain "WorkOrderBilling" ; ogit.meta:familyId "99"^^xsd:unsignedByte . +# ProductCatalog 0x64 — product catalogue + pricelist + UoM basin (odoo +# richness harvest, plan odoo-savant-reasoners-v1 D-ODOO-SAV-1). NOTE: the +# woa-rs SAVANTS.md proposal named 0x63 for this, but 0x63=99 is already +# MRORepair above; the lance-graph authoritative registry assigns the next +# free commercial-cluster byte 0x64=100 instead. +ogit:ProductCatalog + a ogit:FamilyNamespace ; + ogit.meta:superDomain "WorkOrderBilling" ; + ogit.meta:familyId "100"^^xsd:unsignedByte . + # ── OSINT basins 0x70..=0x74 ───────────────────────────────────────────────── ogit:OsintMaltego @@ -198,6 +208,16 @@ ogit:SmbFoundryTaxDeclaration ogit.meta:superDomain "SMB" ; ogit.meta:familyId "130"^^xsd:unsignedByte . +# ── HR basin 0x90 ──────────────────────────────────────────────────────────── +# HRFoundation 0x90 — employee / org / job / base-contract basin (odoo +# richness harvest, plan odoo-savant-reasoners-v1 D-ODOO-SAV-1). hr.* resolve +# None today; this basin gives them a home. Payroll ENGINE is odoo Enterprise +# (absent) — only the hr base data/structure is aligned here. +ogit:HRFoundation + a ogit:FamilyNamespace ; + ogit.meta:superDomain "HR" ; + ogit.meta:familyId "144"^^xsd:unsignedByte . + # ── SMB BSON-shape basins 0xA0..=0xAD ──────────────────────────────────────── # OQ-4 resolution (locked 2026-05-13): ogit.SMB.bson: sub-namespace carries the # 14 BSON-shape entities. Slot range 0xA0..=0xAD (160..=173) is unconflicted diff --git a/crates/lance-graph-callcenter/src/odoo_alignment.rs b/crates/lance-graph-callcenter/src/odoo_alignment.rs index 565ffef2..effd34ab 100644 --- a/crates/lance-graph-callcenter/src/odoo_alignment.rs +++ b/crates/lance-graph-callcenter/src/odoo_alignment.rs @@ -44,6 +44,17 @@ pub const FAMILY_SMB_ACCOUNTING: OgitFamily = OgitFamily(0x62); pub const FAMILY_SMB_FOUNDRY_CUSTOMER: OgitFamily = OgitFamily(0x80); /// `ogit:SmbFoundryInvoice` — invoice / transaction document. familyId 129. pub const FAMILY_SMB_FOUNDRY_INVOICE: OgitFamily = OgitFamily(0x81); +/// `ogit:ProductCatalog` — product catalogue + pricelist + UoM. familyId 100. +/// +/// NOTE: the woa-rs `SAVANTS.md` proposal named `0x63` for this basin, but +/// `0x63` (=99) is already `ogit:MRORepair` in `data/family_registry.ttl`. +/// The lance-graph authoritative registry therefore assigns the next free +/// commercial-cluster byte `0x64` (=100). (Plan odoo-savant-reasoners-v1 +/// D-ODOO-SAV-1; deviation from proposal documented there.) +pub const FAMILY_PRODUCT_CATALOG: OgitFamily = OgitFamily(0x64); +/// `ogit:HRFoundation` — employee / org / job / base-contract. familyId 144. +/// Base HR data only; payroll engine is odoo Enterprise (absent). +pub const FAMILY_HR_FOUNDATION: OgitFamily = OgitFamily(0x90); // ═══════════════════════════════════════════════════════════════════════════ // OwlPivot — the resolved owl:equivalentClass landing (leg 1 output) @@ -107,12 +118,58 @@ pub fn dolce_odoo(class: &str) -> DolceMarker { { return DolceMarker::Abstract; } + if class.starts_with("uom.") { + // Units of measure are Qualities in DOLCE (dimensions of comparison). + return DolceMarker::Quality; + } if class.starts_with("res.") || class.ends_with(".account") || class.starts_with("product.") { return DolceMarker::Endurant; } DolceMarker::Unknown } +// ═══════════════════════════════════════════════════════════════════════════ +// family_default_style — inherited ThinkingStyle cluster per OGIT family +// (Plan odoo-savant-reasoners-v1 D-ODOO-SAV-3). SAVANTS.md inherits each +// savant's style from its family; this is the authoritative family→cluster map. +// ═══════════════════════════════════════════════════════════════════════════ + +pub use lance_graph_contract::thinking::StyleCluster; + +/// The default `StyleCluster` a Savant inherits from its OGIT family. Pinned +/// per `SAVANTS.md` § "OGIT family map ... and inherited style": +/// +/// - `0x60` WorkOrderCore → Direct (task execution) +/// - `0x61` BillingCore → Analytical (pricing math) +/// - `0x62` SMBAccounting → Analytical (ledger reasoning) +/// - `0x64` ProductCatalog → Analytical (catalogue / pricing) +/// - `0x80` SmbFoundryCustomer → Empathic (relationship + trust) +/// - `0x81` SmbFoundryInvoice → Direct (transaction processing) +/// - `0x90` HRFoundation → Empathic (people / org) +/// +/// Returns `None` for families with no pinned default (caller proposes a +/// cluster with rationale, per the BRIEFING delegation discipline). +pub fn family_default_style(family: OgitFamily) -> Option { + match family.raw() { + 0x60 => Some(StyleCluster::Direct), + 0x61 => Some(StyleCluster::Analytical), + 0x62 => Some(StyleCluster::Analytical), + 0x64 => Some(StyleCluster::Analytical), + 0x80 => Some(StyleCluster::Empathic), + 0x81 => Some(StyleCluster::Direct), + 0x90 => Some(StyleCluster::Empathic), + _ => None, + } +} + +/// Both legs + style: resolve an odoo class all the way to the +/// `StyleCluster` its inherited family carries. `None` when the class is +/// unmapped or its family has no pinned default. O(1). +pub fn resolve_odoo_style(class: &str) -> Option { + let pivot = resolve_odoo(class)?; + family_default_style(pivot.family) +} + // ═══════════════════════════════════════════════════════════════════════════ // Seed — the realized rows the BRIEFING enumerates (res.partner, account.*, // product.*, SKR). Each row owns a stable slot within its foundry family. @@ -221,6 +278,86 @@ static ODOO_SEED: &[OdooSeedRow] = &[ label_uri: "ogit.Billing:ProductVariant", provenance: "odoo product.product (variant) =owl:equivalentClass=> schema:Product", }, + // ── ProductCatalog 0x64 — catalogue STRUCTURE (pricing + measurement) ── + // product.template / product.product stay on BillingCore (0x61): they are + // billable ITEMS. The pricelist / UoM concepts are catalogue STRUCTURE and + // get their own basin so L8's PricelistAssignmentAgent has a real family + // instead of None. (Deviation from SAVANTS.md noted on FAMILY_PRODUCT_CATALOG.) + OdooSeedRow { + odoo_class: "product.pricelist", + pivot_uri: "schema:PriceSpecification", + family: FAMILY_PRODUCT_CATALOG, + slot: 1, + kind: SchemaKind::Entity, + dolce: DolceMarker::Abstract, + label_uri: "ogit.ProductCatalog:Pricelist", + provenance: "odoo product.pricelist =owl:equivalentClass=> schema:PriceSpecification", + }, + OdooSeedRow { + odoo_class: "product.pricelist.item", + pivot_uri: "schema:UnitPriceSpecification", + family: FAMILY_PRODUCT_CATALOG, + slot: 2, + kind: SchemaKind::Entity, + dolce: DolceMarker::Abstract, + label_uri: "ogit.ProductCatalog:PricelistRule", + provenance: "odoo product.pricelist.item =owl:equivalentClass=> schema:UnitPriceSpecification", + }, + OdooSeedRow { + // uom.uom ties into the QUDT Foundation namespace (qudt:Unit) — the + // measurement spine the bO-4 hydrator already loads. + odoo_class: "uom.uom", + pivot_uri: "qudt:Unit", + family: FAMILY_PRODUCT_CATALOG, + slot: 3, + kind: SchemaKind::Entity, + dolce: DolceMarker::Quality, + label_uri: "ogit.ProductCatalog:UnitOfMeasure", + provenance: "odoo uom.uom =owl:equivalentClass=> qudt:Unit", + }, + // ── HRFoundation 0x90 — employee / org / job / base-contract ── + // Payroll ENGINE is odoo Enterprise (absent): only base HR data aligns here. + OdooSeedRow { + odoo_class: "hr.employee", + pivot_uri: "vcard:Individual", + family: FAMILY_HR_FOUNDATION, + slot: 1, + kind: SchemaKind::Entity, + dolce: DolceMarker::Endurant, + label_uri: "ogit.HR:Employee", + provenance: "odoo hr.employee =owl:equivalentClass=> vcard:Individual", + }, + OdooSeedRow { + odoo_class: "hr.department", + pivot_uri: "org:OrganizationalUnit", + family: FAMILY_HR_FOUNDATION, + slot: 2, + kind: SchemaKind::Entity, + dolce: DolceMarker::Endurant, + label_uri: "ogit.HR:Department", + provenance: "odoo hr.department =owl:equivalentClass=> org:OrganizationalUnit", + }, + OdooSeedRow { + odoo_class: "hr.job", + pivot_uri: "org:Role", + family: FAMILY_HR_FOUNDATION, + slot: 3, + kind: SchemaKind::Entity, + dolce: DolceMarker::Abstract, + label_uri: "ogit.HR:Job", + provenance: "odoo hr.job =owl:equivalentClass=> org:Role", + }, + OdooSeedRow { + // Base employment contract only — payroll computation is Enterprise. + odoo_class: "hr.contract", + pivot_uri: "fibo:Contract", + family: FAMILY_HR_FOUNDATION, + slot: 4, + kind: SchemaKind::Entity, + dolce: DolceMarker::Abstract, + label_uri: "ogit.HR:EmploymentContract", + provenance: "odoo hr.contract (base, payroll is Enterprise/absent) =owl:equivalentClass=> fibo:Contract", + }, ]; // ═══════════════════════════════════════════════════════════════════════════ @@ -358,10 +495,12 @@ mod tests { #[test] fn unmapped_classes_return_none() { // The "needs a Layer-2 alignment axiom" signal — NOT a minted family. + // NOTE: hr.employee USED to be here but D-ODOO-SAV-1 mapped it to + // HRFoundation (0x90); these are the classes that genuinely stay None. assert!(resolve_odoo("stock.move").is_none()); assert!(resolve_odoo("sale.order").is_none()); - assert!(resolve_odoo("hr.employee").is_none()); assert!(resolve_odoo("account.reconcile.model").is_none()); + assert!(resolve_odoo("account.analytic.distribution.model").is_none()); } #[test] @@ -417,4 +556,149 @@ mod tests { let table = OgitFamilyTable::empty(FAMILY_SMB_ACCOUNTING); assert!(resolve_odoo_to_family("account.account", &table).is_none()); } + + // ── D-ODOO-SAV-1: ProductCatalog (0x64) + HRFoundation (0x90) basins ── + + #[test] + fn new_family_bytes_are_free_in_registry() { + // ProductCatalog 0x64=100 (NOT the proposed 0x63=99 which is + // MRORepair); HRFoundation 0x90=144. Both must be the values the + // family_registry.ttl rows added in D-ODOO-SAV-1 carry. + assert_eq!(FAMILY_PRODUCT_CATALOG.raw(), 100); + assert_eq!(FAMILY_HR_FOUNDATION.raw(), 144); + // Guard the deviation: 0x63 (=99) is MRORepair, NOT ProductCatalog. + assert_ne!(FAMILY_PRODUCT_CATALOG.raw(), 0x63); + } + + #[test] + fn product_catalogue_structure_resolves_to_productcatalog() { + // Pricing + UoM STRUCTURE lands on ProductCatalog (0x64)... + let pl = resolve_odoo("product.pricelist").expect("pricelist seeded"); + assert_eq!(pl.family, FAMILY_PRODUCT_CATALOG); + assert_eq!(pl.pivot_uri, "schema:PriceSpecification"); + assert_eq!(pl.dolce, DolceMarker::Abstract); + + let item = resolve_odoo("product.pricelist.item").expect("pricelist item seeded"); + assert_eq!(item.family, FAMILY_PRODUCT_CATALOG); + + let uom = resolve_odoo("uom.uom").expect("uom seeded"); + assert_eq!(uom.family, FAMILY_PRODUCT_CATALOG); + assert_eq!(uom.pivot_uri, "qudt:Unit"); // ties into the bO-4 QUDT namespace + assert_eq!(uom.dolce, DolceMarker::Quality); + } + + #[test] + fn billable_items_stay_on_billing_core() { + // Deviation guard: product.template / product.product are billable + // ITEMS and MUST stay on BillingCore (0x61), NOT move to ProductCatalog. + assert_eq!( + resolve_odoo("product.template").unwrap().family, + FAMILY_BILLING_CORE + ); + assert_eq!( + resolve_odoo("product.product").unwrap().family, + FAMILY_BILLING_CORE + ); + } + + #[test] + fn hr_base_classes_resolve_to_hrfoundation() { + // hr.* resolved None before D-ODOO-SAV-1; now they land on 0x90. + let emp = resolve_odoo("hr.employee").expect("hr.employee seeded"); + assert_eq!(emp.family, FAMILY_HR_FOUNDATION); + assert_eq!(emp.pivot_uri, "vcard:Individual"); + assert_eq!(emp.dolce, DolceMarker::Endurant); + + assert_eq!( + resolve_odoo("hr.department").unwrap().pivot_uri, + "org:OrganizationalUnit" + ); + assert_eq!(resolve_odoo("hr.job").unwrap().family, FAMILY_HR_FOUNDATION); + assert_eq!( + resolve_odoo("hr.contract").unwrap().pivot_uri, + "fibo:Contract" + ); + } + + #[test] + fn end_to_end_productcatalog_live_table() { + let mut table = OgitFamilyTable::empty(FAMILY_PRODUCT_CATALOG); + seed_family_table(&mut table); + assert!(!table.is_empty()); + let (fam, slot) = resolve_odoo_to_family("uom.uom", &table).expect("live uom slot"); + assert_eq!(fam, FAMILY_PRODUCT_CATALOG); + assert_eq!(slot, 3); + } + + #[test] + fn end_to_end_hrfoundation_live_table() { + let mut table = OgitFamilyTable::empty(FAMILY_HR_FOUNDATION); + seed_family_table(&mut table); + let entry = resolve_odoo_entry("hr.employee", &table).expect("live hr.employee entry"); + assert_eq!(entry.label_uri, "ogit.HR:Employee"); + assert_eq!(entry.dolce_marker, DolceMarker::Endurant); + } + + #[test] + fn classes_still_none_after_d1() { + // D-ODOO-SAV-1 added ProductCatalog + HRFoundation only. The genuinely + // cross-cutting classes stay None (Layer-2 axioms in D-ODOO-SAV-2, but + // those record semantics in TTL, not a new foundry family). + assert!(resolve_odoo("stock.move").is_none()); + assert!(resolve_odoo("account.analytic.distribution.model").is_none()); + assert!(resolve_odoo("account.account.tag").is_none()); + } + + // ── D-ODOO-SAV-3: family → inherited StyleCluster ── + + #[test] + fn family_default_style_pins_savant_clusters() { + assert_eq!( + family_default_style(FAMILY_BILLING_CORE), + Some(StyleCluster::Analytical) + ); + assert_eq!( + family_default_style(FAMILY_SMB_ACCOUNTING), + Some(StyleCluster::Analytical) + ); + assert_eq!( + family_default_style(FAMILY_PRODUCT_CATALOG), + Some(StyleCluster::Analytical) + ); + assert_eq!( + family_default_style(FAMILY_SMB_FOUNDRY_CUSTOMER), + Some(StyleCluster::Empathic) + ); + assert_eq!( + family_default_style(FAMILY_SMB_FOUNDRY_INVOICE), + Some(StyleCluster::Direct) + ); + assert_eq!( + family_default_style(FAMILY_HR_FOUNDATION), + Some(StyleCluster::Empathic) + ); + // Unpinned family → None (caller proposes with rationale). + assert_eq!(family_default_style(OgitFamily(0xFE)), None); + } + + #[test] + fn resolve_odoo_style_chains_class_to_cluster() { + // res.partner → SmbFoundryCustomer (0x80) → Empathic. + assert_eq!(resolve_odoo_style("res.partner"), Some(StyleCluster::Empathic)); + // product.pricelist → ProductCatalog (0x64) → Analytical. + assert_eq!( + resolve_odoo_style("product.pricelist"), + Some(StyleCluster::Analytical) + ); + // hr.employee → HRFoundation (0x90) → Empathic. + assert_eq!(resolve_odoo_style("hr.employee"), Some(StyleCluster::Empathic)); + // unmapped class → None. + assert_eq!(resolve_odoo_style("stock.move"), None); + } + + #[test] + fn uom_dolce_suffix_rule() { + assert_eq!(dolce_odoo("uom.uom"), DolceMarker::Quality); + assert_eq!(dolce_odoo("uom.category"), DolceMarker::Quality); + } } diff --git a/crates/lance-graph-callcenter/tests/zone-poison-fixtures/.gitignore b/crates/lance-graph-callcenter/tests/zone-poison-fixtures/.gitignore new file mode 100644 index 00000000..3403b7ea --- /dev/null +++ b/crates/lance-graph-callcenter/tests/zone-poison-fixtures/.gitignore @@ -0,0 +1,8 @@ +# Build artifacts generated when this test-fixture crate compiles during +# `cargo test -p lance-graph-callcenter`. The fixture's sources (Cargo.toml, +# build.rs, src/) are tracked; its lock + target are not (no fixture lock is +# tracked anywhere in the repo, and the workspace root Cargo.lock is the +# tracked one). A fixture-scoped ignore keeps the tree clean without +# un-tracking the workspace lock at root. +Cargo.lock +/target/ diff --git a/data/ontologies/odoo/alignment/odoo-to-foundation.ttl b/data/ontologies/odoo/alignment/odoo-to-foundation.ttl new file mode 100644 index 00000000..25f353ff --- /dev/null +++ b/data/ontologies/odoo/alignment/odoo-to-foundation.ttl @@ -0,0 +1,84 @@ +# Odoo → Foundation/schema/qudt/vcard/org/skos alignment axioms. +# +# Plan odoo-savant-reasoners-v1 D-ODOO-SAV-1 (ProductCatalog + HRFoundation +# basins) and D-ODOO-SAV-2 (Layer-2 axioms for the classes that resolve None +# in odoo_alignment.rs). These are NEW axioms authored on the lance-graph +# side — the upstream public OWL vocabularies (schema.org, QUDT, vCard, W3C +# org, SKOS) are referenced, never modified. +# +# Pivot choices match the Rust ODOO_SEED rows in +# crates/lance-graph-callcenter/src/odoo_alignment.rs. + +@prefix odoo: . +@prefix owl: . +@prefix rdfs: . +@prefix schema: . +@prefix qudt: . +@prefix vcard: . +@prefix org: . +@prefix skos: . +@prefix fibo-fnd-agr-ctr: . + +# ── ProductCatalog 0x64 — catalogue STRUCTURE (pricing + measurement) ──────── +# product.template / product.product stay on schema:Product (BillingCore 0x61, +# see odoo-to-fibo.ttl). The pricing + UoM concepts are catalogue STRUCTURE. + +odoo:product.pricelist + owl:equivalentClass schema:PriceSpecification ; + rdfs:comment "ProductCatalog 0x64 slot 1 — pricing rule container." . + +odoo:product.pricelist.item + owl:equivalentClass schema:UnitPriceSpecification ; + rdfs:subClassOf odoo:product.pricelist . + +odoo:uom.uom + owl:equivalentClass qudt:Unit ; + rdfs:comment "ProductCatalog 0x64 slot 3 — ties into the bO-4 QUDT measurement spine." . + +# ── HRFoundation 0x90 — employee / org / job / base-contract ───────────────── +# Base HR data only; payroll ENGINE is odoo Enterprise (absent). + +odoo:hr.employee + owl:equivalentClass vcard:Individual . + +odoo:hr.department + owl:equivalentClass org:OrganizationalUnit . + +odoo:hr.job + owl:equivalentClass org:Role . + +odoo:hr.contract + owl:equivalentClass fibo-fnd-agr-ctr:Contract ; + rdfs:comment "Base employment contract only — payroll computation is odoo Enterprise/absent." . + +# ── D-ODOO-SAV-2 — Layer-2 axioms for classes that resolve None in Rust ────── +# +# These do NOT get a foundry family (Option B: None stays None when no honest +# foundry pivot exists). The axioms below record the SEMANTIC alignment so a +# future runtime SPO-G edge / reasoner can use it, without minting a family. + +# account.account.tag IS an honest classification concept → SKOS. +# (Still None in resolve_odoo_to_family because SKOS is a Foundation namespace, +# not a foundry family — but the semantic alignment is real and recorded.) +odoo:account.account.tag + owl:equivalentClass skos:Concept ; + rdfs:comment "Reporting/tax classification tag on accounts — a SKOS Concept, not a foundry entity. Stays None for family resolution by design." . + +# stock.* — genuinely cross-cutting inventory events with no honest foundry +# pivot today. Documented None (NOT minted). All L7/L13 stock savants carry +# family=None in SAVANTS.md and reason without an inherited style until a +# runtime SPO-G alignment lands. +odoo:stock.move + rdfs:comment "None by design: inventory move event, no honest foundry pivot. Reasoner uses family=None (proposed cluster in the savant doc), not a minted family." . + +odoo:stock.rule + rdfs:comment "None by design: procurement routing rule — cross-cutting, no foundry pivot." . + +odoo:stock.warehouse.orderpoint + rdfs:comment "None by design: reordering rule — cross-cutting, no foundry pivot." . + +# account.analytic.distribution.model — cost-centre distribution RULE. +# Analytic accounting (Kostenstellen) has no honest FIBO/foundry pivot; stays +# None. The AnalyticModelScorer savant is family=None in SAVANTS.md. +odoo:account.analytic.distribution.model + rdfs:comment "None by design: cost-centre (Kostenstelle) distribution rule, no honest foundry pivot. Layer-2 axiom records the gap; no family minted." .