Skip to content

Commit 29336f4

Browse files
committed
feat(ar_shape): structural-hardening seed — bidirectional canonical-relation participation filter
Phase 1 of operator's "all of the above" 2026-06-19. Adds classes_participating_in_canonical_relations(canonical_triples, ns) → BTreeSet<String> — returns class IRIs that appear as either subject OR object of any OGIT canonical relation after codebook translation. Bidirectional: leaf classes (currency, tax target) are usually referenced AS objects (Document.currency_id → res_currency) rather than emitting Many2one out. A subject-only check would falsely flag them as inert. Object-side IRI shapes handled: - Class IRI (<ns><Class>) — Odoo-translated codebook output names the comodel directly. - Field IRI (<ns><Class>.<assoc>) — Rails-translated codebook output keeps field IRI verbatim; leading <Class> is the SOURCE, already covered by subject side. New test lexical_candidates_survive_canonical_relation_participation_check verifies all 6 lexical-detection candidates on both corpora (OSB InvoiceLineItem/Invoice/Tax/Client/Currency/Payment + Odoo account_move_line/account_move/account_tax/res_partner/res_currency/ account_payment) survive the participation filter. Initial subject-only implementation surfaced res_currency as zero-subject-relations (the right finding) — fixed by adding object-side coverage. Direction-blind today; this is the seed for future class_has_outbound_relation_to_<concept> / class_has_inbound_relation_from_<concept> refinements that wire the concept-to-concept cascade (TaxPolicy needs LineItem participation; CommercialDocument needs LineItem participation; BillingParty needs CommercialDocument; etc.). Plus all 19 prior tests still green → 20/20 total. OGAR alignment update (other-session feedback, 2026-06-19): AdaWorldAPI/OGAR#63 merged — promoted ProjectStatus (Redmine IssueStatus + OP Status → 0x0005) + ProjectType (Redmine Tracker + OP Type → 0x0006). CODEBOOK now at 6 entries, all project-management domain. My commerce-domain CanonicalConcept candidates would slot at 0x0007+ when the OGAR codebook promotion PR (Phase 3 of "all of the above") opens. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Xzyc27Nx3f8WC5KzwfWfjx
1 parent ee7b33a commit 29336f4

1 file changed

Lines changed: 113 additions & 0 deletions

File tree

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

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -525,6 +525,67 @@ pub fn declared_classes(
525525
.collect()
526526
}
527527

528+
/// Structural-hardening seed: return the set of class IRIs that
529+
/// participate in at least one OGIT canonical relation in the supplied
530+
/// CANONICAL triple set, **bidirectionally** — as either subject (the
531+
/// class emits the relation) or object (another class points to it).
532+
///
533+
/// Bidirectional matters because leaf classes like a currency policy or
534+
/// a tax-rate target rarely emit Many2one out; they're SOURCE objects
535+
/// for inbound relations from documents and line items. A
536+
/// subject-only check would falsely flag them as inert.
537+
///
538+
/// Object-side IRI shapes handled:
539+
/// - Class IRI (`<ns><Class>`) — Odoo-translated codebook output
540+
/// names the comodel class directly.
541+
/// - Field IRI (`<ns><Class>.<assoc>`) — Rails-translated codebook
542+
/// output keeps the field-IRI verbatim. The class is the part before
543+
/// the `.`.
544+
///
545+
/// Usage pattern:
546+
/// ```ignore
547+
/// let canonical = translate_rails_to_ogit(&raw_triples);
548+
/// let participants =
549+
/// classes_participating_in_canonical_relations(&canonical, "openproject:");
550+
/// let hardened: Vec<_> = lexical_candidates
551+
/// .into_iter()
552+
/// .filter(|c| participants.contains(c))
553+
/// .collect();
554+
/// ```
555+
///
556+
/// Direction-blind today; the seed for future
557+
/// `class_has_outbound_relation_to_<concept>` /
558+
/// `class_has_inbound_relation_from_<concept>` refinements.
559+
#[must_use]
560+
pub fn classes_participating_in_canonical_relations(
561+
canonical_triples: &[Triple],
562+
namespace_prefix: &str,
563+
) -> std::collections::BTreeSet<String> {
564+
let mut out = std::collections::BTreeSet::new();
565+
for t in canonical_triples {
566+
if !ogit_relations::is_relation_predicate(&t.p) {
567+
continue;
568+
}
569+
// Subject side — the class emitting the relation.
570+
if let Some(s_no_ns) = t.s.strip_prefix(namespace_prefix) {
571+
if !s_no_ns.contains('.') {
572+
out.insert(s_no_ns.to_string());
573+
}
574+
}
575+
// Object side — the class being pointed at. For Rails-translated
576+
// output the object is a field IRI (`<ns><Class>.<assoc>`); the
577+
// leading `<Class>` is the SOURCE class, already covered by the
578+
// subject side above. For Odoo-translated output the object is
579+
// a bare class IRI (`<ns><comodel>`); count that as a participant.
580+
if let Some(o_no_ns) = t.o.strip_prefix(namespace_prefix) {
581+
if !o_no_ns.contains('.') {
582+
out.insert(o_no_ns.to_string());
583+
}
584+
}
585+
}
586+
out
587+
}
588+
528589
/// Find class IRIs in a triple set shaped like a `CommercialDocument`
529590
/// (the parent of line items): class-IRI's lowercased form ends with
530591
/// `"invoice"` (`osb:Invoice`), `"move"` (`odoo:account_move`), or
@@ -1273,6 +1334,58 @@ mod tests {
12731334
);
12741335
}
12751336

