Skip to content

Commit fac386a

Browse files
committed
feat(ar_shape): synergy_registry_one_shot — full canonical ERP label table from 3 ruff/Odoo harvests in one call
Operator "cant you use the export you have from Ruff to do the canonical ERP labels i one shot" (2026-06-19). Mechanizes the doctrine §2 synergy-registry framing as a single function: 3 harvest exports in → 11-entry canonical ERP label table out. What landed: 1. New pub struct CanonicalErpEntry { concept, matches: Vec<(SourceCurator, String)> } — one row of the canonical ERP label table. Carries the promoted concept + every (curator, class_iri) pair that surfaces it. Includes curator_count() helper for the ≥2-curator gate. 2. New pub fn synergy_registry_one_shot(osb, spree, odoo) that: - Takes 3 (triples, namespace_prefix) tuples (OSB Ruby, Spree Ruby, Odoo Python — the three harvests the workspace currently ships) - Uses a method-pointer table mapping each CanonicalConcept variant to its lexical detector (CommercialLineItem uses the vocabulary-aware one that handles both declares_association + target; the other 10 use the lexical declared_classes variants) - Runs every detector against every curator - Sorts + dedups matches - Applies the ≥2-curator promotion rule per entry - Returns Vec<CanonicalErpEntry> sorted by enum-discriminant order, fully deterministic 3. 2 new tests: - synergy_registry_one_shot_returns_full_canonical_erp_label_table loads all 3 fixtures (OSB 1195 triples + Spree 7954 + Odoo 2.8MB), calls the one-shot, asserts every one of the 11 expected concepts surfaces with the right (curator, class_iri) pairs. TaxPolicy + PaymentRecord assert curator_count() >= 3 (the 3-curator convergence). Registry size pinned at 11. - synergy_registry_one_shot_is_deterministic — re-running on the same inputs returns identical Vec. Plus all 26 prior tests still green → 28/28 total. The 11 canonical ERP labels surfaced by the one-shot (the synergy registry's current state): | Concept | OSB | Spree | Odoo | |-------------------|---------------|--------------------|------------------------| | CommercialLineItem| InvoiceLineItem | — | account_move_line | | CommercialDocument| Invoice | — | account_move | | TaxPolicy | Tax | Spree::TaxRate | account_tax | | BillingParty | Client | — | res_partner | | PaymentRecord | Payment | Spree::Payment | account_payment | | CurrencyPolicy | Currency | — | res_currency | | SalesOrder | — | Spree::Order | sale_order | | SalesOrderLine | — | Spree::LineItem | sale_order_line | | FulfillmentFlow | — | Spree::Shipment | stock_picking | | InventoryMovement | — | Spree::InventoryUnit | stock_move | | ProductOffering | — | Spree::Product | product_product | Each row in the table represents a ≥2-curator promotion (OSB+Odoo for accounting concepts; Spree+Odoo for commerce concepts; OSB+Odoo+Spree for TaxPolicy + PaymentRecord — the 3-curator concepts). Cross-references (the synergy registry as canonical ERP label table) parallel OGAR#64's CODEBOOK structure: each row is a candidate for a future u16 ClassId assignment. Today OGAR's CODEBOOK has 0x0001–0x0006 (project-mgmt) + #64 pending 0x0007–0x000C (the SMOKE-1..4 commerce sextet). The 5 SMOKE-5 additions (SalesOrder + 4 commerce siblings) would extend the commerce block at 0x000D–0x0011 in a follow-on promotion. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Xzyc27Nx3f8WC5KzwfWfjx
1 parent 466e73e commit fac386a

1 file changed

Lines changed: 299 additions & 0 deletions

File tree

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

Lines changed: 299 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -808,6 +808,150 @@ pub fn classes_matching_payment_record_shape_canonical(
808808
.collect()
809809
}
810810

811+
// ─── One-shot synergy registry over real ruff/odoo harvests ────────────
812+
//
813+
// `synergy_registry_one_shot` takes the three harvest ndjson byte-buffers
814+
// the workspace currently ships (OSB Ruby via ruff_ruby_spo, Spree Ruby
815+
// via the same harvester, Odoo Python via the existing spo_enrich)
816+
// alongside their namespace prefixes, runs every concept-specific
817+
// lexical detector against each curator, and returns the full canonical
818+
// ERP label table in one call — exactly the synergy-registry shape the
819+
// doctrine §2 corrections framed.
820+
//
821+
// Each `CanonicalErpEntry` carries one concept + the cross-curator
822+
// class IRIs that surface it under their per-curator labels (leaf
823+
// detail per doctrine §2 correction 4). The ≥2-curator promotion rule
824+
// is applied at the entry level: a concept lands in the registry only
825+
// if at least two distinct curators surface a matching class. Single-
826+
// curator hits are dropped (they'd be premature promotions per
827+
// operator acceptance #2-#3).
828+
829+
/// One row of the canonical ERP label table: which `CanonicalConcept`,
830+
/// and which curator-tagged class IRIs surface it.
831+
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
832+
pub struct CanonicalErpEntry {
833+
/// The promoted concept (one of the 11 today).
834+
pub concept: CanonicalConcept,
835+
/// All curator-class pairs whose lexical detector surfaced this
836+
/// concept on the supplied harvests. Sorted by curator then class
837+
/// IRI for deterministic output.
838+
pub matches: Vec<(SourceCurator, String)>,
839+
}
840+
841+
impl CanonicalErpEntry {
842+
/// Number of distinct curators contributing matches. The ≥2 gate
843+
/// is enforced by [`synergy_registry_one_shot`] — every entry it
844+
/// returns has `curator_count() >= 2`.
845+
#[must_use]
846+
pub fn curator_count(&self) -> usize {
847+
let set: std::collections::BTreeSet<_> =
848+
self.matches.iter().map(|(c, _)| c).collect();
849+
set.len()
850+
}
851+
}
852+
853+
/// Run every concept detector on each of three curator harvests and
854+
/// return the full canonical ERP label table, filtered by the
855+
/// ≥2-curator promotion rule.
856+
///
857+
/// Inputs are `(triples, namespace_prefix)` tuples per curator. The
858+
/// namespace prefix is needed because `ruff_ruby_spo` hardcodes
859+
/// `openproject:` for any Rails harvest today — OSB and Spree both
860+
/// arrive with that prefix, distinguishable only by the
861+
/// `SourceCurator` tag passed in. (Once `ruff#27`'s
862+
/// `extract_with(path, ns)` API lands, the harvests will carry
863+
/// their native namespaces and this becomes more explicit.)
864+
///
865+
/// Output: one `CanonicalErpEntry` per promoted concept, sorted by
866+
/// concept (enum-discriminant order). Each entry's `matches` list is
867+
/// sorted by `(SourceCurator, class_iri)` for deterministic
868+
/// reproducibility — re-running on the same inputs returns the same
869+
/// table.
870+
///
871+
/// This is the **canonical ERP label table** referenced in
872+
/// `E-OGAR-AR-SHAPE-ENDGAME` §2's synergy-registry framing: one call,
873+
/// three harvests, the full cross-curator class-mapping table out.
874+
#[must_use]
875+
pub fn synergy_registry_one_shot(
876+
osb: (&[Triple], &str),
877+
spree: (&[Triple], &str),
878+
odoo: (&[Triple], &str),
879+
) -> Vec<CanonicalErpEntry> {
880+
let curators: [(SourceCurator, &[Triple], &str); 3] = [
881+
(SourceCurator::OpenSourceBilling, osb.0, osb.1),
882+
(SourceCurator::Spree, spree.0, spree.1),
883+
(SourceCurator::Odoo, odoo.0, odoo.1),
884+
];
885+
886+
// Method-pointer table: one detector per concept. Order matches
887+
// the `CanonicalConcept` enum so the output is deterministic.
888+
let detectors: [(CanonicalConcept, fn(&[Triple], &str) -> Vec<String>); 11] = [
889+
(
890+
CanonicalConcept::CommercialLineItem,
891+
classes_matching_commercial_line_item_shape,
892+
),
893+
(
894+
CanonicalConcept::CommercialDocument,
895+
classes_matching_commercial_document_shape_canonical,
896+
),
897+
(
898+
CanonicalConcept::TaxPolicy,
899+
classes_matching_tax_policy_shape_canonical,
900+
),
901+
(
902+
CanonicalConcept::BillingParty,
903+
classes_matching_billing_party_shape_canonical,
904+
),
905+
(
906+
CanonicalConcept::PaymentRecord,
907+
classes_matching_payment_record_shape_canonical,
908+
),
909+
(
910+
CanonicalConcept::CurrencyPolicy,
911+
classes_matching_currency_policy_shape_canonical,
912+
),
913+
(
914+
CanonicalConcept::SalesOrder,
915+
classes_matching_sales_order_shape_canonical,
916+
),
917+
(
918+
CanonicalConcept::SalesOrderLine,
919+
classes_matching_sales_order_line_shape_canonical,
920+
),
921+
(
922+
CanonicalConcept::FulfillmentFlow,
923+
classes_matching_fulfillment_flow_shape_canonical,
924+
),
925+
(
926+
CanonicalConcept::InventoryMovement,
927+
classes_matching_inventory_movement_shape_canonical,
928+
),
929+
(
930+
CanonicalConcept::ProductOffering,
931+
classes_matching_product_offering_shape_canonical,
932+
),
933+
];
934+
935+
let mut out = Vec::new();
936+
for (concept, detector) in detectors {
937+
let mut matches: Vec<(SourceCurator, String)> = Vec::new();
938+
for &(curator, triples, ns) in &curators {
939+
for class_iri in detector(triples, ns) {
940+
matches.push((curator, class_iri));
941+
}
942+
}
943+
matches.sort();
944+
matches.dedup();
945+
let entry = CanonicalErpEntry { concept, matches };
946+
// ≥2-curator promotion rule: a concept lands in the registry
947+
// only if at least two distinct curators contribute matches.
948+
if entry.curator_count() >= 2 {
949+
out.push(entry);
950+
}
951+
}
952+
out
953+
}
954+
811955
// ─── Triple-based detection on real ruff-harvested corpora ──────────────
812956
//
813957
// The hand-fixture path above remains as the structural CLAIM. The Triple
@@ -1470,6 +1614,161 @@ mod tests {
14701614
);
14711615
}
14721616

