Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions .claude/board/entries/2026-10-31-behaviour-lens-full-classid-l1.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# D-DT64-7-L1: behaviour lenses read their basis by the full classid — 2026-10-31

**Status:** MEASURED (`crates/lance-graph-dt64/tests/bind_store.rs`, `crates/lance-graph-r2il/tests/integration/polyfill*.rs`). Implements decision O9 (plan §1d), step 2 of the execution order.

## Contract change and its compatibility impact
- `lance_graph_contract::class_view::ClassView` gains `fn lens_fields(&self, lens: u32) -> Option<&[FieldRef]>` with a default that returns `None`. **Source-compatible:** every existing `impl ClassView` compiles unchanged (a test pins this with a view that implements only the required methods).
- New `class_view::is_behaviour_lens(classid) -> bool`: right (custom) half in `0xF000..=0xFFFF`.
- `lance_graph_dt64::Refusal` gains `UnknownLensBasis(u32)`. No crate matches `Refusal` exhaustively (searched across `crates/`), so no match breaks.

## Behaviour change
- `BindStore::resolve_algebra` reads a behaviour lens's basis through `lens_fields(lens)`. `None` refuses with `UnknownLensBasis`; the left half's basis is never inherited, even when its field count matches.
- Unchanged: every lens with a right half below `0xF000` (the `0xC401` R2IL reading, the lab lenses `0x7F0A_0001` and friends) and every data class (`resolve`) keep the left-half lookup.
- The one lens on `main` this moves is the polyfill lens `0xFFFF_F000` (#1899). A caller that passed a view answering it only through `fields(0xFFFF)` now gets `UnknownLensBasis`, which is O9's intent. `PolyfillView` answers it through `lens_fields` and no longer holds a concept basis; `WithPolyfill` routes `lens_fields` for lenses under `0xFFFF`. The existing polyfill test view gained `lens_fields`; its assertions are unchanged.

## Measured
- Red first: on the old `resolve_algebra`, 4 of the 5 new DT64 tests failed; the compatibility twin passed.
- Two lenses under `0xFFFF` with 17 and 5 words resolve independently; a data class `0x7F0B_0001` and a behaviour lens `0x7F0B_F001` do not cross-read; an unknown behaviour lens refuses even when the concept's basis has the right count.
- Disable runs, all red: behaviour branch off (4 fail), inherit the concept basis on an unknown lens (2), every lens treated as a behaviour lens (2), `WithPolyfill` not routing `lens_fields` (2).
- Dependents checked: `lance-graph-contract` (1562 + others), `lance-graph-dt64`, `lance-graph-r2il`, `xlog-relcomp-probe`, `dt64-admission-probe` green; their lenses use right halves `0x0000`/`0x0001`.

Not widened: `AlgebraRef.word`, `WideFieldMask`, the 256-position limit.
10 changes: 10 additions & 0 deletions .claude/board/entries/2026-10-31-polyfill-alias-views-l4.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# D-DT64-7 L4: production alias views for the polyfill lens — 2026-10-31

**Status:** MEASURED (`crates/lance-graph-r2il/tests/integration/polyfill_views.rs`). First step of the execution order in plan §1d.

- Each `POLYFILL` word records which of its existing aliases is its SQL name and which its Quack name (`sql_at`, `quack_at`); no name is spelled twice and no spelling rule picks the language. `FrozenWord::{name_of, name_in, is_alias}`.
- `PolyfillView::new(AliasLanguage)` answers `0xFFFF` with one label per position. The `predicate_iri` names the word (`lg:polyfill:f000/<R2IL name>`), the label the language; a position with no name in that language carries its R2IL name (SQL: `SUM`, `MIN`, `MAX`, `ALL`, where SQL's empty-selection `NULL` differs).
- `WithPolyfill::new(&consumer, &view)` answers `0xFFFF` from the polyfill view and delegates every other concept, template and DOLCE category to the consumer's view. It checks no activation (L3).
- Measured: R2IL, SQL and Quack views over the same consumer view bind the same steps and plan id; the consumer's own stale `0xFFFF` labels are refused alone and hidden behind the adapter.
- Disable runs, all red: SQL reading the Quack index (2 fail), one wrong index (2), no R2IL fallback (3), the polyfill view answering every concept (1), the adapter not routing `0xFFFF` (2), a per-language IRI (1), the adapter dropping the consumer's DOLCE category (1).
- Unchanged: `binding.rs`, the `0xC401` reading, the 9 existing polyfill tests.
4 changes: 3 additions & 1 deletion .claude/board/entries/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,14 +25,15 @@ index row, (3) no duplicate entry id. Checks 1 and 2 are deliberately
opposite directions; the stranding this convention prevents shows up in
exactly one of them, never both.

534 entries, 2026-08-06 .. 2026-10-31.
536 entries, 2026-08-06 .. 2026-10-31.

| date | entry id | finding | file |
|---|---|---|---|
| 2026-10-31 | `x2-dt64-admission-policy` | | [2026-10-31-x2-dt64-admission-policy.md](2026-10-31-x2-dt64-admission-policy.md) |
| 2026-10-31 | `r1-r2il-binding-reading` | | [2026-10-31-r1-r2il-binding-reading.md](2026-10-31-r1-r2il-binding-reading.md) |
| 2026-10-31 | `r1-native-prepared-successor` | | [2026-10-31-r1-native-prepared-successor.md](2026-10-31-r1-native-prepared-successor.md) |
| 2026-10-31 | `polyfill-positional-lens` | | [2026-10-31-polyfill-positional-lens.md](2026-10-31-polyfill-positional-lens.md) |
| 2026-10-31 | `D-DT64-7` | | [2026-10-31-polyfill-alias-views-l4.md](2026-10-31-polyfill-alias-views-l4.md) |
| 2026-10-31 | `ogar-behavior-probe-p2-p3-p9` | | [2026-10-31-ogar-behavior-probe-p2-p3-p9.md](2026-10-31-ogar-behavior-probe-p2-p3-p9.md) |
| 2026-10-31 | `D-SAG-6` | | [2026-10-31-kythe-bridge-ra-facts.md](2026-10-31-kythe-bridge-ra-facts.md) |
| 2026-10-31 | `D-XLOG-NATIVE-6` | | [2026-10-31-d-xlog-native-6-ndarray-simd-execution.md](2026-10-31-d-xlog-native-6-ndarray-simd-execution.md) |
Expand All @@ -47,6 +48,7 @@ exactly one of them, never both.
| 2026-10-31 | `D-DYNBAG-X` | | [2026-10-31-cross-frontend-projection-crosswalk.md](2026-10-31-cross-frontend-projection-crosswalk.md) |
| 2026-10-31 | `coresearch-mandatory-codegraph` | | [2026-10-31-coresearch-mandatory-codegraph.md](2026-10-31-coresearch-mandatory-codegraph.md) |
| 2026-10-31 | `D-STAUNEN` | | [2026-10-31-coresearch-d-staunen-arc.md](2026-10-31-coresearch-d-staunen-arc.md) |
| 2026-10-31 | `D-DT64-7-L1` | | [2026-10-31-behaviour-lens-full-classid-l1.md](2026-10-31-behaviour-lens-full-classid-l1.md) |
| 2026-10-30 | `D-XLOG-NATIVE-0` | | [2026-10-30-d-xlog-native-crosswalk.md](2026-10-30-d-xlog-native-crosswalk.md) |
| 2026-10-30 | `D-XLOG-DR64-0` | XLOG's CPU frontend lowers onto DR64 relcomp (answer {2}, 2 oracles); its optimizer emits ChainJoin and swapped orders; arrow 60 vs 58 blocks in-tree use; masks lose to scalar on selective data | [2026-10-30-d-xlog-dr64-0-xlog-rir-over-dr64-relcomp.md](2026-10-30-d-xlog-dr64-0-xlog-rir-over-dr64-relcomp.md) |
| 2026-10-30 | `D-TRACT-SHAPE-1` | | [2026-10-30-d-tract-shape-1-cold-shape-folding.md](2026-10-30-d-tract-shape-1-cold-shape-folding.md) |
Expand Down
26 changes: 26 additions & 0 deletions crates/lance-graph-contract/src/class_view.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1046,6 +1046,14 @@ pub fn execute_defaults<S, E>(
/// "Single lookup" (leaf, today) vs "meta lookup" (the class, this trait): a
/// single lookup is `uri → row`; a meta lookup is `class_id → shape`, composing
/// many leaf lookups into one projected view.
/// Whether `classid` addresses a behaviour lens: its right (custom) half is
/// in `0xF000..=0xFFFF` (D-DT64-7 O9). A behaviour lens's basis is read with
/// [`ClassView::lens_fields`], by the full classid.
#[must_use]
pub const fn is_behaviour_lens(classid: u32) -> bool {
crate::ogar_codebook::split_classid(classid).1 >= 0xF000
}

pub trait ClassView {
/// The class's ordered field set — the bit basis. Position `i` in this slice
/// is the stable [`FieldMask`] bit `i` (N3 append-only). This IS the
Expand All @@ -1060,6 +1068,24 @@ pub trait ClassView {
/// as the cache's opaque category id; the consumer maps it to its own enum.
fn dolce_category_id(&self, class: ClassId) -> u8;

/// The field basis of the behaviour lens `lens`, by its FULL classid.
///
/// A behaviour lens is a classid whose right (custom) half is in
/// `0xF000..=0xFFFF` ([`is_behaviour_lens`]). Several lenses can share a
/// left half (`0xFFFF_F000` and `0xFFFF_F200`, or a consumer's data class
/// `0x7F0B_0001` and its behaviour lens `0x7F0B_F001`), so their bases
/// cannot be read from [`fields`](Self::fields), which is keyed by the
/// left half alone.
///
/// `None` means this view does not know the lens. A reader must then
/// refuse; it must not fall back to the left half's basis. The default
/// answers `None` for every lens, so a view that never implements this
/// keeps its current behaviour for every other classid.
fn lens_fields(&self, lens: u32) -> Option<&[FieldRef]> {
let _ = lens;
None
}

/// The label of field position `n` in `class`, resolved late from the cache
/// (locale resolution is the consumer's job). `None` if `n` is out of range.
fn field_label(&self, class: ClassId, n: u8) -> Option<&str> {
Expand Down
29 changes: 27 additions & 2 deletions crates/lance-graph-dt64/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@

use std::collections::BTreeMap;

use lance_graph_contract::class_view::{ClassId, ClassView, WideFieldMask};
use lance_graph_contract::class_view::{is_behaviour_lens, ClassId, ClassView, WideFieldMask};
use lance_graph_contract::ogar_codebook::classid_concept;
use lance_graph_contract::ontology::FieldRef;
use lance_graph_contract::property::{PropertySchema, PropertySpec};
Expand Down Expand Up @@ -285,6 +285,9 @@ impl MethodRef {
pub enum Refusal {
/// The classid has no binding.
UnknownClass(u32),
/// A behaviour lens (right half `0xF###`) whose basis the view does not
/// know by its full classid. It is never read from its left half.
UnknownLensBasis(u32),
/// The classid is bound to other behaviour (D5). Override with a derived
/// classid and a re-route.
BehaviourImmutable(u32),
Expand Down Expand Up @@ -669,6 +672,18 @@ impl BindStore {
schema_rev: SchemaRev,
) -> Result<Resolved<'s, 'v>, Refusal> {
let bound = self.algebra.get(&lens).ok_or(Refusal::UnknownClass(lens))?;
if is_behaviour_lens(lens) {
let fields = view
.lens_fields(lens)
.ok_or(Refusal::UnknownLensBasis(lens))?;
return check_count(lens, &bound.schema, schema_rev, fields).map(|fields| Resolved {
class: lens,
schema: &bound.schema,
fields,
methods: &[],
derived_from: None,
});
}
check_basis(view, lens, &bound.schema, schema_rev).map(|fields| Resolved {
class: lens,
schema: &bound.schema,
Expand Down Expand Up @@ -829,6 +844,17 @@ fn check_basis<'v, V: ClassView + ?Sized>(
class: u32,
schema: &ClassSchema,
schema_rev: SchemaRev,
) -> Result<&'v [FieldRef], Refusal> {
check_count(class, schema, schema_rev, view.fields(view_class(class)))
}

/// The revision and field-count checks of [`check_basis`], over a basis
/// already looked up.
fn check_count<'v>(
class: u32,
schema: &ClassSchema,
schema_rev: SchemaRev,
fields: &'v [FieldRef],
) -> Result<&'v [FieldRef], Refusal> {
if schema.schema_rev != schema_rev {
return Err(Refusal::SchemaRevisionMismatch {
Expand All @@ -837,7 +863,6 @@ fn check_basis<'v, V: ClassView + ?Sized>(
asked: schema_rev,
});
}
let fields = view.fields(view_class(class));
if fields.len() != schema.field_count {
return Err(Refusal::FieldBasisChanged {
class,
Expand Down
135 changes: 135 additions & 0 deletions crates/lance-graph-dt64/tests/bind_store.rs
Original file line number Diff line number Diff line change
Expand Up @@ -964,3 +964,138 @@ fn jina_reserves_the_dr256_identity_space_and_claims_only_routed_tiles() {
"a refused route moves no version"
);
}

// --------------------------------------------------------------------------
// D-DT64-7 O9 / L1: a behaviour lens (right half 0xF###) reads its basis by
// its full classid, through ClassView::lens_fields; it never inherits the
// basis of its left half.

/// A view holding concept bases and behaviour-lens bases separately.
struct Lenses {
concepts: Vec<(ClassId, Vec<FieldRef>)>,
lenses: Vec<(u32, Vec<FieldRef>)>,
}

fn labels(prefix: &str, n: usize) -> Vec<FieldRef> {
(0..n)
.map(|i| FieldRef::new(format!("urn:{prefix}/{i}"), format!("{prefix}{i}")))
.collect()
}

impl ClassView for Lenses {
fn fields(&self, class: ClassId) -> &[FieldRef] {
self.concepts
.iter()
.find(|(c, _)| *c == class)
.map_or(&[], |(_, f)| f.as_slice())
}
fn lens_fields(&self, lens: u32) -> Option<&[FieldRef]> {
self.lenses
.iter()
.find(|(l, _)| *l == lens)
.map(|(_, f)| f.as_slice())
}
fn template(&self, _class: ClassId) -> DisplayTemplate {
DisplayTemplate::Detail
}
fn dolce_category_id(&self, _class: ClassId) -> u8 {
0
}
}

const RELALG_LENS: u32 = 0xFFFF_F000;
const ONNX_LENS: u32 = 0xFFFF_F200;
const UNKNOWN_LENS: u32 = 0xFFFF_F300;

#[test]
fn two_behaviour_lenses_under_one_left_half_resolve_independently() {
let mut s = BindStore::new();
s.declare_algebra(RELALG_LENS, SchemaRev(1), Extent::DR64_CUBE, vec![2; 17])
.unwrap();
s.declare_algebra(ONNX_LENS, SchemaRev(1), Extent::DR64_CUBE, vec![1; 5])
.unwrap();
let view = Lenses {
// The concept 0xFFFF itself carries a basis of neither size.
concepts: vec![(0xFFFF, labels("concept", 3))],
lenses: vec![
(RELALG_LENS, labels("relalg", 17)),
(ONNX_LENS, labels("onnx", 5)),
],
};
let relalg = s.resolve_algebra(&view, RELALG_LENS, SchemaRev(1)).unwrap();
let onnx = s.resolve_algebra(&view, ONNX_LENS, SchemaRev(1)).unwrap();
assert_eq!(relalg.fields.len(), 17);
assert_eq!(onnx.fields.len(), 5);
assert_eq!(relalg.fields[0].label, "relalg0");
assert_eq!(onnx.fields[0].label, "onnx0");
}

#[test]
fn a_data_class_and_a_behaviour_lens_sharing_a_left_half_do_not_cross_read() {
const DATA: u32 = 0x7F0B_0001;
const LENS: u32 = 0x7F0B_F001;
let mut s = BindStore::new();
s.register(DATA, schema(1, 2), vec![]).unwrap();
s.declare_algebra(LENS, SchemaRev(1), Extent::DR64_CUBE, vec![1; 3])
.unwrap();
let view = Lenses {
concepts: vec![(0x7F0B, labels("data", 2))],
lenses: vec![(LENS, labels("word", 3))],
};
let data = s.resolve(&view, DATA, SchemaRev(1)).unwrap();
let lens = s.resolve_algebra(&view, LENS, SchemaRev(1)).unwrap();
assert_eq!(data.fields.len(), 2);
assert_eq!(data.fields[1].label, "data1");
assert_eq!(lens.fields.len(), 3);
assert_eq!(lens.fields[2].label, "word2");
}

#[test]
fn an_unknown_behaviour_lens_fails_closed_instead_of_inheriting_its_concept() {
let mut s = BindStore::new();
s.declare_algebra(UNKNOWN_LENS, SchemaRev(1), Extent::DR64_CUBE, vec![1; 3])
.unwrap();
// The concept's basis has exactly the lens's word count: inheriting it
// would succeed, which is the failure this guards against.
let view = Lenses {
concepts: vec![(0xFFFF, labels("concept", 3))],
lenses: vec![],
};
assert_eq!(
s.resolve_algebra(&view, UNKNOWN_LENS, SchemaRev(1))
.unwrap_err(),
Refusal::UnknownLensBasis(UNKNOWN_LENS)
);
}

#[test]
fn a_lens_outside_the_behaviour_range_still_reads_its_concept() {
// Backward-compatibility twin: right half 0x0001 is not a behaviour
// lens, so the concept basis answers and `lens_fields` is not asked.
let mut s = BindStore::new();
s.declare_algebra(POLYFILL, SchemaRev(1), Extent::DR64_CUBE, ARITY.to_vec())
.unwrap();
let view = Lenses {
concepts: vec![(view_class(POLYFILL), labels("w", ARITY.len()))],
lenses: vec![(POLYFILL, labels("ignored", 1))],
};
let lens = s.resolve_algebra(&view, POLYFILL, SchemaRev(1)).unwrap();
assert_eq!(lens.fields.len(), ARITY.len());
assert_eq!(lens.fields[0].label, "w0");
}

#[test]
fn a_view_that_does_not_implement_lens_fields_still_compiles_and_refuses_behaviour_lenses() {
// `View` above implements only the required methods: the default
// `lens_fields` answers None, so a behaviour lens fails closed.
let mut s = BindStore::new();
s.declare_algebra(RELALG_LENS, SchemaRev(1), Extent::DR64_CUBE, vec![1; 2])
.unwrap();
let view = View::with_fields(0xFFFF, 2);
assert_eq!(view.lens_fields(RELALG_LENS), None);
assert_eq!(
s.resolve_algebra(&view, RELALG_LENS, SchemaRev(1))
.unwrap_err(),
Refusal::UnknownLensBasis(RELALG_LENS)
);
}
Loading
Loading