Skip to content

Commit a77d9b7

Browse files
committed
feat(ar_shape): 5 sibling concepts land on real OSB+Odoo corpora — TaxPolicy / CommercialDocument / BillingParty / CurrencyPolicy / PaymentRecord (CanonicalConcept enum 1→6)
Operator "continue with opensource-billing <> odoo" (2026-06-19). Each concept promotion is gated by ≥2-curator evidence on the real harvested corpora (OSB ndjson fixture + workspace Odoo manifest), not pre-emptively added. New shared helper: - declared_classes(triples, ns) — returns class IRIs surfaced as (class, rdf:type, ogit:ObjectType). Filters out method/field IRIs. 5 sibling lexical-shape detectors: - classes_matching_commercial_document_shape_canonical Ends-with(invoice|move|order) AND NOT contains("line"). Surfaces Invoice + account_move. Filters out InvoiceLineItem / account_move_line (those are CommercialLineItem, not Document). - classes_matching_tax_policy_shape_canonical Ends-with("tax"). Surfaces Tax + account_tax. Excludes TaxGroup / account_tax_group (lowercased "taxgroup" / "account_tax_group" don't end with "tax"). - classes_matching_billing_party_shape_canonical Ends-with(client|customer|partner). Surfaces Client + res_partner. - classes_matching_currency_policy_shape_canonical Ends-with("currency"). Surfaces Currency + res_currency. - classes_matching_payment_record_shape_canonical Ends-with("payment"). Surfaces Payment + account_payment (also CreditPayment as expected sub-type; downstream ranking picks primary). 5 corpus-driven tests matching operator's naming convention: - open_source_billing_invoice_and_odoo_account_move_overlap_as_commercial_document - open_source_billing_tax_and_odoo_tax_overlap_as_tax_policy - open_source_billing_client_and_odoo_res_partner_overlap_as_billing_party - open_source_billing_currency_and_odoo_res_currency_overlap_as_currency_policy - open_source_billing_payment_and_odoo_account_payment_overlap_as_payment_record Each loads the OSB ndjson fixture (1195 triples) + workspace Odoo manifest (2.8 MB) and asserts the expected class IRI appears on each side via lexical shape on declared OGIT ObjectTypes. Plus all 14 prior tests still green → 19/19 total. Scope discipline: - Lexical class-shape is the minimal viable detector. Structural refinement (incoming OGIT canonical relations FROM line-items for TaxPolicy; outgoing has_many to line-items for CommercialDocument) is the natural follow-up — not added pre-emptively. - CanonicalConcept enum at 6 variants: CommercialLineItem, CommercialDocument, TaxPolicy, BillingParty, PaymentRecord, CurrencyPolicy. SalesOrder / SalesOrderLine / ProductOffering / FulfillmentFlow / InventoryMovement / ProjectWorkItem / BillableWorkEntry stay deferred — operator smoke target B needs a Spree harvest; target C needs Project::TimeEntry + materialization rules. Cross-repo alignment with AdaWorldAPI/OGAR#61: the merged PR introduced const CODEBOOK: &[(&str, u16)] minting stable u16 ClassIds for promoted canonical concepts (today: project, project_work_item, billable_work_entry). The ar_shape CanonicalConcept enum is the upstream candidate set — concepts get promoted into OGAR's CODEBOOK once they clear the ≥2-curator gate AND OGAR-side review. Each new variant in this commit is a candidate row for a future OGAR CODEBOOK assignment. EPIPHANIES E-OGAR-AR-SHAPE-SMOKE-4 prepended. Cross-refs: - crates/lance-graph-ontology/src/ar_shape.rs (5 detectors + declared_classes helper + 5 tests; 19/19 green) - E-OGAR-AR-SHAPE-SMOKE-3 (OGIT canonical predecessor) - E-OGAR-AR-SHAPE-SMOKE-2 (predicate-vocabulary divergence finding) - E-OGAR-AR-SHAPE-SMOKE-1 (hand-fixture predecessor) - AdaWorldAPI/OGAR#61 (upstream u16 CODEBOOK registry — candidate promotion target for each ar_shape::CanonicalConcept variant) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Xzyc27Nx3f8WC5KzwfWfjx
1 parent f8e9d48 commit a77d9b7

2 files changed

Lines changed: 297 additions & 0 deletions

File tree

.claude/board/EPIPHANIES.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -302,6 +302,30 @@ The local-scan `memberof` (#this-branch) is the **in-mailbox special case** of "
302302
**Three threads are ONE structure:** basin-tree (1→4→16…) = Morton tile pyramid (quadtree subdivision) = `perturbation-sim` L1-L4 cascade levels = the field perturbation-learning runs over. Distance = hop count = pyramid level = cascade round — **one number, three readings**.
303303

304304
**Wiring (gated on the 5+3 council for the SoA-ownership commitment):** add `row_for_member_index` to the contract (basin-node → member row); wire the EdgeBlock out-family slot → sibling basin-node via ClassView (Options 1+2); reclassify **CHAODA as a unary `node_anomaly`** (point↔manifold LFD), NOT a pairwise `DistanceMeans` — it never belonged in the hop-distance dispatch. The `node_distance(PrefixDepth)` tree-hop is already the basin-tree distance; no new means needed for the structural tier. Cross-refs: `E-PANCAKES-IS-RADIX-IS-HHTL` (radix subtree = basin), `E-COARSE-QUANTIZER-IS-SCALE-FREE-ROUTER` (basin-node = IVF coarse centroid = parent SoA), `E-CLAM-IS-THE-MANIFOLD-ENGINE` (CHAODA = unary anomaly), `E-WHT-META-AWARENESS-AND-KRONECKER-LOOKUP` (pyramid energy = the field summary), `E-GUID-IS-THE-GRAPH`, canon node 512 B, `perturbation-sim::{sketch::walsh_pyramid_energy, chaoda}`, OGAR "256×256 centroid tile" + "perturbation = (exponent, location, phase, magnitude)".
305+
## 2026-06-19 — E-OGAR-AR-SHAPE-SMOKE-4 — five more OSB↔Odoo synergies land on real corpora: TaxPolicy / CommercialDocument / BillingParty / CurrencyPolicy / PaymentRecord (lexical class-shape on OGIT-declared types); CanonicalConcept enum at 6 variants
306+
307+
**Status:** FINDING (operator-directed "continue with opensource-billing <> odoo", 2026-06-19; 19/19 ar_shape tests green including 5 new sibling-concept corpus-driven tests). Extends `E-OGAR-AR-SHAPE-SMOKE-3` (OGIT canonical + codebooks) with five more harvest-proven `CanonicalConcept` variants per the operator's named smoke targets.
308+
309+
**What landed:**
310+
311+
- `CanonicalConcept` enum extended from 1 → 6 variants. Each new variant — `CommercialDocument`, `TaxPolicy`, `BillingParty`, `PaymentRecord`, `CurrencyPolicy` — corresponds to an operator-named concept from the smoke pass spec, and each is gated by ≥2-curator evidence on the real corpora (the OSB harvest fixture + the workspace Odoo manifest), not pre-emptively added.
312+
- Shared `declared_classes(triples, ns) -> BTreeSet<String>` helper returns class IRIs that surface as `(class, rdf:type, ogit:ObjectType)` — filters out method / field IRIs.
313+
- 5 sibling lexical-shape detectors built on the helper:
314+
- `classes_matching_commercial_document_shape_canonical` — lowercase ends with `invoice`/`move`/`order` AND does NOT contain `line` (the line filter discriminates `InvoiceLineItem` / `account_move_line` from documents). Surfaces `Invoice` (OSB) + `account_move` (Odoo).
315+
- `classes_matching_tax_policy_shape_canonical` — lowercase ends with `tax`. Surfaces `Tax` + `account_tax`. Filters out `TaxGroup` / `account_tax_group` (lowercased neither ends with `tax`).
316+
- `classes_matching_billing_party_shape_canonical` — lowercase ends with `client`/`customer`/`partner`. Surfaces `Client` + `res_partner`.
317+
- `classes_matching_currency_policy_shape_canonical` — lowercase ends with `currency`. Surfaces `Currency` + `res_currency`.
318+
- `classes_matching_payment_record_shape_canonical` — lowercase ends with `payment`. Surfaces `Payment` + `account_payment` (also `CreditPayment` as expected sub-type; downstream ranking picks the primary).
319+
- 5 corpus-driven tests with names matching the operator's convention (e.g. `open_source_billing_tax_and_odoo_tax_overlap_as_tax_policy`). Each loads the OSB ndjson fixture (`tests/fixtures/osb_ruby_spo.ndjson`, 1195 triples) + the workspace Odoo manifest (`crates/lance-graph/src/graph/spo/odoo_ontology.spo.ndjson`, 2.8 MB) and asserts the expected class IRI appears on each side.
320+
321+
**Scope discipline (carried).** Lexical class-shape is the minimal viable detector. Structural-shape refinement (incoming OGIT canonical relations FROM line-items for TaxPolicy; outgoing has_many to line-items for CommercialDocument; etc.) is the natural follow-up. Each concept promotion is one named test on real data, not a class hierarchy hand-built up-front. The `CanonicalConcept` enum still doesn't include SalesOrder / SalesOrderLine / ProductOffering / FulfillmentFlow / InventoryMovement / ProjectWorkItem / BillableWorkEntry — those land when ≥2-curator evidence appears (operator smoke target B needs a Spree harvest; smoke target C needs Project::TimeEntry + materialization rules).
322+
323+
**Cross-repo alignment with `AdaWorldAPI/OGAR#61`.** The merged `OGAR#61` introduced the `const CODEBOOK: &[(&str, u16)]` registry minting stable `u16` ClassIds for promoted canonical concepts (today: `project`, `project_work_item`, `billable_work_entry`). The ar_shape `CanonicalConcept` enum is the UPSTREAM candidate set — concepts get promoted into OGAR's CODEBOOK once they've cleared the ≥2-curator gate AND the OGAR-side review. Each `CanonicalConcept::CommercialLineItem`/`CommercialDocument`/`TaxPolicy`/`BillingParty`/`PaymentRecord`/`CurrencyPolicy` is a candidate row for a future OGAR CODEBOOK assignment.
324+
325+
**Cross-refs:** `crates/lance-graph-ontology/src/ar_shape.rs` (5 new detectors + `declared_classes` helper + 5 new tests; 19/19 green); `E-OGAR-AR-SHAPE-SMOKE-3` (OGIT canonical predecessor); `E-OGAR-AR-SHAPE-SMOKE-2` (predicate-vocabulary divergence finding); `E-OGAR-AR-SHAPE-SMOKE-1` (hand-fixture predecessor); `AdaWorldAPI/OGAR#61` (the upstream u16 CODEBOOK registry; each ar_shape canonical concept is a candidate for promotion into it).
326+
327+
---
328+
305329
## 2026-06-19 — E-OGAR-AR-SHAPE-SMOKE-3 — OGIT is the canonical predicate vocabulary; OGAR consumes canonical via per-extractor codebooks; `translate_rails_to_ogit` + `translate_odoo_to_ogit` fold curator vocabularies into `ogit:includes/isMemberOf/contains/isPartOf`
306330

307331
**Status:** FINDING (operator-directed clarification, 2026-06-19; 14/14 ar_shape tests green including 4 new OGIT-canonical ones). **Closes the predicate-vocabulary divergence from `E-OGAR-AR-SHAPE-SMOKE-2`** by naming what the convergence target IS, not just describing the divergence.

crates/lance-graph-ontology/src/ar_shape.rs

Lines changed: 273 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,24 @@ pub enum CanonicalConcept {
121121
/// Promoted from `{ osb:InvoiceLineItem, odoo:account.move.line }`
122122
/// pair on 2026-06-19.
123123
CommercialLineItem,
124+
/// `CommercialDocument` — the parent commercial document (invoice,
125+
/// journal entry, sales order). Promoted from
126+
/// `{ osb:Invoice, odoo:account.move }` on 2026-06-19.
127+
CommercialDocument,
128+
/// `TaxPolicy` — a named, rate-bearing tax binding. Promoted from
129+
/// `{ osb:Tax, odoo:account.tax }` on 2026-06-19.
130+
TaxPolicy,
131+
/// `BillingParty` — a counterparty (customer / supplier / partner).
132+
/// Promoted from `{ osb:Client, odoo:res.partner }` on 2026-06-19.
133+
BillingParty,
134+
/// `PaymentRecord` — an amount-bearing event tied to a commercial
135+
/// document. Promoted from `{ osb:Payment, odoo:account.payment }`
136+
/// on 2026-06-19.
137+
PaymentRecord,
138+
/// `CurrencyPolicy` — a named currency carrying a code (ISO 4217)
139+
/// and a label. Promoted from `{ osb:Currency, odoo:res.currency }`
140+
/// on 2026-06-19.
141+
CurrencyPolicy,
124142
}
125143

126144
/// A typed fixture for one curator's class declaration. Hand-built today;
@@ -467,6 +485,132 @@ pub fn classes_matching_commercial_line_item_shape_canonical(
467485
has_doc_assoc.intersection(&has_tax_assoc).cloned().collect()
468486
}
469487

488+
// ─── Sibling concept detectors (lexical class-name shape on declared OGIT
489+
// ObjectTypes) ─────────────────────────────────────────────────────────
490+
//
491+
// Each of the five sibling detectors below answers "which classes in
492+
// this corpus are shaped like <CanonicalConcept>?" by combining two
493+
// signals:
494+
//
495+
// 1. The class must be DECLARED — surfaces as the subject of an
496+
// `(s, rdf:type, ogit:ObjectType)` triple. This filters out method /
497+
// field IRIs (e.g. `Foo.bar`) and unrelated namespaces.
498+
// 2. The class IRI matches a concept-specific lexical hint
499+
// (e.g. ends_with `"tax"` for TaxPolicy). Hints converge across
500+
// Rails (PascalCase: `Tax`, `Invoice`, `Client`, `Currency`,
501+
// `Payment`) and Odoo (snake_case underscored: `account_tax`,
502+
// `account_move`, `res_partner`, `res_currency`, `account_payment`)
503+
// because both serialize the canonical concept name in the class
504+
// leaf.
505+
//
506+
// Structural-shape refinement (incoming/outgoing OGIT canonical
507+
// relations) is the natural follow-up — today's lexical shape is the
508+
// minimal viable detector that lets the ≥2-curator promotion rule fire
509+
// per concept. Each promotion is gated by a dedicated test on the real
510+
// OSB + Odoo corpora.
511+
512+
/// Return the set of class IRIs declared as `rdf:type ogit:ObjectType`
513+
/// in this triple set, with `namespace_prefix` stripped. Filters out
514+
/// non-class subjects (anything containing `.`).
515+
#[must_use]
516+
pub fn declared_classes(
517+
triples: &[Triple],
518+
namespace_prefix: &str,
519+
) -> std::collections::BTreeSet<String> {
520+
triples
521+
.iter()
522+
.filter(|t| t.p == "rdf:type" && t.o == "ogit:ObjectType")
523+
.filter_map(|t| t.s.strip_prefix(namespace_prefix).map(String::from))
524+
.filter(|c| !c.contains('.'))
525+
.collect()
526+
}
527+
528+
/// Find class IRIs in a triple set shaped like a `CommercialDocument`
529+
/// (the parent of line items): class-IRI's lowercased form ends with
530+
/// `"invoice"` (`osb:Invoice`), `"move"` (`odoo:account_move`), or
531+
/// `"order"` (`odoo:sale_order`), and the IRI does NOT contain `"line"`
532+
/// (to filter out `InvoiceLineItem` / `account_move_line` which are
533+
/// CommercialLineItem candidates, not document candidates).
534+
#[must_use]
535+
pub fn classes_matching_commercial_document_shape_canonical(
536+
triples: &[Triple],
537+
namespace_prefix: &str,
538+
) -> Vec<String> {
539+
declared_classes(triples, namespace_prefix)
540+
.into_iter()
541+
.filter(|c| {
542+
let lower = c.to_lowercase();
543+
if lower.contains("line") {
544+
return false;
545+
}
546+
lower.ends_with("invoice")
547+
|| lower.ends_with("move")
548+
|| lower.ends_with("order")
549+
})
550+
.collect()
551+
}
552+
553+
/// Find class IRIs shaped like a `TaxPolicy`: class IRI's lowercased
554+
/// form ends with `"tax"`. Catches `osb:Tax` and `odoo:account_tax`;
555+
/// excludes `TaxGroup` / `account_tax_group` (lowercased `"taxgroup"`,
556+
/// `"account_tax_group"` — neither ends with `"tax"`).
557+
#[must_use]
558+
pub fn classes_matching_tax_policy_shape_canonical(
559+
triples: &[Triple],
560+
namespace_prefix: &str,
561+
) -> Vec<String> {
562+
declared_classes(triples, namespace_prefix)
563+
.into_iter()
564+
.filter(|c| c.to_lowercase().ends_with("tax"))
565+
.collect()
566+
}
567+
568+
/// Find class IRIs shaped like a `BillingParty`: lowercased ends with
569+
/// `"client"`, `"customer"`, or `"partner"`. Catches `osb:Client` and
570+
/// `odoo:res_partner`.
571+
#[must_use]
572+
pub fn classes_matching_billing_party_shape_canonical(
573+
triples: &[Triple],
574+
namespace_prefix: &str,
575+
) -> Vec<String> {
576+
declared_classes(triples, namespace_prefix)
577+
.into_iter()
578+
.filter(|c| {
579+
let lower = c.to_lowercase();
580+
lower.ends_with("client") || lower.ends_with("customer") || lower.ends_with("partner")
581+
})
582+
.collect()
583+
}
584+
585+
/// Find class IRIs shaped like a `CurrencyPolicy`: lowercased ends with
586+
/// `"currency"`. Catches `osb:Currency` and `odoo:res_currency`.
587+
#[must_use]
588+
pub fn classes_matching_currency_policy_shape_canonical(
589+
triples: &[Triple],
590+
namespace_prefix: &str,
591+
) -> Vec<String> {
592+
declared_classes(triples, namespace_prefix)
593+
.into_iter()
594+
.filter(|c| c.to_lowercase().ends_with("currency"))
595+
.collect()
596+
}
597+
598+
/// Find class IRIs shaped like a `PaymentRecord`: lowercased ends with
599+
/// `"payment"`. Catches `osb:Payment` and `odoo:account_payment`. Also
600+
/// catches `osb:CreditPayment` (a sub-type) — that's expected; the
601+
/// detector returns multiple candidates and downstream ranking picks
602+
/// the primary one.
603+
#[must_use]
604+
pub fn classes_matching_payment_record_shape_canonical(
605+
triples: &[Triple],
606+
namespace_prefix: &str,
607+
) -> Vec<String> {
608+
declared_classes(triples, namespace_prefix)
609+
.into_iter()
610+
.filter(|c| c.to_lowercase().ends_with("payment"))
611+
.collect()
612+
}
613+
470614
// ─── Triple-based detection on real ruff-harvested corpora ──────────────
471615
//
472616
// The hand-fixture path above remains as the structural CLAIM. The Triple
@@ -1000,6 +1144,135 @@ mod tests {
10001144
);
10011145
}
10021146

1147+
// ─── Five sibling-concept corpus-driven tests ───────────────────
1148+
1149+
/// `open_source_billing_invoice_and_odoo_account_move_overlap_as_commercial_document`
1150+
/// — the strongest accounting pair after CommercialLineItem.
1151+
#[test]
1152+
fn open_source_billing_invoice_and_odoo_account_move_overlap_as_commercial_document() {
1153+
let osb_bytes = include_bytes!("../tests/fixtures/osb_ruby_spo.ndjson");
1154+
let odoo_bytes = include_bytes!(
1155+
"../../lance-graph/src/graph/spo/odoo_ontology.spo.ndjson"
1156+
);
1157+
let osb = load_triples_ndjson(osb_bytes).unwrap();
1158+
let odoo = load_triples_ndjson(odoo_bytes).unwrap();
1159+
1160+
let osb_c = classes_matching_commercial_document_shape_canonical(&osb, "openproject:");
1161+
let odoo_c = classes_matching_commercial_document_shape_canonical(&odoo, "odoo:");
1162+
1163+
assert!(
1164+
osb_c.iter().any(|c| c == "Invoice"),
1165+
"OSB candidates missing Invoice; got {osb_c:?}",
1166+
);
1167+
assert!(
1168+
odoo_c.iter().any(|c| c == "account_move"),
1169+
"Odoo candidates missing account_move; got first 5: {:?}",
1170+
odoo_c.iter().take(5).collect::<Vec<_>>(),
1171+
);
1172+
// CommercialLineItem candidates (Invoice*Line* / account_move_line)
1173+
// must NOT promote as CommercialDocument — the "line" filter
1174+
// discriminates them.
1175+
assert!(!osb_c.iter().any(|c| c == "InvoiceLineItem"));
1176+
assert!(!odoo_c.iter().any(|c| c == "account_move_line"));
1177+
}
1178+
1179+
/// `open_source_billing_tax_and_odoo_tax_overlap_as_tax_policy` —
1180+
/// the operator's named second smoke test.
1181+
#[test]
1182+
fn open_source_billing_tax_and_odoo_tax_overlap_as_tax_policy() {
1183+
let osb_bytes = include_bytes!("../tests/fixtures/osb_ruby_spo.ndjson");
1184+
let odoo_bytes = include_bytes!(
1185+
"../../lance-graph/src/graph/spo/odoo_ontology.spo.ndjson"
1186+
);
1187+
let osb = load_triples_ndjson(osb_bytes).unwrap();
1188+
let odoo = load_triples_ndjson(odoo_bytes).unwrap();
1189+
1190+
let osb_c = classes_matching_tax_policy_shape_canonical(&osb, "openproject:");
1191+
let odoo_c = classes_matching_tax_policy_shape_canonical(&odoo, "odoo:");
1192+
1193+
assert!(
1194+
osb_c.iter().any(|c| c == "Tax"),
1195+
"OSB candidates missing Tax; got {osb_c:?}",
1196+
);
1197+
assert!(
1198+
odoo_c.iter().any(|c| c == "account_tax"),
1199+
"Odoo candidates missing account_tax; got first 5: {:?}",
1200+
odoo_c.iter().take(5).collect::<Vec<_>>(),
1201+
);
1202+
}
1203+
1204+
/// `open_source_billing_client_and_odoo_res_partner_overlap_as_billing_party`.
1205+
#[test]
1206+
fn open_source_billing_client_and_odoo_res_partner_overlap_as_billing_party() {
1207+
let osb_bytes = include_bytes!("../tests/fixtures/osb_ruby_spo.ndjson");
1208+
let odoo_bytes = include_bytes!(
1209+
"../../lance-graph/src/graph/spo/odoo_ontology.spo.ndjson"
1210+
);
1211+
let osb = load_triples_ndjson(osb_bytes).unwrap();
1212+
let odoo = load_triples_ndjson(odoo_bytes).unwrap();
1213+
1214+
let osb_c = classes_matching_billing_party_shape_canonical(&osb, "openproject:");
1215+
let odoo_c = classes_matching_billing_party_shape_canonical(&odoo, "odoo:");
1216+
1217+
assert!(
1218+
osb_c.iter().any(|c| c == "Client"),
1219+
"OSB candidates missing Client; got {osb_c:?}",
1220+
);
1221+
assert!(
1222+
odoo_c.iter().any(|c| c == "res_partner"),
1223+
"Odoo candidates missing res_partner; got first 5: {:?}",
1224+
odoo_c.iter().take(5).collect::<Vec<_>>(),
1225+
);
1226+
}
1227+
1228+
/// `open_source_billing_currency_and_odoo_res_currency_overlap_as_currency_policy`.
1229+
#[test]
1230+
fn open_source_billing_currency_and_odoo_res_currency_overlap_as_currency_policy() {
1231+
let osb_bytes = include_bytes!("../tests/fixtures/osb_ruby_spo.ndjson");
1232+
let odoo_bytes = include_bytes!(
1233+
"../../lance-graph/src/graph/spo/odoo_ontology.spo.ndjson"
1234+
);
1235+
let osb = load_triples_ndjson(osb_bytes).unwrap();
1236+
let odoo = load_triples_ndjson(odoo_bytes).unwrap();
1237+
1238+
let osb_c = classes_matching_currency_policy_shape_canonical(&osb, "openproject:");
1239+
let odoo_c = classes_matching_currency_policy_shape_canonical(&odoo, "odoo:");
1240+
1241+
assert!(
1242+
osb_c.iter().any(|c| c == "Currency"),
1243+
"OSB candidates missing Currency; got {osb_c:?}",
1244+
);
1245+
assert!(
1246+
odoo_c.iter().any(|c| c == "res_currency"),
1247+
"Odoo candidates missing res_currency; got first 5: {:?}",
1248+
odoo_c.iter().take(5).collect::<Vec<_>>(),
1249+
);
1250+
}
1251+
1252+
/// `open_source_billing_payment_and_odoo_account_payment_overlap_as_payment_record`.
1253+
#[test]
1254+
fn open_source_billing_payment_and_odoo_account_payment_overlap_as_payment_record() {
1255+
let osb_bytes = include_bytes!("../tests/fixtures/osb_ruby_spo.ndjson");
1256+
let odoo_bytes = include_bytes!(
1257+
"../../lance-graph/src/graph/spo/odoo_ontology.spo.ndjson"
1258+
);
1259+
let osb = load_triples_ndjson(osb_bytes).unwrap();
1260+
let odoo = load_triples_ndjson(odoo_bytes).unwrap();
1261+
1262+
let osb_c = classes_matching_payment_record_shape_canonical(&osb, "openproject:");
1263+
let odoo_c = classes_matching_payment_record_shape_canonical(&odoo, "odoo:");
1264+
1265+
assert!(
1266+
osb_c.iter().any(|c| c == "Payment"),
1267+
"OSB candidates missing Payment; got {osb_c:?}",
1268+
);
1269+
assert!(
1270+
odoo_c.iter().any(|c| c == "account_payment"),
1271+
"Odoo candidates missing account_payment; got first 5: {:?}",
1272+
odoo_c.iter().take(5).collect::<Vec<_>>(),
1273+
);
1274+
}
1275+
10031276
/// Hand-fixture detection (the `overlap_commercial_line_item` path
10041277
/// committed in the prior smoke) and the harvest detection
10051278
/// (`classes_matching_commercial_line_item_shape`) must agree on

0 commit comments

Comments
 (0)