Skip to content

Commit d59994b

Browse files
committed
feat: TD-INT-5 corrected + TD-INT-8 + TD-INT-11 — palette distance, schema validation, neural-debug registry
TD-INT-5 (CORRECTED) — per-plane palette distance via ndarray: REVERTED hamming_in_slice from role_keys.rs — slicing/comparison algebra belongs in ndarray, not in lance-graph-contract. RoleKey stays Layer-2 catalogue (slice boundaries only). ADDED PlaneDistance in lance-graph-planner/cache/convergence.rs: wraps ndarray::hpc::palette_distance::SpoDistanceMatrices for per-plane O(1) comparison. Methods: spo_distance(), subject_distance(), predicate_distance(), object_distance(). Build once from palette codebooks, compare via pre-computed 256×256 L1 tables. This is architecturally correct: ndarray = hardware (SIMD, palette, Base17); lance-graph = thinking (calls ndarray). No bit ops in contract. TD-INT-8 — Schema validation on SPO commit: SpoBuilder::with_schema(Schema) + validate() + commit_validated() in lance-graph/graph/spo/builder.rs. Commit-time validation returns FailureTicket::missing_required on missing Required predicates. Existing SpoBuilder::commit() unchanged for backward compat. FailureTicket::missing_required constructor added to contract. TD-INT-11 — neural-debug runtime registry populated: RuntimeRegistry with record(row, NeuronState), snapshot() → Vec, global accessor via registry()/init_registry(). NeuronState enum (Alive/Static/NaN). Re-exported from neural-debug lib.rs. 10 tests passing. Full workspace cargo check clean. 423 lib tests pass across all affected crates. https://claude.ai/code/session_01SbYsmmbPf9YQuYbHZN52Zh
1 parent b39acdf commit d59994b

7 files changed

Lines changed: 515 additions & 67 deletions

File tree

crates/lance-graph-contract/src/grammar/free_energy.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -227,6 +227,7 @@ mod tests {
227227
tekamolo: TekamoloSlots::default(),
228228
wechsel: vec![],
229229
coverage: 0.0,
230+
missing_required: vec![],
230231
}
231232
}
232233

crates/lance-graph-contract/src/grammar/role_keys.rs

Lines changed: 0 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -78,47 +78,6 @@ impl RoleKey {
7878
self.slice_end - self.slice_start
7979
}
8080