1337+
/// Structural-hardening seed: every concept-shape candidate the
1338+
/// six lexical detectors surface on the real OSB + Odoo corpora
1339+
/// must ALSO appear in the participating-classes set (i.e. surface
1340+
/// as the subject of at least one OGIT canonical relation after
1341+
/// codebook translation). If the lexical detector returns a class
1342+
/// that has zero canonical relations, that's a structural false
1343+
/// positive worth flagging — but on today's corpora, all 6
1344+
/// expected pairs participate.
1345+
#[test]
1346+
fn lexical_candidates_survive_canonical_relation_participation_check() {
1347+
let osb_raw = load_triples_ndjson(include_bytes!(
1348+
"../tests/fixtures/osb_ruby_spo.ndjson"
1349+
))
1350+
.unwrap();
1351+
let odoo_raw = load_triples_ndjson(include_bytes!(
1352+
"../../lance-graph/src/graph/spo/odoo_ontology.spo.ndjson"
1353+
))
1354+
.unwrap();
1355+
1356+
let osb_canonical = translate_rails_to_ogit(&osb_raw);
1357+
let odoo_canonical = translate_odoo_to_ogit(&odoo_raw, "odoo:");
1358+
1359+
let osb_participants = classes_participating_in_canonical_relations(
1360+
&osb_canonical,
1361+
"openproject:",
1362+
);
1363+
let odoo_participants =
1364+
classes_participating_in_canonical_relations(&odoo_canonical, "odoo:");
1365+
1366+
for c in ["InvoiceLineItem", "Invoice", "Tax", "Client", "Currency", "Payment"] {
1367+
assert!(
1368+
osb_participants.contains(c),
1369+
"OSB candidate `{c}` is lexically matched but has ZERO OGIT \
1370+
canonical relations as subject — structural false positive?",
1371+
);
1372+
}
1373+
for c in [
1374+
"account_move_line",
1375+
"account_move",
1376+
"account_tax",
1377+
"res_partner",
1378+
"res_currency",
1379+
"account_payment",
1380+
] {
1381+
assert!(
1382+
odoo_participants.contains(c),
1383+
"Odoo candidate `{c}` is lexically matched but has ZERO OGIT \
1384+
canonical relations as subject — structural false positive?",
1385+
);
1386+
}
1387+
}
1388+
12761389
/// Hand-fixture detection (the `overlap_commercial_line_item` path
12771390
/// committed in the prior smoke) and the harvest detection
12781391
/// (`classes_matching_commercial_line_item_shape`) must agree on

0 commit comments

Comments
 (0)