Skip to content

Commit eb2feb9

Browse files
authored
Merge pull request #412 from AdaWorldAPI/claude/lance-graph-att-activate-Jd2iZ
feat(ontology): odoo hydrator + DOLCE classifier (four-way alignment seam)
2 parents 7ef6c2b + df7c234 commit eb2feb9

13 files changed

Lines changed: 847 additions & 8 deletions

File tree

.claude/board/AGENT_LOG.md

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -426,3 +426,36 @@ W11 [2026-05-14T12:29] test-plan-unification: spec at .claude/specs/sprint-10-te
426426
**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.
427427

428428
---
429+
430+
---
431+
432+
## [odoo-seam-bO] [IN PR] D-ODOO-1 odoo hydrator + DOLCE classifier (branch claude/lance-graph-att-activate-Jd2iZ)
433+
434+
**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.
435+
436+
**Worker:** general-purpose agent (Opus). Spec: `woa-rs/.claude/reference/four_way_alignment_seam.md`.
437+
438+
**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).
439+
440+
**Files added:**
441+
- `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#>`.
442+
- `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).
443+
- `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).
444+
- `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.
445+
- `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".
446+
- `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).
447+
- `crates/lance-graph-ontology/tests/odoo_dolce_classifier.rs` — 4 tests incl. the full 21-row seam matrix.
448+
449+
**Files modified:**
450+
- `crates/lance-graph-ontology/src/hydrators/mod.rs``pub mod odoo; pub mod dolce_odoo;` + re-exports.
451+
- `crates/lance-graph-ontology/src/lib.rs` — re-export `classify_odoo, DolceCategory, hydrate_odoo, hydrate_odoo_from`.
452+
453+
**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).
454+
455+
**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.
456+
457+
**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.
458+
459+
**Outcome:** D-ODOO-1 ready for review. Workspace compiles; both touched crates green. NOT pushed (orchestrator reviews + pushes).
460+
461+
---

.claude/board/LATEST_STATE.md

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

409409
PR sequence: #360#361post-#360 substrate-sweep (this PR).
410+
411+
---
412+
413+
## APPEND-ONLY annotation — D-ODOO-1 odoo hydrator (2026-05-27)
414+
415+
> 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`.
416+
417+
### Current Contract Inventory — new entry
418+
419+
- **`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.
420+
421+
### New module surface (`lance-graph-ontology`)
422+
423+
- **`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`.
424+
- **`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.)
425+
426+
### New data artifacts
427+
428+
- `data/ontologies/odoo/odoo-core.ttl` — 17 odoo core classes (`odoo: <https://ada.world/onto/odoo#>`).
429+
- `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).
430+
431+
### Tests
432+
433+
`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.
434+
435+
### Relationship to prior art
436+
437+
`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.

crates/lance-graph-contract/build.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,14 @@ const CANONICAL_SLOTS: &[(&str, u32)] = &[
6161
// slot rather than the canonical SKR03_V1 slot so mixed consumers
6262
// can hold both account sets in one OntologyRegistry.
6363
("SKR03BAU", 42),
64+
// L1 odoo extraction source (four-way alignment seam). Odoo-extracted
65+
// business models (res.partner, account.move, product.template, …) as
66+
// OWL classes interned via OwlHydrator. Declares `inherits_from: fibofnd`
67+
// and reaches the financial ontology via the `owl:equivalentClass`
68+
// alignment axioms in data/ontologies/odoo/alignment/ (Seam decision 1 /
69+
// Option B: odoo inherits existing FIBO/SKR slots, it does NOT get its
70+
// own CAM codebook family).
71+
("ODOO", 50),
6472
];
6573

6674
fn canonical_slot(token: &str) -> Option<u32> {
Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
//! Odoo DOLCE suffix classifier — Seam decision 2, in its own module.
2+
//!
3+
//! Per Open-question 3 of the four-way alignment seam
4+
//! (`woa-rs/.claude/reference/four_way_alignment_seam.md`), the odoo-specific
5+
//! DOLCE heuristics live in a separate module rather than inline in
6+
//! `dolce.rs`, so each extraction source (odoo, FMA, SNOMED, future ones) owns
7+
//! its own per-source heuristic logic.
8+
//!
9+
//! The odoo namespace uses dotted lowercase model names (`account.move`,
10+
//! `stock.move`, `hr.attendance`) where event semantics are encoded by
11+
//! *suffix* (`.move`, `.message`, `.attendance`). [`classify_odoo`] maps a
12+
//! model name onto its DOLCE upper category from those suffixes, with one
13+
//! explicit special-case (`product.template` — odoo's "template" there means
14+
//! the master product record, an Endurant, not a config template).
15+
//!
16+
//! Litmus (CLAUDE.md): this is a stateless pure function with no carrier — it
17+
//! reads a `&str` and returns a category. That is the sanctioned shape for a
18+
//! classifier; there is no odoo-class struct to hang it on.
19+
20+
/// DOLCE upper categories used by the odoo suffix classifier.
21+
///
22+
/// These are the four DOLCE-Lite-Plus top categories. **Canonical DOLCE+DUL
23+
/// renames `Endurant` → `Object` and `Perdurant` → `Event`** per the DUL
24+
/// ontology header (see `dolce.rs` module docs); this enum keeps the original
25+
/// DOLCE-Lite-Plus names because that is the vocabulary the seam doc's test
26+
/// matrix and the `lance-graph-callcenter::super_domain::DolceMarker` seed use.
27+
///
28+
/// Unlike `DolceMarker` in `lance-graph-callcenter`, there is no `Unknown`
29+
/// variant: [`classify_odoo`] always returns a concrete category (defaulting
30+
/// to [`DolceCategory::Endurant`] for persistent stateful objects), matching
31+
/// the seam's "Default: Endurant" rule.
32+
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
33+
pub enum DolceCategory {
34+
/// Persistent stateful object (DUL: `Object`). The default.
35+
Endurant,
36+
/// Event / occurrence that unfolds in time (DUL: `Event`).
37+
Perdurant,
38+
/// An attribute / rate / classification that characterises something.
39+
Quality,
40+
/// A reference / configuration / template — an abstract entity.
41+
AbstractEntity,
42+
}
43+
44+
/// Name suffixes indicating a Perdurant (event / occurrence).
45+
///
46+
/// `.move.line` precedes `.move` in spirit — a line within a move event is a
47+
/// fact within that Perdurant (seam matrix: `account.move.line → Perdurant`).
48+
/// It is listed explicitly because it does not end with `.move`. Order within
49+
/// this list does not matter (any match returns Perdurant), but the comment
50+
/// records why both are present.
51+
const PERDURANT_SUFFIXES: &[&str] = &[
52+
".move.line", // account.move.line — fact within the move event
53+
".move", // account.move, stock.move, hr.leave.allocation.move
54+
".message", // mail.message
55+
".activity", // mail.activity
56+
".attendance", // hr.attendance
57+
".transition", // workflow transitions
58+
".event", // calendar.event
59+
".log", // any .log model
60+
".history", // change history
61+
".transaction", // payment.transaction
62+
".picking", // stock.picking (logistic event)
63+
".scrap", // stock.scrap (logistic event)
64+
];
65+
66+
/// Name suffixes indicating a Quality (attribute / classification / rate).
67+
const QUALITY_SUFFIXES: &[&str] = &[
68+
".tag", // crm.tag, account.account.tag
69+
".category", // product.category, res.partner.category
70+
".type", // account.account.type, sale.order.type
71+
".group", // res.groups, account.tax.group
72+
".tax", // account.tax (it's a rate, a quality, not an event)
73+
];
74+
75+
/// Name suffixes indicating an AbstractEntity (reference / config / template).
76+
const ABSTRACT_SUFFIXES: &[&str] = &[
77+
".template", // mail.template, account.chart.template
78+
".config", // *.config.settings
79+
".policy", // any *.policy
80+
".rule", // account.reconcile.model rules
81+
".formula", // hr.payroll.structure.line formulas
82+
];
83+
84+
/// Classify an odoo model IRI / name onto its DOLCE upper category.
85+
///
86+
/// Accepts either a bare model name (`"res.partner"`) or a prefixed IRI
87+
/// (`"odoo:res.partner"` / `"https://ada.world/onto/odoo#res.partner"`); the
88+
/// prefix is stripped to the model name before matching.
89+
///
90+
/// Resolution order (first match wins):
91+
/// 1. `product.template` special-case → [`DolceCategory::Endurant`] (odoo uses
92+
/// "template" here for the master product record, not a config template).
93+
/// 2. Perdurant suffix → [`DolceCategory::Perdurant`].
94+
/// 3. Quality suffix → [`DolceCategory::Quality`].
95+
/// 4. Abstract suffix → [`DolceCategory::AbstractEntity`].
96+
/// 5. Default → [`DolceCategory::Endurant`] (persistent stateful object).
97+
pub fn classify_odoo(iri: &str) -> DolceCategory {
98+
let model = model_name(iri);
99+
100+
// (1) The single special-case: product.template is a master record
101+
// (Endurant), NOT an abstract config template — even though `.template`
102+
// is an Abstract suffix below. Must be checked before the suffix lists.
103+
if model == "product.template" {
104+
return DolceCategory::Endurant;
105+
}
106+
107+
// (2) Perdurant — event / occurrence by suffix.
108+
for suffix in PERDURANT_SUFFIXES {
109+
if model.ends_with(suffix) {
110+
return DolceCategory::Perdurant;
111+
}
112+
}
113+
// (3) Quality — attribute / classification / rate by suffix.
114+
for suffix in QUALITY_SUFFIXES {
115+
if model.ends_with(suffix) {
116+
return DolceCategory::Quality;
117+
}
118+
}
119+
// (4) AbstractEntity — reference / config / template by suffix.
120+
for suffix in ABSTRACT_SUFFIXES {
121+
if model.ends_with(suffix) {
122+
return DolceCategory::AbstractEntity;
123+
}
124+
}
125+
126+
// (5) Default: Endurant (res.partner, res.users, res.company,
127+
// product.product, account.account, account.journal, stock.warehouse,
128+
// crm.lead, hr.employee, …).
129+
DolceCategory::Endurant
130+
}
131+
132+
/// Strip a leading `odoo:` prefix or the full odoo namespace IRI, returning the
133+
/// bare odoo model name.
134+
fn model_name(iri: &str) -> &str {
135+
if let Some(rest) = iri.strip_prefix("https://ada.world/onto/odoo#") {
136+
return rest;
137+
}
138+
iri.trim_start_matches("odoo:")
139+
}
140+
141+
#[cfg(test)]
142+
mod tests {
143+
use super::*;
144+
145+
#[test]
146+
fn strips_iri_prefixes() {
147+
assert_eq!(model_name("odoo:res.partner"), "res.partner");
148+
assert_eq!(
149+
model_name("https://ada.world/onto/odoo#account.move"),
150+
"account.move"
151+
);
152+
assert_eq!(model_name("res.partner"), "res.partner");
153+
}
154+
155+
#[test]
156+
fn classifier_handles_prefixed_iris() {
157+
assert_eq!(classify_odoo("odoo:account.move"), DolceCategory::Perdurant);
158+
assert_eq!(
159+
classify_odoo("https://ada.world/onto/odoo#res.partner"),
160+
DolceCategory::Endurant
161+
);
162+
}
163+
}

crates/lance-graph-ontology/src/hydrators/mod.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,9 @@
1919
//! glue + one TTL artifact.
2020
2121
pub mod dolce;
22+
pub mod dolce_odoo;
2223
pub mod fibo;
24+
pub mod odoo;
2325
pub mod owl;
2426
pub mod owltime;
2527
pub mod provo;
@@ -33,7 +35,9 @@ pub mod xsd;
3335
pub mod zugferd;
3436

3537
pub use dolce::{hydrate_dolce, hydrate_dolce_from, hydrate_dolce_from_many};
38+
pub use dolce_odoo::{classify_odoo, DolceCategory};
3639
pub use fibo::{hydrate_fibo_be, hydrate_fibo_be_from, hydrate_fibo_fnd, hydrate_fibo_fnd_from};
40+
pub use odoo::{hydrate_odoo, hydrate_odoo_from};
3741
pub use owl::{ContextBundle, EntityId, HydrateErr, MetaStructureHydrator, OntologySlot, OwlHydrator};
3842
pub use owltime::{hydrate_owltime, hydrate_owltime_from};
3943
pub use provo::{hydrate_provo, hydrate_provo_from};

0 commit comments

Comments
 (0)