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
33 changes: 33 additions & 0 deletions .claude/board/AGENT_LOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -426,3 +426,36 @@ W11 [2026-05-14T12:29] test-plan-unification: spec at .claude/specs/sprint-10-te
**Process note:** user explicitly called out my prior context-reset framing — corrected via Explore agent research before writing. All 8 docs grounded in shipped source (file:line refs throughout) or referenced plan documents.

---

---

## [odoo-seam-bO] [IN PR] D-ODOO-1 odoo hydrator + DOLCE classifier (branch claude/lance-graph-att-activate-Jd2iZ)

**D-id:** D-ODOO-1 — first concrete increment of the odoo → lance-graph-ontology integration (four-way alignment seam, Layer 1 + Layer 2 seed). Adds the odoo OWL hydrator, the odoo DOLCE suffix classifier (Seam decision 2, own module per Open-question 3), seed + alignment TTLs, and an `ODOO_V1` OGIT slot. Honors Seam decision 1 / Option B: odoo gets NO new CAM family — it inherits FIBO/SKR slots via `owl:equivalentClass` alignment axioms.

**Worker:** general-purpose agent (Opus). Spec: `woa-rs/.claude/reference/four_way_alignment_seam.md`.

**OGIT-slot decision: (a) — manifest YAML.** Added `modules/odoo/manifest.yaml` (`ogit_g: ODOO`, `inherits_from: fibofnd`, 17 entity_types at u16=4300..4316, no collision — highest prior code was 4204) and registered `("ODOO", 50)` in `crates/lance-graph-contract/build.rs` CANONICAL_SLOTS. Verified: `cargo build -p lance-graph-contract` regenerates `OUT_DIR/ogit_namespace.rs` with `pub const ODOO_V1: (u32, u32) = (50, 1);`. Slot 50 is fresh (prior slots: 0-6, 10-14, 20-21, 30-31, 40-42).

**Files added:**
- `data/ontologies/odoo/odoo-core.ttl` — 17 core classes as owl:Class + rdfs:label + rdfs:subClassOf (res.partner{.Company,.Individual}, account.{move,move.line,account,tax,journal}, product.{product,template,category}, stock.{move,picking}, mail.{message,template}, hr.{employee,attendance}). Namespace `odoo: <https://ada.world/onto/odoo#>`.
- `data/ontologies/odoo/alignment/odoo-to-fibo.ttl` — owl:equivalentClass/equivalentProperty per seam worked example (res.partner.Company→fibo:LegalEntity, res.partner.Individual→vcard:Individual, account.move→fibo:FinancialTransaction + account.move.Invoice→ubl:Invoice dual-nature per Open-question 5, account.account→fibo:Account, product.template→schema:Product; name→foaf:name, vat→fibo:hasTaxIdentifier).
- `data/ontologies/odoo/alignment/odoo-to-skr.ttl` — odoo accounting → SKR03/SKR04 chart pivots (account.account→skr:Konto, account.tax→skr:Steuersatz, account.journal→skr:Journal, code→kontonummer).
- `crates/lance-graph-ontology/src/hydrators/odoo.rs` — `hydrate_odoo(registry)` (canonical seed + alignment overlays) + `hydrate_odoo_from(paths, registry)` (test/multi-file). `g: OGIT::ODOO_V1.0`, `inherits_from: Some(OGIT::FIBOFND_V1.0)`, edge whitelist {rdfs:subClassOf, owl:equivalentClass, rdfs:subPropertyOf, owl:equivalentProperty}. Doc-commented as Layer-1 odoo extraction source.
- `crates/lance-graph-ontology/src/hydrators/dolce_odoo.rs` — `pub fn classify_odoo(iri: &str) -> DolceCategory` + `pub enum DolceCategory { Endurant, Perdurant, Quality, AbstractEntity }` (doc-noted: canonical DUL renames Endurant→Object / Perdurant→Event). Suffix heuristics + product.template Endurant special-case + default Endurant per seam §"Seam decision 2".
- `crates/lance-graph-ontology/tests/odoo_hydrator_smoke.rs` — 3 tests (seed hydrate Ok + non-zero count + L1 invariants; edge whitelist; canonical-paths incl. alignment TTL parse-validation via fibo:LegalEntity interning).
- `crates/lance-graph-ontology/tests/odoo_dolce_classifier.rs` — 4 tests incl. the full 21-row seam matrix.