1617+
// ─── One-shot synergy registry test ─────────────────────────────
1618+
1619+
/// `synergy_registry_one_shot` consumes all three workspace
1620+
/// harvests (OSB Ruby, Spree Ruby, Odoo Python) and returns the
1621+
/// full canonical ERP label table — every concept the ≥2-curator
1622+
/// promotion rule admits, with the cross-curator class IRIs that
1623+
/// surface it. **The operator's "canonical ERP labels in one
1624+
/// shot" request, mechanized.**
1625+
#[test]
1626+
fn synergy_registry_one_shot_returns_full_canonical_erp_label_table() {
1627+
let osb_bytes = include_bytes!("../tests/fixtures/osb_ruby_spo.ndjson");
1628+
let spree_bytes =
1629+
include_bytes!("../tests/fixtures/spree_ruby_spo.ndjson");
1630+
let odoo_bytes = include_bytes!(
1631+
"../../lance-graph/src/graph/spo/odoo_ontology.spo.ndjson"
1632+
);
1633+
1634+
let osb = load_triples_ndjson(osb_bytes).unwrap();
1635+
let spree = load_triples_ndjson(spree_bytes).unwrap();
1636+
let odoo = load_triples_ndjson(odoo_bytes).unwrap();
1637+
1638+
let registry = synergy_registry_one_shot(
1639+
(&osb, "openproject:"),
1640+
(&spree, "openproject:"),
1641+
(&odoo, "odoo:"),
1642+
);
1643+
1644+
// Helper: lookup the entry for a concept, panicking on absence
1645+
// with a useful message (the test author should know which
1646+
// concept didn't surface).
1647+
let entry_for = |concept: CanonicalConcept| -> &CanonicalErpEntry {
1648+
registry
1649+
.iter()
1650+
.find(|e| e.concept == concept)
1651+
.unwrap_or_else(|| {
1652+
panic!(
1653+
"concept {concept:?} missing from registry; \
1654+
got {} entries: {:?}",
1655+
registry.len(),
1656+
registry.iter().map(|e| e.concept).collect::<Vec<_>>(),
1657+
)
1658+
})
1659+
};
1660+
1661+
// Helper: assert a (curator, class) pair is in the entry's
1662+
// matches list.
1663+
let assert_match =
1664+
|entry: &CanonicalErpEntry, curator: SourceCurator, class_iri: &str| {
1665+
assert!(
1666+
entry.matches.iter().any(|(c, s)| *c == curator && s == class_iri),
1667+
"{:?} missing ({:?}, {class_iri}); matches: {:?}",
1668+
entry.concept,
1669+
curator,
1670+
entry.matches,
1671+
);
1672+
};
1673+
1674+
// Every one of the 11 promoted concepts must appear and carry
1675+
// its expected OSB/Spree/Odoo classes.
1676+
let cli = entry_for(CanonicalConcept::CommercialLineItem);
1677+
assert_match(cli, SourceCurator::OpenSourceBilling, "InvoiceLineItem");
1678+
assert_match(cli, SourceCurator::Odoo, "account_move_line");
1679+
1680+
let cd = entry_for(CanonicalConcept::CommercialDocument);
1681+
assert_match(cd, SourceCurator::OpenSourceBilling, "Invoice");
1682+
assert_match(cd, SourceCurator::Odoo, "account_move");
1683+
1684+
let tax = entry_for(CanonicalConcept::TaxPolicy);
1685+
assert_match(tax, SourceCurator::OpenSourceBilling, "Tax");
1686+
assert_match(tax, SourceCurator::Odoo, "account_tax");
1687+
assert_match(tax, SourceCurator::Spree, "Spree::TaxRate");
1688+
assert!(
1689+
tax.curator_count() >= 3,
1690+
"TaxPolicy is a 3-curator concept; got {} curators",
1691+
tax.curator_count(),
1692+
);
1693+
1694+
let bp = entry_for(CanonicalConcept::BillingParty);
1695+
assert_match(bp, SourceCurator::OpenSourceBilling, "Client");
1696+
assert_match(bp, SourceCurator::Odoo, "res_partner");
1697+
1698+
let pay = entry_for(CanonicalConcept::PaymentRecord);
1699+
assert_match(pay, SourceCurator::OpenSourceBilling, "Payment");
1700+
assert_match(pay, SourceCurator::Odoo, "account_payment");
1701+
assert_match(pay, SourceCurator::Spree, "Spree::Payment");
1702+
assert!(
1703+
pay.curator_count() >= 3,
1704+
"PaymentRecord is a 3-curator concept; got {} curators",
1705+
pay.curator_count(),
1706+
);
1707+
1708+
let cur = entry_for(CanonicalConcept::CurrencyPolicy);
1709+
assert_match(cur, SourceCurator::OpenSourceBilling, "Currency");
1710+
assert_match(cur, SourceCurator::Odoo, "res_currency");
1711+
1712+
let so = entry_for(CanonicalConcept::SalesOrder);
1713+
assert_match(so, SourceCurator::Spree, "Spree::Order");
1714+
assert_match(so, SourceCurator::Odoo, "sale_order");
1715+
1716+
let sol = entry_for(CanonicalConcept::SalesOrderLine);
1717+
assert_match(sol, SourceCurator::Spree, "Spree::LineItem");
1718+
assert_match(sol, SourceCurator::Odoo, "sale_order_line");
1719+
1720+
let ff = entry_for(CanonicalConcept::FulfillmentFlow);
1721+
assert_match(ff, SourceCurator::Spree, "Spree::Shipment");
1722+
assert_match(ff, SourceCurator::Odoo, "stock_picking");
1723+
1724+
let im = entry_for(CanonicalConcept::InventoryMovement);
1725+
assert_match(im, SourceCurator::Spree, "Spree::InventoryUnit");
1726+
assert_match(im, SourceCurator::Odoo, "stock_move");
1727+
1728+
let po = entry_for(CanonicalConcept::ProductOffering);
1729+
assert_match(po, SourceCurator::Spree, "Spree::Product");
1730+
assert_match(po, SourceCurator::Odoo, "product_product");
1731+
1732+
// Registry size — all 11 concepts cleared the ≥2-curator gate.
1733+
assert_eq!(
1734+
registry.len(),
1735+
11,
1736+
"expected 11 registry entries; got {}: {:?}",
1737+
registry.len(),
1738+
registry.iter().map(|e| e.concept).collect::<Vec<_>>(),
1739+
);
1740+
}
1741+
1742+
/// Determinism: the same inputs MUST produce the same registry on
1743+
/// repeated calls (matches sorted, dedup applied).
1744+
#[test]
1745+
fn synergy_registry_one_shot_is_deterministic() {
1746+
let osb = load_triples_ndjson(include_bytes!(
1747+
"../tests/fixtures/osb_ruby_spo.ndjson"
1748+
))
1749+
.unwrap();
1750+
let spree = load_triples_ndjson(include_bytes!(
1751+
"../tests/fixtures/spree_ruby_spo.ndjson"
1752+
))
1753+
.unwrap();
1754+
let odoo = load_triples_ndjson(include_bytes!(
1755+
"../../lance-graph/src/graph/spo/odoo_ontology.spo.ndjson"
1756+
))
1757+
.unwrap();
1758+
1759+
let r1 = synergy_registry_one_shot(
1760+
(&osb, "openproject:"),
1761+
(&spree, "openproject:"),
1762+
(&odoo, "odoo:"),
1763+
);
1764+
let r2 = synergy_registry_one_shot(
1765+
(&osb, "openproject:"),
1766+
(&spree, "openproject:"),
1767+
(&odoo, "odoo:"),
1768+
);
1769+
assert_eq!(r1, r2);
1770+
}
1771+
14731772
// ─── Spree harvest tests (smoke target B; 3rd curator) ────────
14741773

14751774
/// `spree_order_and_odoo_sale_order_overlap_as_sales_order`

0 commit comments

Comments
 (0)