Skip to content

Commit 9b22c39

Browse files
committed
B3: D6 role-key catalogue with contiguous slice addressing
1 parent f01f5f8 commit 9b22c39

1 file changed

Lines changed: 313 additions & 0 deletions

File tree

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

Lines changed: 313 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -314,6 +314,154 @@ pub static BANK_KEY: LazyLock<RoleKey> = LazyLock::new(|| RoleKey::generate
314314
pub static FIBU_KEY: LazyLock<RoleKey> = LazyLock::new(|| RoleKey::generate("smb.fibu", 13_072, 13_584));
315315
pub static STEUER_KEY: LazyLock<RoleKey> = LazyLock::new(|| RoleKey::generate("smb.steuer", 13_584, 14_096));
316316

317+
// ---------------------------------------------------------------------------
318+
// D6 — RoleKeySlice catalogue (const-addressable [start:stop) slices + FNV-64
319+
// fingerprint over the role label). This layer is the **catalogue index** for
320+
// the live `RoleKey` static instances above: same boundaries, no duplication
321+
// of the bipolar payload — just `Copy`/`const`-friendly descriptors that can
322+
// be embedded in tables, dispatch maps, or codecs without taking a LazyLock.
323+
//
324+
// `RoleKeySlice::fnv_seed` is the FNV-64 of the canonical label string and
325+
// can be used as a stable per-role identifier (e.g. unbinding lookup, codec
326+
// keying). All slices are sub-ranges of the existing 16,384-dim VSA space.
327+
// ---------------------------------------------------------------------------
328+
329+
/// A role key descriptor: a contiguous `[start:stop)` slice of the VSA space
330+
/// plus a deterministic FNV-64 fingerprint over the role's canonical label
331+
/// (used for unbinding / similarity / codec keying).
332+
///
333+
/// This is the `Copy`/`const`-friendly companion to [`RoleKey`]; both share
334+
/// the same slice boundaries by construction (see `role_key_slice_*` tests).
335+
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
336+
pub struct RoleKeySlice {
337+
pub start: usize,
338+
pub stop: usize,
339+
pub fnv_seed: u64,
340+
}
341+
342+
impl RoleKeySlice {
343+
/// Construct a const slice. `start <= stop <= VSA_DIMS` is the caller's
344+
/// invariant (debug-checked at first use, not in this `const fn` body).
345+
pub const fn new(start: usize, stop: usize, fnv_seed: u64) -> Self {
346+
Self { start, stop, fnv_seed }
347+
}
348+
pub const fn len(&self) -> usize { self.stop - self.start }
349+
pub const fn is_empty(&self) -> bool { self.start == self.stop }
350+
pub const fn range(&self) -> std::ops::Range<usize> { self.start..self.stop }
351+
}
352+
353+
/// Hand-rolled FNV-64a over raw bytes. `const fn` so role-key tables can
354+
/// be evaluated at compile time. No new deps.
355+
pub const fn fnv64_bytes(bytes: &[u8]) -> u64 {
356+
let mut hash: u64 = 0xcbf29ce484222325;
357+
let mut i = 0;
358+
while i < bytes.len() {
359+
hash ^= bytes[i] as u64;
360+
hash = hash.wrapping_mul(0x100000001b3);
361+
i += 1;
362+
}
363+
hash
364+
}
365+
366+
// --- SPO core role slices (mirror of SUBJECT_KEY..CONTEXT_KEY) --------------
367+
368+
pub const SUBJECT_SLICE: RoleKeySlice = RoleKeySlice::new(0, 2000, fnv64_bytes(b"SUBJECT"));
369+
pub const PREDICATE_SLICE: RoleKeySlice = RoleKeySlice::new(2000, 4000, fnv64_bytes(b"PREDICATE"));
370+
pub const OBJECT_SLICE: RoleKeySlice = RoleKeySlice::new(4000, 6000, fnv64_bytes(b"OBJECT"));
371+
pub const MODIFIER_SLICE: RoleKeySlice = RoleKeySlice::new(6000, 7500, fnv64_bytes(b"MODIFIER"));
372+
pub const CONTEXT_SLICE: RoleKeySlice = RoleKeySlice::new(7500, 9000, fnv64_bytes(b"CONTEXT"));
373+
374+
// --- TEKAMOLO sub-slices (mirror of TEMPORAL_KEY..LOKAL_KEY + extras) ------
375+
376+
pub const TEMPORAL_SLICE: RoleKeySlice = RoleKeySlice::new(9000, 9200, fnv64_bytes(b"TEMPORAL"));
377+
pub const KAUSAL_SLICE: RoleKeySlice = RoleKeySlice::new(9200, 9400, fnv64_bytes(b"KAUSAL"));
378+
pub const MODAL_SLICE: RoleKeySlice = RoleKeySlice::new(9400, 9500, fnv64_bytes(b"MODAL"));
379+
pub const LOKAL_SLICE: RoleKeySlice = RoleKeySlice::new(9500, 9650, fnv64_bytes(b"LOKAL"));
380+
pub const INSTRUMENT_SLICE: RoleKeySlice = RoleKeySlice::new(9650, 9750, fnv64_bytes(b"INSTRUMENT"));
381+
pub const BENEFICIARY_SLICE: RoleKeySlice = RoleKeySlice::new(9750, 9780, fnv64_bytes(b"BENEFICIARY"));
382+
pub const GOAL_SLICE: RoleKeySlice = RoleKeySlice::new(9780, 9810, fnv64_bytes(b"GOAL"));
383+
pub const SOURCE_SLICE: RoleKeySlice = RoleKeySlice::new(9810, 9840, fnv64_bytes(b"SOURCE"));
384+
385+
// --- Finnish 15 cases (mirror FINNISH_SLICES, indexed by FinnishCase as u8)
386+
387+
pub static FINNISH_CASE_SLICES: LazyLock<[(FinnishCase, RoleKeySlice); 15]> = LazyLock::new(|| {
388+
[
389+
(FinnishCase::Nominative, RoleKeySlice::new(FINNISH_SLICES[0].0, FINNISH_SLICES[0].1, fnv64_bytes(b"FI_NOMINATIVE"))),
390+
(FinnishCase::Genitive, RoleKeySlice::new(FINNISH_SLICES[1].0, FINNISH_SLICES[1].1, fnv64_bytes(b"FI_GENITIVE"))),
391+
(FinnishCase::Accusative, RoleKeySlice::new(FINNISH_SLICES[2].0, FINNISH_SLICES[2].1, fnv64_bytes(b"FI_ACCUSATIVE"))),
392+
(FinnishCase::Partitive, RoleKeySlice::new(FINNISH_SLICES[3].0, FINNISH_SLICES[3].1, fnv64_bytes(b"FI_PARTITIVE"))),
393+
(FinnishCase::Inessive, RoleKeySlice::new(FINNISH_SLICES[4].0, FINNISH_SLICES[4].1, fnv64_bytes(b"FI_INESSIVE"))),
394+
(FinnishCase::Elative, RoleKeySlice::new(FINNISH_SLICES[5].0, FINNISH_SLICES[5].1, fnv64_bytes(b"FI_ELATIVE"))),
395+
(FinnishCase::Illative, RoleKeySlice::new(FINNISH_SLICES[6].0, FINNISH_SLICES[6].1, fnv64_bytes(b"FI_ILLATIVE"))),
396+
(FinnishCase::Adessive, RoleKeySlice::new(FINNISH_SLICES[7].0, FINNISH_SLICES[7].1, fnv64_bytes(b"FI_ADESSIVE"))),
397+
(FinnishCase::Ablative, RoleKeySlice::new(FINNISH_SLICES[8].0, FINNISH_SLICES[8].1, fnv64_bytes(b"FI_ABLATIVE"))),
398+
(FinnishCase::Allative, RoleKeySlice::new(FINNISH_SLICES[9].0, FINNISH_SLICES[9].1, fnv64_bytes(b"FI_ALLATIVE"))),
399+
(FinnishCase::Essive, RoleKeySlice::new(FINNISH_SLICES[10].0, FINNISH_SLICES[10].1, fnv64_bytes(b"FI_ESSIVE"))),
400+
(FinnishCase::Translative, RoleKeySlice::new(FINNISH_SLICES[11].0, FINNISH_SLICES[11].1, fnv64_bytes(b"FI_TRANSLATIVE"))),
401+
(FinnishCase::Instructive, RoleKeySlice::new(FINNISH_SLICES[12].0, FINNISH_SLICES[12].1, fnv64_bytes(b"FI_INSTRUCTIVE"))),
402+
(FinnishCase::Abessive, RoleKeySlice::new(FINNISH_SLICES[13].0, FINNISH_SLICES[13].1, fnv64_bytes(b"FI_ABESSIVE"))),
403+
(FinnishCase::Comitative, RoleKeySlice::new(FINNISH_SLICES[14].0, FINNISH_SLICES[14].1, fnv64_bytes(b"FI_COMITATIVE"))),
404+
]
405+
});
406+
407+
/// Lookup the [`RoleKeySlice`] for a Finnish case (round-trip via the
408+
/// `LazyLock` array — exactly one slice per variant by construction).
409+
pub fn finnish_case_slice(case: FinnishCase) -> RoleKeySlice {
410+
FINNISH_CASE_SLICES[case as usize].1
411+
}
412+
413+
// --- 12 Tense slices (mirror TENSE_KEYS) -----------------------------------
414+
415+
pub static TENSE_SLICES: LazyLock<[(Tense, RoleKeySlice); 12]> = LazyLock::new(|| {
416+
let s = |i: usize| TENSE_START + i * TENSE_WIDTH;
417+
let e = |i: usize| TENSE_START + (i + 1) * TENSE_WIDTH;
418+
[
419+
(Tense::Present, RoleKeySlice::new(s(0), e(0), fnv64_bytes(b"T_PRESENT"))),
420+
(Tense::Past, RoleKeySlice::new(s(1), e(1), fnv64_bytes(b"T_PAST"))),
421+
(Tense::Future, RoleKeySlice::new(s(2), e(2), fnv64_bytes(b"T_FUTURE"))),
422+
(Tense::PresentContinuous, RoleKeySlice::new(s(3), e(3), fnv64_bytes(b"T_PRESENT_CONTINUOUS"))),
423+
(Tense::PastContinuous, RoleKeySlice::new(s(4), e(4), fnv64_bytes(b"T_PAST_CONTINUOUS"))),
424+
(Tense::FutureContinuous, RoleKeySlice::new(s(5), e(5), fnv64_bytes(b"T_FUTURE_CONTINUOUS"))),
425+
(Tense::Perfect, RoleKeySlice::new(s(6), e(6), fnv64_bytes(b"T_PERFECT"))),
426+
(Tense::Pluperfect, RoleKeySlice::new(s(7), e(7), fnv64_bytes(b"T_PLUPERFECT"))),
427+
(Tense::FuturePerfect, RoleKeySlice::new(s(8), e(8), fnv64_bytes(b"T_FUTURE_PERFECT"))),
428+
(Tense::Habitual, RoleKeySlice::new(s(9), e(9), fnv64_bytes(b"T_HABITUAL"))),
429+
(Tense::Potential, RoleKeySlice::new(s(10), e(10), fnv64_bytes(b"T_POTENTIAL"))),
430+
(Tense::Imperative, RoleKeySlice::new(s(11), e(11), fnv64_bytes(b"T_IMPERATIVE"))),
431+
]
432+
});
433+
434+
pub fn tense_slice(tense: Tense) -> RoleKeySlice {
435+
TENSE_SLICES[tense as usize].1
436+
}
437+
438+
// --- 7 NARS-inference slices (mirror NARS_SLICES) --------------------------
439+
440+
pub static NARS_INFERENCE_SLICES: LazyLock<[(NarsInference, RoleKeySlice); 7]> = LazyLock::new(|| {
441+
[
442+
(NarsInference::Deduction, RoleKeySlice::new(NARS_SLICES[0].0, NARS_SLICES[0].1, fnv64_bytes(b"N_DEDUCTION"))),
443+
(NarsInference::Induction, RoleKeySlice::new(NARS_SLICES[1].0, NARS_SLICES[1].1, fnv64_bytes(b"N_INDUCTION"))),
444+
(NarsInference::Abduction, RoleKeySlice::new(NARS_SLICES[2].0, NARS_SLICES[2].1, fnv64_bytes(b"N_ABDUCTION"))),
445+
(NarsInference::Revision, RoleKeySlice::new(NARS_SLICES[3].0, NARS_SLICES[3].1, fnv64_bytes(b"N_REVISION"))),
446+
(NarsInference::Synthesis, RoleKeySlice::new(NARS_SLICES[4].0, NARS_SLICES[4].1, fnv64_bytes(b"N_SYNTHESIS"))),
447+
(NarsInference::Extrapolation, RoleKeySlice::new(NARS_SLICES[5].0, NARS_SLICES[5].1, fnv64_bytes(b"N_EXTRAPOLATION"))),
448+
(NarsInference::CounterfactualSynthesis, RoleKeySlice::new(NARS_SLICES[6].0, NARS_SLICES[6].1, fnv64_bytes(b"N_COUNTERFACTUAL"))),
449+
]
450+
});
451+
452+
pub fn nars_inference_slice(inf: NarsInference) -> RoleKeySlice {
453+
let idx = match inf {
454+
NarsInference::Deduction => 0,
455+
NarsInference::Induction => 1,
456+
NarsInference::Abduction => 2,
457+
NarsInference::Revision => 3,
458+
NarsInference::Synthesis => 4,
459+
NarsInference::Extrapolation => 5,
460+
NarsInference::CounterfactualSynthesis => 6,
461+
};
462+
NARS_INFERENCE_SLICES[idx].1
463+
}
464+
317465
// ---------------------------------------------------------------------------
318466
// Tests
319467
// ---------------------------------------------------------------------------
@@ -461,4 +609,169 @@ mod tests {
461609
assert!(k.slice_end <= TENSE_END);
462610
}
463611
}
612+
613+
// -----------------------------------------------------------------------
614+
// D6 — RoleKeySlice catalogue tests
615+
// -----------------------------------------------------------------------
616+
617+
/// All five SPO core slices are non-overlapping and union to [0, 9000)
618+
/// (the "SPO-spine" prefix of the 16,384-dim VSA carrier).
619+
#[test]
620+
fn spo_slices_disjoint_and_contiguous() {
621+
let spo = [
622+
SUBJECT_SLICE, PREDICATE_SLICE, OBJECT_SLICE, MODIFIER_SLICE, CONTEXT_SLICE,
623+
];
624+
// Contiguous: each slice starts where the previous ended.
625+
for pair in spo.windows(2) {
626+
assert_eq!(
627+
pair[0].stop, pair[1].start,
628+
"SPO slices not contiguous: {:?} vs {:?}", pair[0], pair[1]
629+
);
630+
}
631+
// Union covers [0, 9000) — the SPO+TEKAMOLO-prefix region. (CONTEXT
632+
// ends at 9000; TEKAMOLO sub-slices begin there.)
633+
assert_eq!(spo[0].start, 0);
634+
assert_eq!(spo[spo.len() - 1].stop, 9000);
635+
}
636+
637+
/// TEKAMOLO sub-slices fit within [9000, 9840) — the slice region beyond
638+
/// CONTEXT_KEY where the original prompt placed them. (CONTEXT_KEY itself
639+
/// owns [7500, 9000) and TEKAMOLO sits AFTER it in the LF-2 layout.)
640+
#[test]
641+
fn tekamolo_sub_slices_in_post_context_band() {
642+
let teka = [
643+
TEMPORAL_SLICE, KAUSAL_SLICE, MODAL_SLICE, LOKAL_SLICE,
644+
INSTRUMENT_SLICE, BENEFICIARY_SLICE, GOAL_SLICE, SOURCE_SLICE,
645+
];
646+
for s in teka {
647+
assert!(s.start >= 9000, "TEKAMOLO slice starts before 9000: {s:?}");
648+
assert!(s.stop <= 9840, "TEKAMOLO slice ends after 9840: {s:?}");
649+
assert!(s.len() > 0, "empty TEKAMOLO slice: {s:?}");
650+
}
651+
}
652+
653+
/// Finnish case slices are non-overlapping AND fall inside the existing
654+
/// `FINNISH_START..FINNISH_END` band.
655+
#[test]
656+
fn finnish_case_slices_disjoint_in_band() {
657+
let arr = &*FINNISH_CASE_SLICES;
658+
let mut by_start: Vec<RoleKeySlice> = arr.iter().map(|(_, s)| *s).collect();
659+
by_start.sort_by_key(|s| s.start);
660+
for pair in by_start.windows(2) {
661+
assert!(
662+
pair[0].stop <= pair[1].start,
663+
"Finnish slice overlap: {:?} vs {:?}", pair[0], pair[1]
664+
);
665+
}
666+
for (_, s) in arr.iter() {
667+
assert!(s.start >= FINNISH_START);
668+
assert!(s.stop <= FINNISH_END);
669+
}
670+
}
671+
672+
/// FNV-64 of distinct labels does not collide on the canonical role names.
673+
#[test]
674+
fn fnv64_no_collisions_on_role_labels() {
675+
let labels: &[&[u8]] = &[
676+
b"SUBJECT", b"PREDICATE", b"OBJECT", b"MODIFIER", b"CONTEXT",
677+
b"TEMPORAL", b"KAUSAL", b"MODAL", b"LOKAL",
678+
b"INSTRUMENT", b"BENEFICIARY", b"GOAL", b"SOURCE",
679+
b"FI_NOMINATIVE", b"FI_GENITIVE", b"FI_ACCUSATIVE", b"FI_PARTITIVE",
680+
b"FI_INESSIVE", b"FI_ELATIVE", b"FI_ILLATIVE",
681+
b"FI_ADESSIVE", b"FI_ABLATIVE", b"FI_ALLATIVE",
682+
b"FI_ESSIVE", b"FI_TRANSLATIVE", b"FI_INSTRUCTIVE",
683+
b"FI_ABESSIVE", b"FI_COMITATIVE",
684+
b"T_PRESENT", b"T_PAST", b"T_FUTURE",
685+
b"T_PRESENT_CONTINUOUS", b"T_PAST_CONTINUOUS", b"T_FUTURE_CONTINUOUS",
686+
b"T_PERFECT", b"T_PLUPERFECT", b"T_FUTURE_PERFECT",
687+
b"T_HABITUAL", b"T_POTENTIAL", b"T_IMPERATIVE",
688+
b"N_DEDUCTION", b"N_INDUCTION", b"N_ABDUCTION", b"N_REVISION",
689+
b"N_SYNTHESIS", b"N_EXTRAPOLATION", b"N_COUNTERFACTUAL",
690+
];
691+
let mut seen = std::collections::HashSet::new();
692+
for l in labels {
693+
let h = fnv64_bytes(l);
694+
assert!(seen.insert(h), "FNV-64 collision on label {:?}", std::str::from_utf8(l).unwrap());
695+
}
696+
// Spot-check the prompt's pinned non-collision.
697+
assert_ne!(fnv64_bytes(b"SUBJECT"), fnv64_bytes(b"OBJECT"));
698+
}
699+
700+
/// Round-trip: each FinnishCase variant maps to exactly one
701+
/// `RoleKeySlice` via the LazyLock array, and the array is keyed by
702+
/// `FinnishCase as u8` (i.e. `arr[c as usize].0 == c`).
703+
#[test]
704+
fn finnish_case_round_trip() {
705+
let all = [
706+
FinnishCase::Nominative, FinnishCase::Genitive, FinnishCase::Accusative,
707+
FinnishCase::Partitive, FinnishCase::Inessive, FinnishCase::Elative,
708+
FinnishCase::Illative, FinnishCase::Adessive, FinnishCase::Ablative,
709+
FinnishCase::Allative, FinnishCase::Essive, FinnishCase::Translative,
710+
FinnishCase::Instructive, FinnishCase::Abessive, FinnishCase::Comitative,
711+
];
712+
for case in all {
713+
let (stored_case, slice) = FINNISH_CASE_SLICES[case as usize];
714+
assert_eq!(stored_case, case, "FINNISH_CASE_SLICES not indexed by `as u8`");
715+
// The free-function lookup must agree with the array entry.
716+
assert_eq!(finnish_case_slice(case), slice);
717+
// Slice mirrors the live RoleKey boundaries.
718+
let live = finnish_case_key(case);
719+
assert_eq!(slice.start, live.slice_start);
720+
assert_eq!(slice.stop, live.slice_end);
721+
// And the FNV-64 fingerprint is non-zero (every label hashes to
722+
// something distinct from the empty string's seed).
723+
assert_ne!(slice.fnv_seed, 0xcbf29ce484222325);
724+
}
725+
}
726+
727+
/// The slice catalogue mirrors the live `RoleKey` boundaries for SPO/
728+
/// TEKAMOLO so consumers can swap the two without re-deriving widths.
729+
#[test]
730+
fn role_key_slice_mirrors_live_role_key_boundaries() {
731+
let pairs: &[(RoleKeySlice, &RoleKey)] = &[
732+
(SUBJECT_SLICE, &SUBJECT_KEY),
733+
(PREDICATE_SLICE, &PREDICATE_KEY),
734+
(OBJECT_SLICE, &OBJECT_KEY),
735+
(MODIFIER_SLICE, &MODIFIER_KEY),
736+
(CONTEXT_SLICE, &CONTEXT_KEY),
737+
(TEMPORAL_SLICE, &TEMPORAL_KEY),
738+
(KAUSAL_SLICE, &KAUSAL_KEY),
739+
(MODAL_SLICE, &MODAL_KEY),
740+
(LOKAL_SLICE, &LOKAL_KEY),
741+
(INSTRUMENT_SLICE, &INSTRUMENT_KEY),
742+
(BENEFICIARY_SLICE,&BENEFICIARY_KEY),
743+
(GOAL_SLICE, &GOAL_KEY),
744+
(SOURCE_SLICE, &SOURCE_KEY),
745+
];
746+
for (slice, live) in pairs {
747+
assert_eq!(slice.start, live.slice_start, "slice/live start mismatch for {}", live.label);
748+
assert_eq!(slice.stop, live.slice_end, "slice/live stop mismatch for {}", live.label);
749+
assert!(slice.stop <= VSA_DIMS);
750+
}
751+
}
752+
753+
#[test]
754+
fn role_key_slice_const_helpers() {
755+
assert_eq!(SUBJECT_SLICE.len(), 2000);
756+
assert!(!SUBJECT_SLICE.is_empty());
757+
let r = SUBJECT_SLICE.range();
758+
assert_eq!(r.start, 0);
759+
assert_eq!(r.end, 2000);
760+
}
761+
762+
#[test]
763+
fn nars_inference_slice_round_trip() {
764+
let all = [
765+
NarsInference::Deduction, NarsInference::Induction,
766+
NarsInference::Abduction, NarsInference::Revision,
767+
NarsInference::Synthesis, NarsInference::Extrapolation,
768+
NarsInference::CounterfactualSynthesis,
769+
];
770+
for inf in all {
771+
let s = nars_inference_slice(inf);
772+
assert!(s.start >= NARS_START);
773+
assert!(s.stop <= NARS_END);
774+
assert!(s.len() > 0);
775+
}
776+
}
464777
}

0 commit comments

Comments
 (0)