**Files modified:**
- `crates/lance-graph-ontology/src/hydrators/mod.rs` — `pub mod odoo; pub mod dolce_odoo;` + re-exports.
- `crates/lance-graph-ontology/src/lib.rs` — re-export `classify_odoo, DolceCategory, hydrate_odoo, hydrate_odoo_from`.

**Tests:** `cargo test -p lance-graph-ontology` → 127 passed / 0 failed (all binaries; +7 new odoo tests, +4 new lib unit tests). `cargo test -p lance-graph-contract` → 449 passed / 0 failed (build.rs change verified).

**Bug caught + fixed during impl:** the seam's reference classifier snippet only lists `.move` in PERDURANT_SUFFIXES, but `account.move.line` ends with `.line` → fell through to default Endurant, contradicting the seam matrix row (`account.move.line → Perdurant`). Added explicit `.move.line` suffix (a line is a fact within the move event). Matches lance-graph-callcenter::odoo_alignment::dolce_odoo's handling.

**Note — prior art:** `lance-graph-callcenter::odoo_alignment` already ships a parallel `dolce_odoo()` + `DolceMarker` + `ODOO_SEED` static table (Option B family bytes). This D-ODOO-1 work is the lance-graph-ONTOLOGY side (TTL hydration into the OntologyRegistry, separate crate, distinct `DolceCategory` enum per the task spec). The two are consistent (same pivots, same Option-B doctrine) but not yet unified; cross-crate dedup is a possible follow-up.

**Outcome:** D-ODOO-1 ready for review. Workspace compiles; both touched crates green. NOT pushed (orchestrator reviews + pushes).

---
28 changes: 28 additions & 0 deletions .claude/board/LATEST_STATE.md
Original file line number Diff line number Diff line change
Expand Up @@ -407,3 +407,31 @@ Sprint-2 W7 → ndarray; sprint-3 W9 → ada-consciousness. Both corrected via m
- `.claude/board/sprint-log-3/{SPRINT_LOG.md,agents/agent-W1..W12.md,meta-1-review.md,sprint-summary.md}`

PR sequence: #360 → #361 → post-#360 substrate-sweep (this PR).

---

## APPEND-ONLY annotation — D-ODOO-1 odoo hydrator (2026-05-27)

> Per the APPEND-ONLY governance rule, this section augments — does not edit — prior content. Treat as the new top-of-state. Branch: `claude/lance-graph-att-activate-Jd2iZ`.

### Current Contract Inventory — new entry

- **`OGIT::ODOO_V1` = (50, 1)** — new OGIT G slot (first manifest-declared slot above SKR03BAU=42). Source: `modules/odoo/manifest.yaml` (`ogit_g: ODOO`, `inherits_from: fibofnd`, 17 entity_types u16=4300..4316). Registered in `crates/lance-graph-contract/build.rs` CANONICAL_SLOTS as `("ODOO", 50)`; build regenerates `OUT_DIR/ogit_namespace.rs` accordingly.

### New module surface (`lance-graph-ontology`)

- **`hydrators::odoo`** — Layer-1 odoo extraction hydrator (four-way alignment seam). `hydrate_odoo(registry)` + `hydrate_odoo_from(paths, registry)`; `inherits_from: Some(OGIT::FIBOFND_V1.0)`; edge whitelist {rdfs:subClassOf, owl:equivalentClass, rdfs:subPropertyOf, owl:equivalentProperty}. Re-exported from `lib.rs`.
- **`hydrators::dolce_odoo`** — odoo DOLCE suffix classifier (Seam decision 2, own module per Open-question 3). `pub fn classify_odoo(iri: &str) -> DolceCategory` + `pub enum DolceCategory { Endurant, Perdurant, Quality, AbstractEntity }`. Re-exported from `lib.rs`. (Doc-noted: canonical DUL renames Endurant→Object / Perdurant→Event.)

### New data artifacts

- `data/ontologies/odoo/odoo-core.ttl` — 17 odoo core classes (`odoo: <https://ada.world/onto/odoo#>`).
- `data/ontologies/odoo/alignment/odoo-to-fibo.ttl` + `odoo-to-skr.ttl` — Layer-2 `owl:equivalentClass`/`owl:equivalentProperty` alignment axioms (Seam decision 1 / Option B: odoo inherits existing FIBO/SKR slots, no new CAM family).

### Tests

`cargo test -p lance-graph-ontology` → 127 passed / 0 failed (+7 odoo integration tests across `tests/odoo_hydrator_smoke.rs` + `tests/odoo_dolce_classifier.rs`, incl. the full 21-row seam classifier matrix; +4 lib unit tests). `cargo test -p lance-graph-contract` → 449 passed / 0 failed.