81-
/// Hamming distance between two `Binary16K`-shaped fingerprints,
82-
/// restricted to this role's slice `[slice_start..slice_end)`.
83-
/// Bits outside the slice contribute zero. Used by the content
84-
/// cascade for role-indexed similarity (TD-INT-5): "compare two
85-
/// triples on the SUBJECT plane only" rather than over all 16384
86-
/// bits. Binary-space analogue of unbind+cosine on a role.
87-
pub fn hamming_in_slice(&self, fp_a: &[u64], fp_b: &[u64]) -> u32 {
88-
debug_assert!(fp_a.len() == fp_b.len(), "fingerprint widths differ");
89-
let mut total = 0u32;
90-
let start_word = self.slice_start / 64;
91-
let end_word = (self.slice_end + 63) / 64;
92-
for w in start_word..end_word.min(fp_a.len()) {
93-
let mut diff = fp_a[w] ^ fp_b[w];
94-
if w == start_word {
95-
let lo = self.slice_start % 64;
96-
if lo > 0 {
97-
diff &= !((1u64 << lo) - 1);
98-
}
99-
}
100-
if w + 1 == end_word {
101-
let hi = self.slice_end % 64;
102-
if hi > 0 {
103-
diff &= (1u64 << hi) - 1;
104-
}
105-
}
106-
total += diff.count_ones();
107-
}
108-
total
109-
}
110-
111-
/// Resonance score in `[0, 1]` for this role's slice. Higher = more similar.
112-
/// `1 - hamming / slice_width`, density-agnostic.
113-
pub fn resonance_in_slice(&self, fp_a: &[u64], fp_b: &[u64]) -> f32 {
114-
let width = self.slice_width() as f32;
115-
if width <= 0.0 {
116-
return 1.0;
117-
}
118-
let h = self.hamming_in_slice(fp_a, fp_b) as f32;
119-
(1.0 - h / width).clamp(0.0, 1.0)
120-
}
121-
12281
// NOTE: `bind/unbind/recovery_margin` methods removed in cleanup commit
12382
// `cd5c049...` (see CHANGELOG.md). Those operated on a hallucinated
12483
// `Vsa10k = [u64; 157]` bitpacked carrier with GF(2)/XOR algebra —
@@ -350,29 +309,6 @@ pub fn nars_inference_key(inf: NarsInference) -> &'static RoleKey {
350309
mod tests {
351310
use super::*;
352311

353-
#[test]
354-
fn hamming_in_slice_zero_for_identical() {
355-
let fp = vec![0xDEAD_BEEFu64; 256];
356-
assert_eq!(SUBJECT_KEY.hamming_in_slice(&fp, &fp), 0);
357-
}
358-
359-
#[test]
360-
fn hamming_in_slice_only_counts_within_slice() {
361-
let mut fp_a = vec![0u64; 256];
362-
let mut fp_b = vec![0u64; 256];
363-
fp_b[50] = 1u64 << 5; // outside SUBJECT slice — must not count
364-
assert_eq!(SUBJECT_KEY.hamming_in_slice(&fp_a, &fp_b), 0);
365-
fp_a[5] = 1u64 << 3; // inside SUBJECT slice — must count
366-
assert_eq!(SUBJECT_KEY.hamming_in_slice(&fp_a, &fp_b), 1);
367-
}
368-
369-
#[test]
370-
fn resonance_in_slice_full_match_is_one() {
371-
let fp = vec![0xAAAA_5555u64; 256];
372-
let r = SUBJECT_KEY.resonance_in_slice(&fp, &fp);
373-
assert!((r - 1.0).abs() < 1e-6);
374-
}
375-
376312
/// Collect every (start, end, label) from every defined role key.
377313
fn all_slices() -> Vec<(usize, usize, &'static str)> {
378314
let mut v: Vec<(usize, usize, &'static str)> = Vec::new();

crates/lance-graph-contract/src/grammar/ticket.rs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,7 @@ mod tests {
150150
tekamolo: TekamoloSlots::default(),
151151
wechsel: vec![],
152152
coverage: 0.33,
153+
missing_required: vec![],
153154
};
154155
assert!(t.needs_llm(0.9));
155156
}
@@ -160,4 +161,20 @@ mod tests {
160161
assert_eq!(c.plausible_count(), 2);
161162
assert!(!c.is_resolved(0.75));
162163
}
164+
165+
#[test]
166+
fn missing_required_constructor_preserves_predicate_names() {
167+
let t = FailureTicket::missing_required(vec!["customer_name", "tax_id"]);
168+
let m: Vec<&'static str> = t.missing_predicates().collect();
169+
assert_eq!(m, vec!["customer_name", "tax_id"]);
170+
assert_eq!(t.recommended_next, NarsInference::Abduction);
171+
assert_eq!(t.coverage, 0.0);
172+
assert!(t.needs_llm(0.9), "schema miss must escalate");
173+
}
174+
175+
#[test]
176+
fn missing_required_constructor_empty_for_no_misses() {
177+
let t = FailureTicket::missing_required(vec![]);
178+
assert_eq!(t.missing_predicates().count(), 0);
179+
}
163180
}

crates/lance-graph-planner/src/cache/convergence.rs

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,52 @@
2020
2121
use super::kv_bundle::HeadPrint;
2222
use super::nars_engine::{SpoHead, MASK_SPO, CausalEdge64};
23+
use ndarray::hpc::palette_distance::{Palette, DistanceMatrix, SpoDistanceMatrices};
24+
use ndarray::hpc::bgz17_bridge::SpoBase17;
25+
26+
/// Per-plane palette distance context (TD-INT-5).
27+
///
28+
/// Wraps ndarray's `SpoDistanceMatrices` (pre-computed 256×256 per-plane
29+
/// L1 distance tables). All comparison algebra lives in ndarray; this
30+
/// struct is a session-scoped handle that the planner cache and cascade
31+
/// use to compare triplets on individual role planes (subject-only,
32+
/// predicate-only, etc.) without doing bit ops in lance-graph.
33+
///
34+
/// Build once from the palette codebooks; compare in O(1) per pair.
35+
pub struct PlaneDistance {
36+
matrices: SpoDistanceMatrices,
37+
}
38+
39+
impl PlaneDistance {
40+
/// Build from three palettes (one per S/P/O plane).
41+
pub fn build(s_pal: &Palette, p_pal: &Palette, o_pal: &Palette) -> Self {
42+
Self { matrices: SpoDistanceMatrices::build(s_pal, p_pal, o_pal) }
43+
}
44+
45+
/// Combined S+P+O distance. O(1): three table lookups.
46+
#[inline]
47+
pub fn spo_distance(&self, a: &SpoHead, b: &SpoHead) -> u32 {
48+
self.matrices.spo_distance(a.s_idx, a.p_idx, a.o_idx, b.s_idx, b.p_idx, b.o_idx)
49+
}
50+
51+
/// Subject-plane only distance. O(1): one table lookup.
52+
#[inline]
53+
pub fn subject_distance(&self, a: &SpoHead, b: &SpoHead) -> u16 {
54+
self.matrices.subject.distance(a.s_idx, b.s_idx)
55+
}
56+
57+
/// Predicate-plane only distance. O(1): one table lookup.
58+
#[inline]
59+
pub fn predicate_distance(&self, a: &SpoHead, b: &SpoHead) -> u16 {
60+
self.matrices.predicate.distance(a.p_idx, b.p_idx)
61+
}
62+
63+
/// Object-plane only distance. O(1): one table lookup.
64+
#[inline]
65+
pub fn object_distance(&self, a: &SpoHead, b: &SpoHead) -> u16 {
66+
self.matrices.object.distance(a.o_idx, b.o_idx)
67+
}
68+
}
2369

2470
/// Convert an SPO triplet (as strings) into a HeadPrint fingerprint.
2571
///
@@ -165,6 +211,34 @@ pub fn episodes_to_palette_layers(
165211
mod tests {
166212
use super::*;
167213

214+
#[test]
215+
fn test_plane_distance_subject_only() {
216+
// Build a 256-entry palette (production size) from spread Base17 patterns
217+
let patterns: Vec<ndarray::hpc::bgz17_bridge::Base17> = (0..256)
218+
.map(|i| {
219+
let mut b = ndarray::hpc::bgz17_bridge::Base17::zero();
220+
b.dims[0] = (i as i16).wrapping_mul(127);
221+
b.dims[1] = (i as i16).wrapping_mul(31);
222+
b
223+
})
224+
.collect();
225+
let pal = Palette::build(&patterns, 256, 1);
226+
let pd = PlaneDistance::build(&pal, &pal, &pal);
227+
228+
// Same palette index → zero distance
229+
let a = headprint_to_spo(&triplet_to_headprint("Alice", "knows", "Bob"), 0.9, 0.8);
230+
assert_eq!(pd.subject_distance(&a, &a), 0);
231+
assert_eq!(pd.spo_distance(&a, &a), 0);
232+
233+
// Different triplets → likely nonzero distance
234+
let b = headprint_to_spo(&triplet_to_headprint("Zephyr", "loves", "Qux"), 0.9, 0.8);
235+
let combined = pd.spo_distance(&a, &b);
236+
let sub_only = pd.subject_distance(&a, &b);
237+
// Subject-only ≤ combined (since combined adds P + O)
238+
assert!(sub_only as u32 <= combined,
239+
"subject-only {} should be <= combined {}", sub_only, combined);
240+
}
241+
168242
#[test]
169243
fn test_triplet_to_headprint() {
170244
let fp = triplet_to_headprint("Claude", "reasons_like", "Opus4.6");

0 commit comments

Comments
 (0)