Skip to content

Commit 69531ba

Browse files
committed
feat(ontology): D-CLS-RES — class_resolver (RegistryClassView impls ClassView over the live OGIT cache)
Makes the contract ClassView trait live: the OGIT-hashtable-single-lookup -> class-meta- lookup upgrade. RegistryClassView<'a> resolves class_id -> shape over a borrowed OntologyRegistry; DOLCE resolved LATE (enumerate_first_with_entity_type_id -> MappingRow -> ogit_uri -> classify_odoo), never stored on the row (OD-DOLCE cache-resolves). dolce_id u8 consts = the stable ids the trait returns. Dep-inversion (ontology already deps contract). Honest scope: field-SET supplied (ObjectView bit-basis), not enumerated — MappingRow has no field-list; enumeration = deferred D-CLS audit, nothing fabricated. /code-review caught the O(n)-scan+full-clone in registry::enumerate_first_with_entity_type_id called per render -> fixed at my layer with a per-class RefCell memo (DOLCE stable per class) + documented the deferred by_entity_type_id index. 4 teeth-tests + 234 ontology lib green; clippy+fmt clean. https://claude.ai/code/session_01R9AWgFa65uPnLyS2my2d2R
1 parent 931e63f commit 69531ba

4 files changed

Lines changed: 232 additions & 0 deletions

File tree