### Relationship to prior art

`lance-graph-callcenter::odoo_alignment` already ships a parallel `dolce_odoo()` + `DolceMarker` + `ODOO_SEED` table. This is the ontology-side counterpart (TTL hydration into `OntologyRegistry`); consistent doctrine (Option B, same pivots), distinct crate + distinct `DolceCategory` enum per task spec. Cross-crate dedup is a possible follow-up, not done here.
8 changes: 8 additions & 0 deletions crates/lance-graph-contract/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,14 @@ const CANONICAL_SLOTS: &[(&str, u32)] = &[
// slot rather than the canonical SKR03_V1 slot so mixed consumers
// can hold both account sets in one OntologyRegistry.
("SKR03BAU", 42),
// L1 odoo extraction source (four-way alignment seam). Odoo-extracted
// business models (res.partner, account.move, product.template, …) as
// OWL classes interned via OwlHydrator. Declares `inherits_from: fibofnd`
// and reaches the financial ontology via the `owl:equivalentClass`
// alignment axioms in data/ontologies/odoo/alignment/ (Seam decision 1 /
// Option B: odoo inherits existing FIBO/SKR slots, it does NOT get its
// own CAM codebook family).
("ODOO", 50),
];

fn canonical_slot(token: &str) -> Option<u32> {
Expand Down
163 changes: 163 additions & 0 deletions crates/lance-graph-ontology/src/hydrators/dolce_odoo.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
//! Odoo DOLCE suffix classifier — Seam decision 2, in its own module.
//!
//! Per Open-question 3 of the four-way alignment seam
//! (`woa-rs/.claude/reference/four_way_alignment_seam.md`), the odoo-specific
//! DOLCE heuristics live in a separate module rather than inline in
//! `dolce.rs`, so each extraction source (odoo, FMA, SNOMED, future ones) owns
//! its own per-source heuristic logic.
//!
//! The odoo namespace uses dotted lowercase model names (`account.move`,
//! `stock.move`, `hr.attendance`) where event semantics are encoded by
//! *suffix* (`.move`, `.message`, `.attendance`). [`classify_odoo`] maps a
//! model name onto its DOLCE upper category from those suffixes, with one
//! explicit special-case (`product.template` — odoo's "template" there means
//! the master product record, an Endurant, not a config template).
//!
//! Litmus (CLAUDE.md): this is a stateless pure function with no carrier — it
//! reads a `&str` and returns a category. That is the sanctioned shape for a
//! classifier; there is no odoo-class struct to hang it on.

/// DOLCE upper categories used by the odoo suffix classifier.
///
/// These are the four DOLCE-Lite-Plus top categories. **Canonical DOLCE+DUL
/// renames `Endurant` → `Object` and `Perdurant` → `Event`** per the DUL
/// ontology header (see `dolce.rs` module docs); this enum keeps the original
/// DOLCE-Lite-Plus names because that is the vocabulary the seam doc's test
/// matrix and the `lance-graph-callcenter::super_domain::DolceMarker` seed use.
///
/// Unlike `DolceMarker` in `lance-graph-callcenter`, there is no `Unknown`
/// variant: [`classify_odoo`] always returns a concrete category (defaulting
/// to [`DolceCategory::Endurant`] for persistent stateful objects), matching
/// the seam's "Default: Endurant" rule.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum DolceCategory {
/// Persistent stateful object (DUL: `Object`). The default.
Endurant,
/// Event / occurrence that unfolds in time (DUL: `Event`).
Perdurant,
/// An attribute / rate / classification that characterises something.
Quality,
/// A reference / configuration / template — an abstract entity.
AbstractEntity,
}

/// Name suffixes indicating a Perdurant (event / occurrence).
///
/// `.move.line` precedes `.move` in spirit — a line within a move event is a
/// fact within that Perdurant (seam matrix: `account.move.line → Perdurant`).
/// It is listed explicitly because it does not end with `.move`. Order within
/// this list does not matter (any match returns Perdurant), but the comment
/// records why both are present.
const PERDURANT_SUFFIXES: &[&str] = &[
".move.line", // account.move.line — fact within the move event
".move", // account.move, stock.move, hr.leave.allocation.move
".message", // mail.message
".activity", // mail.activity
".attendance", // hr.attendance
".transition", // workflow transitions
".event", // calendar.event
".log", // any .log model
".history", // change history
".transaction", // payment.transaction
".picking", // stock.picking (logistic event)
".scrap", // stock.scrap (logistic event)
];

/// Name suffixes indicating a Quality (attribute / classification / rate).
const QUALITY_SUFFIXES: &[&str] = &[
".tag", // crm.tag, account.account.tag
".category", // product.category, res.partner.category
".type", // account.account.type, sale.order.type
".group", // res.groups, account.tax.group
".tax", // account.tax (it's a rate, a quality, not an event)
];

/// Name suffixes indicating an AbstractEntity (reference / config / template).
const ABSTRACT_SUFFIXES: &[&str] = &[
".template", // mail.template, account.chart.template
".config", // *.config.settings
".policy", // any *.policy
".rule", // account.reconcile.model rules
".formula", // hr.payroll.structure.line formulas
];

/// Classify an odoo model IRI / name onto its DOLCE upper category.
///
/// Accepts either a bare model name (`"res.partner"`) or a prefixed IRI
/// (`"odoo:res.partner"` / `"https://ada.world/onto/odoo#res.partner"`); the
/// prefix is stripped to the model name before matching.
///
/// Resolution order (first match wins):
/// 1. `product.template` special-case → [`DolceCategory::Endurant`] (odoo uses
/// "template" here for the master product record, not a config template).
/// 2. Perdurant suffix → [`DolceCategory::Perdurant`].
/// 3. Quality suffix → [`DolceCategory::Quality`].
/// 4. Abstract suffix → [`DolceCategory::AbstractEntity`].
/// 5. Default → [`DolceCategory::Endurant`] (persistent stateful object).
pub fn classify_odoo(iri: &str) -> DolceCategory {
let model = model_name(iri);

// (1) The single special-case: product.template is a master record
// (Endurant), NOT an abstract config template — even though `.template`
// is an Abstract suffix below. Must be checked before the suffix lists.
if model == "product.template" {
return DolceCategory::Endurant;
}

// (2) Perdurant — event / occurrence by suffix.
for suffix in PERDURANT_SUFFIXES {
if model.ends_with(suffix) {
return DolceCategory::Perdurant;
}
}
// (3) Quality — attribute / classification / rate by suffix.
for suffix in QUALITY_SUFFIXES {
if model.ends_with(suffix) {
return DolceCategory::Quality;
}
}
// (4) AbstractEntity — reference / config / template by suffix.
for suffix in ABSTRACT_SUFFIXES {
if model.ends_with(suffix) {
return DolceCategory::AbstractEntity;
}
}

// (5) Default: Endurant (res.partner, res.users, res.company,
// product.product, account.account, account.journal, stock.warehouse,
// crm.lead, hr.employee, …).
DolceCategory::Endurant
}

/// Strip a leading `odoo:` prefix or the full odoo namespace IRI, returning the
/// bare odoo model name.
fn model_name(iri: &str) -> &str {
if let Some(rest) = iri.strip_prefix("https://ada.world/onto/odoo#") {
return rest;
}
iri.trim_start_matches("odoo:")
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn strips_iri_prefixes() {
assert_eq!(model_name("odoo:res.partner"), "res.partner");
assert_eq!(
model_name("https://ada.world/onto/odoo#account.move"),
"account.move"
);
assert_eq!(model_name("res.partner"), "res.partner");
}

#[test]
fn classifier_handles_prefixed_iris() {
assert_eq!(classify_odoo("odoo:account.move"), DolceCategory::Perdurant);
assert_eq!(
classify_odoo("https://ada.world/onto/odoo#res.partner"),
DolceCategory::Endurant
);
}
}
4 changes: 4 additions & 0 deletions crates/lance-graph-ontology/src/hydrators/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,9 @@
//! glue + one TTL artifact.

pub mod dolce;
pub mod dolce_odoo;
pub mod fibo;
pub mod odoo;
pub mod owl;
pub mod owltime;
pub mod provo;
Expand All @@ -33,7 +35,9 @@ pub mod xsd;
pub mod zugferd;

pub use dolce::{hydrate_dolce, hydrate_dolce_from, hydrate_dolce_from_many};
pub use dolce_odoo::{classify_odoo, DolceCategory};
pub use fibo::{hydrate_fibo_be, hydrate_fibo_be_from, hydrate_fibo_fnd, hydrate_fibo_fnd_from};
pub use odoo::{hydrate_odoo, hydrate_odoo_from};
pub use owl::{ContextBundle, EntityId, HydrateErr, MetaStructureHydrator, OntologySlot, OwlHydrator};
pub use owltime::{hydrate_owltime, hydrate_owltime_from};
pub use provo::{hydrate_provo, hydrate_provo_from};
Expand Down
Loading
Loading