.claude/board/EPIPHANIES.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,16 @@
1+
## 2026-05-31 — SHIPPED-in-PR: D-CLS-RES — class_resolver (ontology-side impl ClassView; the meta-DTO flies over the LIVE OGIT cache)
2+
3+
**Status:** SHIPPED-in-PR (#440 D-CLS). Makes the contract `ClassView` trait LIVE — the "OGIT hashtable single-lookup → class meta-lookup" upgrade, done.
4+
5+
`lance-graph-ontology::class_resolver::RegistryClassView<'a>` impls `contract::class_view::ClassView` over a borrowed live `OntologyRegistry`: `class_id → shape`. DOLCE resolved LATE from the cache (`enumerate_first_with_entity_type_id(class) → MappingRow → ogit_uri → classify_odoo`), never stored on the row (OD-DOLCE "use the ontology cache" ratified). `dolce_id::{ENDURANT,PERDURANT,QUALITY,ABSTRACT}` = the stable u8 ids the contract trait returns (contract has no DOLCE enum; consumer maps back). Dep-inversion: contract owns vocabulary, ontology owns answers (ontology already deps contract).
6+
7+
**Honest scope (no fabrication):** resolves class existence + DOLCE + template from the live cache; the per-class field-SET (ObjectView, the bit-basis) is SUPPLIED, not enumerated — a MappingRow is a single entity's leaf row with no field-list. Field enumeration = the deferred D-CLS structural-signature audit (scope: 64 curated consts). No field-set fabricated.
8+
9+
**Review→fix caught a real perf gap (the ///-review-fix pipeline working):** `registry::enumerate_first_with_entity_type_id` is an O(n) row-scan + FULL MappingRow clone — called per dolce_category_id would be O(n)-with-heavy-clone per render. Fixed at MY layer: per-class `RefCell<HashMap>` memo (DOLCE is stable per class → scan once, not per call) + documented the underlying registry `by_entity_type_id` index as a deferred registry slice. No registry edit (collision avoidance). 4 teeth-tests (incl memo-stability) + 234 ontology lib green; clippy+fmt clean.
10+
11+
**Next (deferred D-CLS):** the structural-signature audit (64 curated OdooEntity → ObjectView field-sets + shape-family group-by-on-structural-hash, NOT aerial-cluster); the registry `by_entity_type_id` O(1) index; the askama render crate consuming `project()`.
12+
13+
**Cross-ref:** D-CLS-FM (the contract trait this implements); #440 plan; OD-DOLCE ratification (cache-resolves); `registry::enumerate_first_with_entity_type_id` (the O(n) gap, memoized); `hydrators::dolce_odoo::classify_odoo` (the live DOLCE resolution); classes.md:39 (resolve-not-store).
114
## 2026-05-31 — SHIPPED-in-PR: D-CLS-FM — class_view (FieldMask + ClassView meta-DTO; the class flies ABOVE the agnostic SoA)
215

316
**Status:** SHIPPED-in-PR (#440 D-CLS contract foundation). The XML-parse framing made real, OD-gates ratified.

.claude/board/STATUS_BOARD.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -569,6 +569,7 @@ Plan path: `.claude/plans/unified-soa-convergence-v1.md`. Handover `.claude/hand
569569
| D-MBX-A6-P3a | StyleStrategy: thinking-style -> cluster -> mechanism -> recipe_kernels Tactic selection (planning substrate; carries tau JIT addr) | lance-graph-planner | 130 | LOW | **In PR** | #439; first cut of A6-P3 consumer wiring; planner now consumes contract recipes/styles; deferred: i4-32D decode, Outcome->Candidate, tau->JIT, membrane commit |
570570
| D-MBX-A6-P3-M1 | `Tactic::requires() -> ThoughtMask` + `ThoughtField`/`ThoughtMask` (checklist-as-data keystone): 34 tactics declare their ThoughtCtx field-reads; `covered_by` = reliability-coverage gate | lance-graph-contract | 120 | LOW | **In PR** | #439; the panel-recalibrated keystone (extraction not construction); makes P1/P7/P11 derived; teeth-test asserts masks varied not stub |
571571
| D-CLS-FM | `class_view`: FieldMask(u64 presence) + ClassView meta-DTO resolver trait + ClassProjection (the class flies ABOVE the SoA; labels resolved late from OGIT cache, zero in the bytes) — extends ObjectView, reuses class_id | lance-graph-contract | 270 | LOW | **In PR** | #440 D-CLS contract foundation; OD-gates ratified; presence!=semantics (C2); N3 stable positions; 3 teeth-tests |
572+
| D-CLS-RES | `class_resolver`: `RegistryClassView` impls `ClassView` over the live OntologyRegistry — the ontology-side 'parser' (class_id -> shape, DOLCE resolved LATE via classify_odoo from the cache URI, memoized over the O(n) registry scan) | lance-graph-ontology | 200 | LOW | **In PR** | #440 D-CLS; makes the contract trait live; field-set supplied (D-CLS audit deferred); 4 teeth-tests |
572573

573574
---
574575

Lines changed: 217 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,217 @@
1+
//! # `class_resolver` — the ontology-side `impl ClassView` (the "parser" over the OGIT cache).
2+
//!
3+
//! This is the layer the contract [`ClassView`](lance_graph_contract::class_view::ClassView)
4+
//! trait inverts onto: the contract owns the *vocabulary* (FieldMask presence bits,
5+
//! the resolver trait); this crate owns the *answers* — it resolves a `class_id`
6+
//! into its shape (template, DOLCE category, field labels) **late, from the live
7+
//! `OntologyRegistry` cache**, so the SoA row never stores a label.
8+
//!
9+
//! ## What "meta lookup" means here (vs the leaf hashtable)
10+
//!
11+
//! [`OntologyRegistry`] today is the hashtable doing *single* lookups:
12+
//! `enumerate_first_with_entity_type_id(class_id) -> MappingRow` is one key → one
13+
//! row. [`RegistryClassView`] composes that leaf lookup with the per-class
14+
//! [`ObjectView`](lance_graph_contract::ontology::ObjectView) field-set into the
15+
//! *meta* lookup the class needs: `class_id -> (ordered fields + labels + template
16+
//! + DOLCE)`.
17+
//!
18+
//! ## Honest scope (what resolves today vs what's deferred)
19+
//!
20+
//! - **Resolves live from the cache:** the class's *existence* + its DOLCE category
21+
//! (via [`classify_odoo`] over the row's OGIT URI — OD-DOLCE "use the ontology
22+
//! cache") + its render template.
23+
//! - **Field-set is supplied, not yet enumerated:** a `MappingRow` is a single
24+
//! entity's leaf row; it does **not** carry an enumerated field-set with labels.
25+
//! That enumeration is the deferred D-CLS structural-signature audit. Until it
26+
//! lands, the per-class `ObjectView`s are passed in (the bit-basis), and this
27+
//! adapter resolves everything *else* from the cache. No field-set is fabricated.
28+
29+
use std::cell::RefCell;
30+
use std::collections::HashMap;
31+
32+
use lance_graph_contract::class_view::{ClassId, ClassView, FieldMask};
33+
use lance_graph_contract::ontology::{DisplayTemplate, FieldRef, ObjectView};
34+
35+
use crate::hydrators::dolce_odoo::{classify_odoo, DolceCategory};
36+
use crate::registry::OntologyRegistry;
37+
38+
/// Stable `u8` ids for the DOLCE upper categories — the opaque category id the
39+
/// contract [`ClassView::dolce_category_id`] returns (the contract has no DOLCE
40+
/// enum; consumers map this back). Positions are append-only (N3 discipline).
41+
///
42+
/// [`ClassView::dolce_category_id`]: lance_graph_contract::class_view::ClassView::dolce_category_id
43+
pub mod dolce_id {
44+
/// DOLCE Endurant (persistent stateful object) — the default.
45+
pub const ENDURANT: u8 = 0;
46+
/// DOLCE Perdurant (event / process).
47+
pub const PERDURANT: u8 = 1;
48+
/// DOLCE Quality (inhering property — VAT rate, currency).
49+
pub const QUALITY: u8 = 2;
50+
/// DOLCE Abstract object (template, OGIT class).
51+
pub const ABSTRACT: u8 = 3;
52+
}
53+
54+
/// Map a resolved [`DolceCategory`] to its stable `u8` id (the cache's opaque
55+
/// category id, per the contract `ClassView` contract).
56+
fn dolce_to_id(c: DolceCategory) -> u8 {
57+
match c {
58+
DolceCategory::Endurant => dolce_id::ENDURANT,
59+
DolceCategory::Perdurant => dolce_id::PERDURANT,
60+
DolceCategory::Quality => dolce_id::QUALITY,
61+
DolceCategory::AbstractEntity => dolce_id::ABSTRACT,
62+
}
63+
}
64+
65+
/// The ontology-side [`ClassView`] — resolves a class's shape from the live cache.
66+
///
67+
/// Holds a borrow of the [`OntologyRegistry`] (the cache) plus the per-class
68+
/// [`ObjectView`] field-sets (the bit-basis the registry does not yet enumerate).
69+
/// It RESOLVES; it does not STORE labels on any row (classes.md:39).
70+
pub struct RegistryClassView<'a> {
71+
registry: &'a OntologyRegistry,
72+
/// `class_id -> its ObjectView` (ordered fields + template). The field
73+
/// enumeration is the deferred D-CLS audit; supplied here as the bit-basis.
74+
views: HashMap<ClassId, ObjectView>,
75+
/// Empty fallback so `fields()` can return a `&[FieldRef]` for unknown classes.
76+
empty: Vec<FieldRef>,
77+
/// Memo of `class_id -> resolved DOLCE id`. A class's DOLCE category is stable
78+
/// (its OGIT URI does not change), and the underlying registry lookup is an
79+
/// O(n) scan + full `MappingRow` clone (`registry::enumerate_first_with_entity_type_id`
80+
/// — a known perf gap; the `by_entity_type_id` index is a deferred registry slice).
81+
/// Memoizing here makes the scan happen at most once per class, not per render call.
82+
dolce_memo: RefCell<HashMap<ClassId, u8>>,
83+
}
84+
85+
impl<'a> RegistryClassView<'a> {
86+
/// Build over the live registry with the per-class field-set views.
87+
pub fn new(registry: &'a OntologyRegistry, views: HashMap<ClassId, ObjectView>) -> Self {
88+
Self {
89+
registry,
90+
views,
91+
empty: Vec::new(),
92+
dolce_memo: RefCell::new(HashMap::new()),
93+
}
94+
}
95+
96+
/// Does the cache know this class? (a real leaf lookup against the registry)
97+
pub fn is_known(&self, class: ClassId) -> bool {
98+
self.registry
99+
.enumerate_first_with_entity_type_id(class)
100+
.is_some()
101+
}
102+
}
103+
104+
impl ClassView for RegistryClassView<'_> {
105+
fn fields(&self, class: ClassId) -> &[FieldRef] {
106+
self.views
107+
.get(&class)
108+
.map(|v| v.fields.as_slice())
109+
.unwrap_or(&self.empty)
110+
}
111+
112+
fn template(&self, class: ClassId) -> DisplayTemplate {
113+
self.views
114+
.get(&class)
115+
.map(|v| v.display_template.clone())
116+
// Default render is the compact Card when no per-class view is supplied.
117+
.unwrap_or(DisplayTemplate::Card)
118+
}
119+
120+
fn dolce_category_id(&self, class: ClassId) -> u8 {
121+
if let Some(&id) = self.dolce_memo.borrow().get(&class) {
122+
return id;
123+
}
124+
// Resolve DOLCE LATE from the cache: class_id -> MappingRow -> OGIT URI ->
125+
// classify_odoo. Never stored on the row (OD-DOLCE "use the ontology cache").
126+
let id = match self.registry.enumerate_first_with_entity_type_id(class) {
127+
Some(row) => dolce_to_id(classify_odoo(row.ogit_uri.as_str())),
128+
// Unknown class → the default persistent-object category.
129+
None => dolce_id::ENDURANT,
130+
};
131+
self.dolce_memo.borrow_mut().insert(class, id);
132+
id
133+
}
134+
}
135+
136+
#[cfg(test)]
137+
mod tests {
138+
use super::*;
139+
use lance_graph_contract::ontology::DisplayTemplate;
140+
141+
fn invoice_view() -> ObjectView {
142+
ObjectView::new(
143+
DisplayTemplate::Detail,
144+
vec![
145+
FieldRef::new("amount_total", "Total"),
146+
FieldRef::new("amount_tax", "Tax"),
147+
FieldRef::new("partner_id", "Partner"),
148+
],
149+
)
150+
}
151+
152+
#[test]
153+
fn resolves_field_set_and_template_from_supplied_view() {
154+
let reg = OntologyRegistry::new_in_memory();
155+
let mut views = HashMap::new();
156+
views.insert(7u16, invoice_view());
157+
let cv = RegistryClassView::new(&reg, views);
158+
159+
// The class's ordered field-set IS the bit basis (positions 0/1/2).
160+
assert_eq!(cv.field_count(7), 3);
161+
assert_eq!(cv.field_label(7, 0), Some("Total"));
162+
assert_eq!(cv.field_label(7, 2), Some("Partner"));
163+
assert_eq!(cv.template(7), DisplayTemplate::Detail);
164+
165+
// Unknown class: empty field-set, default Card template — no panic.
166+
assert_eq!(cv.field_count(999), 0);
167+
assert_eq!(cv.template(999), DisplayTemplate::Card);
168+
}
169+
170+
#[test]
171+
fn projects_above_an_agnostic_class_mask_with_labels_from_the_resolver() {
172+
let reg = OntologyRegistry::new_in_memory();
173+
let mut views = HashMap::new();
174+
views.insert(7u16, invoice_view());
175+
let cv = RegistryClassView::new(&reg, views);
176+
177+
// The "SoA" supplies only (class_id=7, mask). Tax (pos 1) is off.
178+
let mask = FieldMask::from_positions(&[0, 2]);
179+
let rendered: Vec<&str> = cv
180+
.project(7, mask)
181+
.filter(|(_, present)| *present)
182+
.map(|(f, _)| f.label.as_str())
183+
.collect();
184+
assert_eq!(
185+
rendered,
186+
vec!["Total", "Partner"],
187+
"labels resolved by the meta-DTO above the SoA; off-bit Tax skipped"
188+
);
189+
}
190+
191+
#[test]
192+
fn dolce_resolves_from_the_cache_not_the_row() {
193+
// Empty registry → unknown class falls back to the default category, and
194+
// `is_known` honestly reports the leaf lookup miss (no fabrication).
195+
let reg = OntologyRegistry::new_in_memory();
196+
let cv = RegistryClassView::new(&reg, HashMap::new());
197+
assert!(!cv.is_known(7), "empty cache: class is not known");
198+
assert_eq!(
199+
cv.dolce_category_id(7),
200+
dolce_id::ENDURANT,
201+
"unknown class → default persistent-object category"
202+
);
203+
// Memoized: a second call returns the same id (and skips the O(n) re-scan).
204+
assert_eq!(cv.dolce_category_id(7), dolce_id::ENDURANT);
205+
}
206+
207+
#[test]
208+
fn dolce_id_mapping_is_total_and_stable() {
209+
assert_eq!(dolce_to_id(DolceCategory::Endurant), dolce_id::ENDURANT);
210+
assert_eq!(dolce_to_id(DolceCategory::Perdurant), dolce_id::PERDURANT);
211+
assert_eq!(dolce_to_id(DolceCategory::Quality), dolce_id::QUALITY);
212+
assert_eq!(
213+
dolce_to_id(DolceCategory::AbstractEntity),
214+
dolce_id::ABSTRACT
215+
);
216+
}
217+
}

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
3838
pub mod bridge;
3939
pub mod bridges;
40+
pub mod class_resolver;
4041
pub mod error;
4142
pub mod foundry_map;
4243
pub mod hydrators;

0 commit comments

Comments
 (0)