From f01f5f8c5e4660bc9efdb0139bd5de0383a39b70 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 28 Apr 2026 23:08:14 +0000 Subject: [PATCH 1/8] A6: add StepDomain::Medcare variant --- .../lance-graph-contract/src/orchestration.rs | 43 ++++++++++++++++--- 1 file changed, 36 insertions(+), 7 deletions(-) diff --git a/crates/lance-graph-contract/src/orchestration.rs b/crates/lance-graph-contract/src/orchestration.rs index 9a95de6a..177df50b 100644 --- a/crates/lance-graph-contract/src/orchestration.rs +++ b/crates/lance-graph-contract/src/orchestration.rs @@ -47,6 +47,8 @@ pub enum StepDomain { Ndarray, /// SMB entity operations (outside BBB — boringly agnostic). Smb, + /// Medcare reality-check vertical (clinic data sovereignty). + Medcare, } impl StepDomain { @@ -58,21 +60,48 @@ impl StepDomain { /// "n8n.set" → N8n /// "lg.cypher" → LanceGraph /// "nd.hamming" → Ndarray + /// "medcare.check" → Medcare /// ``` pub fn from_step_type(step_type: &str) -> Option { let prefix = step_type.split('.').next()?; match prefix { - "crew" => Some(Self::Crew), - "lb" => Some(Self::Ladybug), - "n8n" => Some(Self::N8n), - "lg" => Some(Self::LanceGraph), - "nd" => Some(Self::Ndarray), - "smb" => Some(Self::Smb), - _ => None, + "crew" => Some(Self::Crew), + "lb" => Some(Self::Ladybug), + "n8n" => Some(Self::N8n), + "lg" => Some(Self::LanceGraph), + "nd" => Some(Self::Ndarray), + "smb" => Some(Self::Smb), + "medcare" => Some(Self::Medcare), + _ => None, } } } +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn step_domain_medcare_constructs_and_matches() { + let d = StepDomain::Medcare; + // Pattern-match smoke test — proves the variant is reachable. + let routed = matches!(d, StepDomain::Medcare); + assert!(routed); + // Distinct from sibling domains. + assert_ne!(StepDomain::Medcare, StepDomain::Crew); + assert_ne!(StepDomain::Medcare, StepDomain::Smb); + } + + #[test] + fn step_domain_medcare_from_step_type() { + assert_eq!( + StepDomain::from_step_type("medcare.reality_check"), + Some(StepDomain::Medcare), + ); + assert_eq!(StepDomain::from_step_type("unknown.foo"), None); + } +} + /// Unified step — the unit of work crossing system boundaries. /// /// This is the canonical type. crewai-rust's UnifiedStep and From 9b22c399df32a8189d991d0bb4f5ca6116b53d0a Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 28 Apr 2026 23:04:47 +0000 Subject: [PATCH 2/8] B3: D6 role-key catalogue with contiguous slice addressing --- .../src/grammar/role_keys.rs | 313 ++++++++++++++++++ 1 file changed, 313 insertions(+) diff --git a/crates/lance-graph-contract/src/grammar/role_keys.rs b/crates/lance-graph-contract/src/grammar/role_keys.rs index ff93167c..3704752c 100644 --- a/crates/lance-graph-contract/src/grammar/role_keys.rs +++ b/crates/lance-graph-contract/src/grammar/role_keys.rs @@ -314,6 +314,154 @@ pub static BANK_KEY: LazyLock = LazyLock::new(|| RoleKey::generate pub static FIBU_KEY: LazyLock = LazyLock::new(|| RoleKey::generate("smb.fibu", 13_072, 13_584)); pub static STEUER_KEY: LazyLock = LazyLock::new(|| RoleKey::generate("smb.steuer", 13_584, 14_096)); +// --------------------------------------------------------------------------- +// D6 — RoleKeySlice catalogue (const-addressable [start:stop) slices + FNV-64 +// fingerprint over the role label). This layer is the **catalogue index** for +// the live `RoleKey` static instances above: same boundaries, no duplication +// of the bipolar payload — just `Copy`/`const`-friendly descriptors that can +// be embedded in tables, dispatch maps, or codecs without taking a LazyLock. +// +// `RoleKeySlice::fnv_seed` is the FNV-64 of the canonical label string and +// can be used as a stable per-role identifier (e.g. unbinding lookup, codec +// keying). All slices are sub-ranges of the existing 16,384-dim VSA space. +// --------------------------------------------------------------------------- + +/// A role key descriptor: a contiguous `[start:stop)` slice of the VSA space +/// plus a deterministic FNV-64 fingerprint over the role's canonical label +/// (used for unbinding / similarity / codec keying). +/// +/// This is the `Copy`/`const`-friendly companion to [`RoleKey`]; both share +/// the same slice boundaries by construction (see `role_key_slice_*` tests). +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct RoleKeySlice { + pub start: usize, + pub stop: usize, + pub fnv_seed: u64, +} + +impl RoleKeySlice { + /// Construct a const slice. `start <= stop <= VSA_DIMS` is the caller's + /// invariant (debug-checked at first use, not in this `const fn` body). + pub const fn new(start: usize, stop: usize, fnv_seed: u64) -> Self { + Self { start, stop, fnv_seed } + } + pub const fn len(&self) -> usize { self.stop - self.start } + pub const fn is_empty(&self) -> bool { self.start == self.stop } + pub const fn range(&self) -> std::ops::Range { self.start..self.stop } +} + +/// Hand-rolled FNV-64a over raw bytes. `const fn` so role-key tables can +/// be evaluated at compile time. No new deps. +pub const fn fnv64_bytes(bytes: &[u8]) -> u64 { + let mut hash: u64 = 0xcbf29ce484222325; + let mut i = 0; + while i < bytes.len() { + hash ^= bytes[i] as u64; + hash = hash.wrapping_mul(0x100000001b3); + i += 1; + } + hash +} + +// --- SPO core role slices (mirror of SUBJECT_KEY..CONTEXT_KEY) -------------- + +pub const SUBJECT_SLICE: RoleKeySlice = RoleKeySlice::new(0, 2000, fnv64_bytes(b"SUBJECT")); +pub const PREDICATE_SLICE: RoleKeySlice = RoleKeySlice::new(2000, 4000, fnv64_bytes(b"PREDICATE")); +pub const OBJECT_SLICE: RoleKeySlice = RoleKeySlice::new(4000, 6000, fnv64_bytes(b"OBJECT")); +pub const MODIFIER_SLICE: RoleKeySlice = RoleKeySlice::new(6000, 7500, fnv64_bytes(b"MODIFIER")); +pub const CONTEXT_SLICE: RoleKeySlice = RoleKeySlice::new(7500, 9000, fnv64_bytes(b"CONTEXT")); + +// --- TEKAMOLO sub-slices (mirror of TEMPORAL_KEY..LOKAL_KEY + extras) ------ + +pub const TEMPORAL_SLICE: RoleKeySlice = RoleKeySlice::new(9000, 9200, fnv64_bytes(b"TEMPORAL")); +pub const KAUSAL_SLICE: RoleKeySlice = RoleKeySlice::new(9200, 9400, fnv64_bytes(b"KAUSAL")); +pub const MODAL_SLICE: RoleKeySlice = RoleKeySlice::new(9400, 9500, fnv64_bytes(b"MODAL")); +pub const LOKAL_SLICE: RoleKeySlice = RoleKeySlice::new(9500, 9650, fnv64_bytes(b"LOKAL")); +pub const INSTRUMENT_SLICE: RoleKeySlice = RoleKeySlice::new(9650, 9750, fnv64_bytes(b"INSTRUMENT")); +pub const BENEFICIARY_SLICE: RoleKeySlice = RoleKeySlice::new(9750, 9780, fnv64_bytes(b"BENEFICIARY")); +pub const GOAL_SLICE: RoleKeySlice = RoleKeySlice::new(9780, 9810, fnv64_bytes(b"GOAL")); +pub const SOURCE_SLICE: RoleKeySlice = RoleKeySlice::new(9810, 9840, fnv64_bytes(b"SOURCE")); + +// --- Finnish 15 cases (mirror FINNISH_SLICES, indexed by FinnishCase as u8) + +pub static FINNISH_CASE_SLICES: LazyLock<[(FinnishCase, RoleKeySlice); 15]> = LazyLock::new(|| { + [ + (FinnishCase::Nominative, RoleKeySlice::new(FINNISH_SLICES[0].0, FINNISH_SLICES[0].1, fnv64_bytes(b"FI_NOMINATIVE"))), + (FinnishCase::Genitive, RoleKeySlice::new(FINNISH_SLICES[1].0, FINNISH_SLICES[1].1, fnv64_bytes(b"FI_GENITIVE"))), + (FinnishCase::Accusative, RoleKeySlice::new(FINNISH_SLICES[2].0, FINNISH_SLICES[2].1, fnv64_bytes(b"FI_ACCUSATIVE"))), + (FinnishCase::Partitive, RoleKeySlice::new(FINNISH_SLICES[3].0, FINNISH_SLICES[3].1, fnv64_bytes(b"FI_PARTITIVE"))), + (FinnishCase::Inessive, RoleKeySlice::new(FINNISH_SLICES[4].0, FINNISH_SLICES[4].1, fnv64_bytes(b"FI_INESSIVE"))), + (FinnishCase::Elative, RoleKeySlice::new(FINNISH_SLICES[5].0, FINNISH_SLICES[5].1, fnv64_bytes(b"FI_ELATIVE"))), + (FinnishCase::Illative, RoleKeySlice::new(FINNISH_SLICES[6].0, FINNISH_SLICES[6].1, fnv64_bytes(b"FI_ILLATIVE"))), + (FinnishCase::Adessive, RoleKeySlice::new(FINNISH_SLICES[7].0, FINNISH_SLICES[7].1, fnv64_bytes(b"FI_ADESSIVE"))), + (FinnishCase::Ablative, RoleKeySlice::new(FINNISH_SLICES[8].0, FINNISH_SLICES[8].1, fnv64_bytes(b"FI_ABLATIVE"))), + (FinnishCase::Allative, RoleKeySlice::new(FINNISH_SLICES[9].0, FINNISH_SLICES[9].1, fnv64_bytes(b"FI_ALLATIVE"))), + (FinnishCase::Essive, RoleKeySlice::new(FINNISH_SLICES[10].0, FINNISH_SLICES[10].1, fnv64_bytes(b"FI_ESSIVE"))), + (FinnishCase::Translative, RoleKeySlice::new(FINNISH_SLICES[11].0, FINNISH_SLICES[11].1, fnv64_bytes(b"FI_TRANSLATIVE"))), + (FinnishCase::Instructive, RoleKeySlice::new(FINNISH_SLICES[12].0, FINNISH_SLICES[12].1, fnv64_bytes(b"FI_INSTRUCTIVE"))), + (FinnishCase::Abessive, RoleKeySlice::new(FINNISH_SLICES[13].0, FINNISH_SLICES[13].1, fnv64_bytes(b"FI_ABESSIVE"))), + (FinnishCase::Comitative, RoleKeySlice::new(FINNISH_SLICES[14].0, FINNISH_SLICES[14].1, fnv64_bytes(b"FI_COMITATIVE"))), + ] +}); + +/// Lookup the [`RoleKeySlice`] for a Finnish case (round-trip via the +/// `LazyLock` array — exactly one slice per variant by construction). +pub fn finnish_case_slice(case: FinnishCase) -> RoleKeySlice { + FINNISH_CASE_SLICES[case as usize].1 +} + +// --- 12 Tense slices (mirror TENSE_KEYS) ----------------------------------- + +pub static TENSE_SLICES: LazyLock<[(Tense, RoleKeySlice); 12]> = LazyLock::new(|| { + let s = |i: usize| TENSE_START + i * TENSE_WIDTH; + let e = |i: usize| TENSE_START + (i + 1) * TENSE_WIDTH; + [ + (Tense::Present, RoleKeySlice::new(s(0), e(0), fnv64_bytes(b"T_PRESENT"))), + (Tense::Past, RoleKeySlice::new(s(1), e(1), fnv64_bytes(b"T_PAST"))), + (Tense::Future, RoleKeySlice::new(s(2), e(2), fnv64_bytes(b"T_FUTURE"))), + (Tense::PresentContinuous, RoleKeySlice::new(s(3), e(3), fnv64_bytes(b"T_PRESENT_CONTINUOUS"))), + (Tense::PastContinuous, RoleKeySlice::new(s(4), e(4), fnv64_bytes(b"T_PAST_CONTINUOUS"))), + (Tense::FutureContinuous, RoleKeySlice::new(s(5), e(5), fnv64_bytes(b"T_FUTURE_CONTINUOUS"))), + (Tense::Perfect, RoleKeySlice::new(s(6), e(6), fnv64_bytes(b"T_PERFECT"))), + (Tense::Pluperfect, RoleKeySlice::new(s(7), e(7), fnv64_bytes(b"T_PLUPERFECT"))), + (Tense::FuturePerfect, RoleKeySlice::new(s(8), e(8), fnv64_bytes(b"T_FUTURE_PERFECT"))), + (Tense::Habitual, RoleKeySlice::new(s(9), e(9), fnv64_bytes(b"T_HABITUAL"))), + (Tense::Potential, RoleKeySlice::new(s(10), e(10), fnv64_bytes(b"T_POTENTIAL"))), + (Tense::Imperative, RoleKeySlice::new(s(11), e(11), fnv64_bytes(b"T_IMPERATIVE"))), + ] +}); + +pub fn tense_slice(tense: Tense) -> RoleKeySlice { + TENSE_SLICES[tense as usize].1 +} + +// --- 7 NARS-inference slices (mirror NARS_SLICES) -------------------------- + +pub static NARS_INFERENCE_SLICES: LazyLock<[(NarsInference, RoleKeySlice); 7]> = LazyLock::new(|| { + [ + (NarsInference::Deduction, RoleKeySlice::new(NARS_SLICES[0].0, NARS_SLICES[0].1, fnv64_bytes(b"N_DEDUCTION"))), + (NarsInference::Induction, RoleKeySlice::new(NARS_SLICES[1].0, NARS_SLICES[1].1, fnv64_bytes(b"N_INDUCTION"))), + (NarsInference::Abduction, RoleKeySlice::new(NARS_SLICES[2].0, NARS_SLICES[2].1, fnv64_bytes(b"N_ABDUCTION"))), + (NarsInference::Revision, RoleKeySlice::new(NARS_SLICES[3].0, NARS_SLICES[3].1, fnv64_bytes(b"N_REVISION"))), + (NarsInference::Synthesis, RoleKeySlice::new(NARS_SLICES[4].0, NARS_SLICES[4].1, fnv64_bytes(b"N_SYNTHESIS"))), + (NarsInference::Extrapolation, RoleKeySlice::new(NARS_SLICES[5].0, NARS_SLICES[5].1, fnv64_bytes(b"N_EXTRAPOLATION"))), + (NarsInference::CounterfactualSynthesis, RoleKeySlice::new(NARS_SLICES[6].0, NARS_SLICES[6].1, fnv64_bytes(b"N_COUNTERFACTUAL"))), + ] +}); + +pub fn nars_inference_slice(inf: NarsInference) -> RoleKeySlice { + let idx = match inf { + NarsInference::Deduction => 0, + NarsInference::Induction => 1, + NarsInference::Abduction => 2, + NarsInference::Revision => 3, + NarsInference::Synthesis => 4, + NarsInference::Extrapolation => 5, + NarsInference::CounterfactualSynthesis => 6, + }; + NARS_INFERENCE_SLICES[idx].1 +} + // --------------------------------------------------------------------------- // Tests // --------------------------------------------------------------------------- @@ -461,4 +609,169 @@ mod tests { assert!(k.slice_end <= TENSE_END); } } + + // ----------------------------------------------------------------------- + // D6 — RoleKeySlice catalogue tests + // ----------------------------------------------------------------------- + + /// All five SPO core slices are non-overlapping and union to [0, 9000) + /// (the "SPO-spine" prefix of the 16,384-dim VSA carrier). + #[test] + fn spo_slices_disjoint_and_contiguous() { + let spo = [ + SUBJECT_SLICE, PREDICATE_SLICE, OBJECT_SLICE, MODIFIER_SLICE, CONTEXT_SLICE, + ]; + // Contiguous: each slice starts where the previous ended. + for pair in spo.windows(2) { + assert_eq!( + pair[0].stop, pair[1].start, + "SPO slices not contiguous: {:?} vs {:?}", pair[0], pair[1] + ); + } + // Union covers [0, 9000) — the SPO+TEKAMOLO-prefix region. (CONTEXT + // ends at 9000; TEKAMOLO sub-slices begin there.) + assert_eq!(spo[0].start, 0); + assert_eq!(spo[spo.len() - 1].stop, 9000); + } + + /// TEKAMOLO sub-slices fit within [9000, 9840) — the slice region beyond + /// CONTEXT_KEY where the original prompt placed them. (CONTEXT_KEY itself + /// owns [7500, 9000) and TEKAMOLO sits AFTER it in the LF-2 layout.) + #[test] + fn tekamolo_sub_slices_in_post_context_band() { + let teka = [ + TEMPORAL_SLICE, KAUSAL_SLICE, MODAL_SLICE, LOKAL_SLICE, + INSTRUMENT_SLICE, BENEFICIARY_SLICE, GOAL_SLICE, SOURCE_SLICE, + ]; + for s in teka { + assert!(s.start >= 9000, "TEKAMOLO slice starts before 9000: {s:?}"); + assert!(s.stop <= 9840, "TEKAMOLO slice ends after 9840: {s:?}"); + assert!(s.len() > 0, "empty TEKAMOLO slice: {s:?}"); + } + } + + /// Finnish case slices are non-overlapping AND fall inside the existing + /// `FINNISH_START..FINNISH_END` band. + #[test] + fn finnish_case_slices_disjoint_in_band() { + let arr = &*FINNISH_CASE_SLICES; + let mut by_start: Vec = arr.iter().map(|(_, s)| *s).collect(); + by_start.sort_by_key(|s| s.start); + for pair in by_start.windows(2) { + assert!( + pair[0].stop <= pair[1].start, + "Finnish slice overlap: {:?} vs {:?}", pair[0], pair[1] + ); + } + for (_, s) in arr.iter() { + assert!(s.start >= FINNISH_START); + assert!(s.stop <= FINNISH_END); + } + } + + /// FNV-64 of distinct labels does not collide on the canonical role names. + #[test] + fn fnv64_no_collisions_on_role_labels() { + let labels: &[&[u8]] = &[ + b"SUBJECT", b"PREDICATE", b"OBJECT", b"MODIFIER", b"CONTEXT", + b"TEMPORAL", b"KAUSAL", b"MODAL", b"LOKAL", + b"INSTRUMENT", b"BENEFICIARY", b"GOAL", b"SOURCE", + b"FI_NOMINATIVE", b"FI_GENITIVE", b"FI_ACCUSATIVE", b"FI_PARTITIVE", + b"FI_INESSIVE", b"FI_ELATIVE", b"FI_ILLATIVE", + b"FI_ADESSIVE", b"FI_ABLATIVE", b"FI_ALLATIVE", + b"FI_ESSIVE", b"FI_TRANSLATIVE", b"FI_INSTRUCTIVE", + b"FI_ABESSIVE", b"FI_COMITATIVE", + b"T_PRESENT", b"T_PAST", b"T_FUTURE", + b"T_PRESENT_CONTINUOUS", b"T_PAST_CONTINUOUS", b"T_FUTURE_CONTINUOUS", + b"T_PERFECT", b"T_PLUPERFECT", b"T_FUTURE_PERFECT", + b"T_HABITUAL", b"T_POTENTIAL", b"T_IMPERATIVE", + b"N_DEDUCTION", b"N_INDUCTION", b"N_ABDUCTION", b"N_REVISION", + b"N_SYNTHESIS", b"N_EXTRAPOLATION", b"N_COUNTERFACTUAL", + ]; + let mut seen = std::collections::HashSet::new(); + for l in labels { + let h = fnv64_bytes(l); + assert!(seen.insert(h), "FNV-64 collision on label {:?}", std::str::from_utf8(l).unwrap()); + } + // Spot-check the prompt's pinned non-collision. + assert_ne!(fnv64_bytes(b"SUBJECT"), fnv64_bytes(b"OBJECT")); + } + + /// Round-trip: each FinnishCase variant maps to exactly one + /// `RoleKeySlice` via the LazyLock array, and the array is keyed by + /// `FinnishCase as u8` (i.e. `arr[c as usize].0 == c`). + #[test] + fn finnish_case_round_trip() { + let all = [ + FinnishCase::Nominative, FinnishCase::Genitive, FinnishCase::Accusative, + FinnishCase::Partitive, FinnishCase::Inessive, FinnishCase::Elative, + FinnishCase::Illative, FinnishCase::Adessive, FinnishCase::Ablative, + FinnishCase::Allative, FinnishCase::Essive, FinnishCase::Translative, + FinnishCase::Instructive, FinnishCase::Abessive, FinnishCase::Comitative, + ]; + for case in all { + let (stored_case, slice) = FINNISH_CASE_SLICES[case as usize]; + assert_eq!(stored_case, case, "FINNISH_CASE_SLICES not indexed by `as u8`"); + // The free-function lookup must agree with the array entry. + assert_eq!(finnish_case_slice(case), slice); + // Slice mirrors the live RoleKey boundaries. + let live = finnish_case_key(case); + assert_eq!(slice.start, live.slice_start); + assert_eq!(slice.stop, live.slice_end); + // And the FNV-64 fingerprint is non-zero (every label hashes to + // something distinct from the empty string's seed). + assert_ne!(slice.fnv_seed, 0xcbf29ce484222325); + } + } + + /// The slice catalogue mirrors the live `RoleKey` boundaries for SPO/ + /// TEKAMOLO so consumers can swap the two without re-deriving widths. + #[test] + fn role_key_slice_mirrors_live_role_key_boundaries() { + let pairs: &[(RoleKeySlice, &RoleKey)] = &[ + (SUBJECT_SLICE, &SUBJECT_KEY), + (PREDICATE_SLICE, &PREDICATE_KEY), + (OBJECT_SLICE, &OBJECT_KEY), + (MODIFIER_SLICE, &MODIFIER_KEY), + (CONTEXT_SLICE, &CONTEXT_KEY), + (TEMPORAL_SLICE, &TEMPORAL_KEY), + (KAUSAL_SLICE, &KAUSAL_KEY), + (MODAL_SLICE, &MODAL_KEY), + (LOKAL_SLICE, &LOKAL_KEY), + (INSTRUMENT_SLICE, &INSTRUMENT_KEY), + (BENEFICIARY_SLICE,&BENEFICIARY_KEY), + (GOAL_SLICE, &GOAL_KEY), + (SOURCE_SLICE, &SOURCE_KEY), + ]; + for (slice, live) in pairs { + assert_eq!(slice.start, live.slice_start, "slice/live start mismatch for {}", live.label); + assert_eq!(slice.stop, live.slice_end, "slice/live stop mismatch for {}", live.label); + assert!(slice.stop <= VSA_DIMS); + } + } + + #[test] + fn role_key_slice_const_helpers() { + assert_eq!(SUBJECT_SLICE.len(), 2000); + assert!(!SUBJECT_SLICE.is_empty()); + let r = SUBJECT_SLICE.range(); + assert_eq!(r.start, 0); + assert_eq!(r.end, 2000); + } + + #[test] + fn nars_inference_slice_round_trip() { + let all = [ + NarsInference::Deduction, NarsInference::Induction, + NarsInference::Abduction, NarsInference::Revision, + NarsInference::Synthesis, NarsInference::Extrapolation, + NarsInference::CounterfactualSynthesis, + ]; + for inf in all { + let s = nars_inference_slice(inf); + assert!(s.start >= NARS_START); + assert!(s.stop <= NARS_END); + assert!(s.len() > 0); + } + } } From 0022962ea19516b36b012a2b52952211c532456d Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 28 Apr 2026 23:04:50 +0000 Subject: [PATCH 3/8] B2: D4 ContextChain reasoning ops (coherence/replay/disambiguate) --- .../src/grammar/context_chain.rs | 355 ++++++++++++++++-- 1 file changed, 323 insertions(+), 32 deletions(-) diff --git a/crates/lance-graph-contract/src/grammar/context_chain.rs b/crates/lance-graph-contract/src/grammar/context_chain.rs index a0b1cec9..aa131369 100644 --- a/crates/lance-graph-contract/src/grammar/context_chain.rs +++ b/crates/lance-graph-contract/src/grammar/context_chain.rs @@ -45,43 +45,97 @@ pub struct ContextChain { /// Result of a counterfactual disambiguation: the chosen candidate, its /// coherence, the margin to second place, the full ranked alternatives, /// and whether the caller should escalate to an LLM. +/// +/// D4 (2026-04 worker B2) extended this with `winner_index`, an alias +/// `winner`, `dispersion` across the top-3 candidates, and a +/// `candidate_count`. `chosen` and `winner` are equal by construction +/// (`winner` is the canonical D4 name; `chosen` is preserved for +/// existing consumers). +/// +/// Empty-candidates contract: returns a sentinel result with +/// `candidate_count = 0`, `winner_index = usize::MAX`, a zero +/// `Binary16K` placeholder fingerprint, and `escalate_to_llm = true`. +/// Callers should check `candidate_count == 0` (or `escalate_to_llm`) +/// before reading `winner` / `chosen`. #[derive(Debug, Clone)] pub struct DisambiguationResult { pub chosen: CrystalFingerprint, pub coherence: f32, - /// `chosen.coherence - second_place.coherence`. Zero if only one candidate. + /// `chosen.coherence - second_place.coherence`. Zero if only one + /// candidate. `> DISAMBIGUATION_MARGIN_THRESHOLD` (~0.1) means + /// the winner is confidently above the runner-up. pub margin: f32, /// All candidates with their scores, sorted descending by coherence. pub alternatives: Vec<(CrystalFingerprint, f32)>, /// True if `margin < DISAMBIGUATION_MARGIN_THRESHOLD` (ambiguous, escalate). pub escalate_to_llm: bool, + + // ── D4 reasoning-operator extensions ────────────────────────── + /// Index of the winner in the original candidate iterator (0-based). + /// `usize::MAX` if the candidate iterator was empty. + pub winner_index: usize, + /// Best candidate's fingerprint. Equal to `chosen`; provided under the + /// canonical D4 name for new callers. + pub winner: CrystalFingerprint, + /// Mean pairwise normalized Hamming distance across the top-3 + /// candidates' Binary16K fingerprints. High value (close to 0.5) + /// indicates the alternatives spread out — "no clear winner." + /// Zero if fewer than two top candidates carry comparable + /// `Binary16K` fingerprints. + pub dispersion: f32, + /// Total candidates evaluated (length of the input iterator). + pub candidate_count: usize, } /// Weighting kernel for temporal position in the Markov chain. /// Mexican-hat emphasizes focal, de-emphasizes distant positions. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)] pub enum WeightingKernel { + /// All positions weighted equally. Uniform, + /// Mexican-hat (DoG) — focal positive, near-neighbors decay through + /// zero-crossing into a small negative tail. Captures + /// anticipation / surprise on context shift. Default kernel. + #[default] MexicanHat, + /// Standard Gaussian decay from focal. Gaussian, } impl WeightingKernel { - /// Weight for a position at distance `d` from focal (0 = focal, 5 = edge). - pub fn weight(&self, d: usize) -> f32 { + /// Weight at signed offset `delta` (signed) for window `radius`. + /// Returns f32 in roughly `[-1, 1]`. + /// + /// `delta = 0` is the focal position; `|delta| = radius` is the edge of + /// the window. The kernel is symmetric in `delta` for all variants + /// (uniform / mexican-hat / gaussian). + /// + /// Approximate ricker wavelet: `(1 - d²) · exp(-d²/2)` where + /// `d = |delta| / max(radius, 1)`. Gaussian uses `exp(-d²/2)`. + pub fn weight(&self, delta: i32, radius: u32) -> f32 { + let r = radius.max(1) as f32; match self { Self::Uniform => 1.0, Self::MexicanHat => { - // Peak at focal (d=0), smooth fall-off, slight negative at edge. - let x = d as f32 / (MARKOV_RADIUS as f32); - (1.0 - 2.0 * x * x) * (-x * x * 2.0).exp() + let d = delta.unsigned_abs() as f32 / r; + let dd = d * d; + (1.0 - dd) * (-dd / 2.0).exp() } Self::Gaussian => { - let x = d as f32 / (MARKOV_RADIUS as f32); - (-x * x * 2.0).exp() + // delta is signed but the kernel is symmetric; using the + // absolute value avoids relying on signed-cast semantics. + let d = delta.unsigned_abs() as f32 / r; + (-(d * d) / 2.0).exp() } } } + + /// Convenience: weight at unsigned distance `d` from focal under the + /// chain's default radius (`MARKOV_RADIUS`). Equivalent to + /// `self.weight(d as i32, MARKOV_RADIUS as u32)`. + pub fn weight_at_distance(&self, d: usize) -> f32 { + self.weight(d as i32, MARKOV_RADIUS as u32) + } } impl ContextChain { @@ -202,10 +256,20 @@ impl ContextChain { /// Counterfactual disambiguation: try each candidate at position `i`, /// return the one with highest coherence and the decision margin. /// + /// Each candidate is scored by the `total_coherence` of the chain + /// after replacing position `i` with that candidate. The result + /// also carries `winner_index` (position in the input iterator), + /// `dispersion` (mean pairwise Binary16K Hamming distance across + /// the top-3 candidates), and `candidate_count`. + /// /// Edge cases: - /// - Empty candidate list: returns a result with a placeholder zero - /// fingerprint and `escalate_to_llm = true`. - /// - Single candidate: `margin = 0.0`, `escalate_to_llm = true`. + /// - **Empty candidate iterator**: returns the documented sentinel + /// (`candidate_count = 0`, `winner_index = usize::MAX`, + /// placeholder `Binary16K` fingerprint, `escalate_to_llm = true`). + /// Does *not* panic — keeping the API total simplifies caller + /// code in the cypher bridge. + /// - **Single candidate**: `margin = 0.0`, `dispersion = 0.0`, + /// `escalate_to_llm = true`. pub fn disambiguate( &self, i: usize, @@ -214,46 +278,90 @@ impl ContextChain { where I: IntoIterator, { - let mut scored: Vec<(CrystalFingerprint, f32)> = candidates + // Score with original input index preserved so we can report + // `winner_index` in the iterator's order. + let mut scored: Vec<(usize, CrystalFingerprint, f32)> = candidates .into_iter() - .map(|cand| { + .enumerate() + .map(|(idx, cand)| { let (_chain, coh) = self.replay_with_alternative(i, cand.clone()); - (cand, coh) + (idx, cand, coh) }) .collect(); + let candidate_count = scored.len(); + if scored.is_empty() { - // Placeholder: caller should check `escalate_to_llm`. + // Documented sentinel — never panic; callers gate on + // `escalate_to_llm` or `candidate_count == 0`. + let placeholder = + CrystalFingerprint::Binary16K(Box::new([0u64; 256])); return DisambiguationResult { - chosen: CrystalFingerprint::Binary16K(Box::new([0u64; 256])), + chosen: placeholder.clone(), coherence: 0.0, margin: 0.0, alternatives: Vec::new(), escalate_to_llm: true, + winner_index: usize::MAX, + winner: placeholder, + dispersion: 0.0, + candidate_count: 0, }; } // Sort descending by coherence; ties resolved by insertion order // (stable sort + NaN-safe partial_cmp fallback to Equal). scored.sort_by(|a, b| { - b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal) + b.2.partial_cmp(&a.2).unwrap_or(std::cmp::Ordering::Equal) }); - let (chosen, coherence) = scored[0].clone(); + let winner_index = scored[0].0; + let chosen = scored[0].1.clone(); + let coherence = scored[0].2; let margin = if scored.len() >= 2 { - scored[0].1 - scored[1].1 + scored[0].2 - scored[1].2 } else { 0.0 }; let escalate_to_llm = scored.len() < 2 || margin < DISAMBIGUATION_MARGIN_THRESHOLD; + // Dispersion: mean pairwise normalized Hamming distance over the + // top-3 candidates. Only Binary16K pairs contribute; if fewer + // than two contribute, dispersion is 0.0 (cannot say). + let top_n = scored.len().min(3); + let mut pair_sum: f32 = 0.0; + let mut pair_count: u32 = 0; + for a_idx in 0..top_n { + for b_idx in (a_idx + 1)..top_n { + let a_bits = binary16k_bits(&scored[a_idx].1); + let b_bits = binary16k_bits(&scored[b_idx].1); + if let (Some(a), Some(b)) = (a_bits, b_bits) { + let d = hamming_256(a, b) as f32 / MAX_HAMMING_BITS as f32; + pair_sum += d; + pair_count += 1; + } + } + } + let dispersion = if pair_count == 0 { + 0.0 + } else { + pair_sum / pair_count as f32 + }; + + let alternatives: Vec<(CrystalFingerprint, f32)> = + scored.into_iter().map(|(_, fp, c)| (fp, c)).collect(); + DisambiguationResult { - chosen, + chosen: chosen.clone(), coherence, margin, - alternatives: scored, + alternatives, escalate_to_llm, + winner_index, + winner: chosen, + dispersion, + candidate_count, } } } @@ -453,23 +561,206 @@ mod tests { #[test] fn mexican_hat_weights_monotone() { // Mexican-hat: peak at d=0, monotone decrease through d=1..5. + // Test through the convenience helper for compactness; the + // primary API is `weight(delta: i32, radius: u32)`. let k = WeightingKernel::MexicanHat; - let w0 = k.weight(0); - let w1 = k.weight(1); - let w2 = k.weight(2); - let w3 = k.weight(3); - let w4 = k.weight(4); - let w5 = k.weight(5); + let w0 = k.weight_at_distance(0); + let w1 = k.weight_at_distance(1); + let w2 = k.weight_at_distance(2); + let w3 = k.weight_at_distance(3); + let w4 = k.weight_at_distance(4); + let w5 = k.weight_at_distance(5); assert!(w0 > w1, "w(0)={w0} should exceed w(1)={w1}"); assert!(w1 > w2, "w(1)={w1} should exceed w(2)={w2}"); assert!(w2 > w3, "w(2)={w2} should exceed w(3)={w3}"); assert!(w3 > w4, "w(3)={w3} should exceed w(4)={w4}"); assert!(w4 > w5, "w(4)={w4} should exceed w(5)={w5}"); // Uniform and Gaussian sanity checks. - assert_eq!(WeightingKernel::Uniform.weight(0), 1.0); - assert_eq!(WeightingKernel::Uniform.weight(5), 1.0); - let g0 = WeightingKernel::Gaussian.weight(0); - let g5 = WeightingKernel::Gaussian.weight(5); + assert_eq!(WeightingKernel::Uniform.weight_at_distance(0), 1.0); + assert_eq!(WeightingKernel::Uniform.weight_at_distance(5), 1.0); + let g0 = WeightingKernel::Gaussian.weight_at_distance(0); + let g5 = WeightingKernel::Gaussian.weight_at_distance(5); assert!(g0 > g5, "gaussian should also decay: g(0)={g0}, g(5)={g5}"); } + + // ── D4 reasoning-operator tests (worker B2, 2026-04) ──────────────── + + /// 1. `Uniform` returns 1.0 at every offset. + #[test] + fn d4_uniform_kernel_is_constant() { + let k = WeightingKernel::Uniform; + for delta in -10i32..=10 { + for radius in 1u32..=5 { + let w = k.weight(delta, radius); + assert!( + (w - 1.0).abs() < f32::EPSILON, + "Uniform({delta}, {radius}) = {w}, expected 1.0" + ); + } + } + } + + /// 2. `MexicanHat` is symmetric: w(-d, r) == w(+d, r). + #[test] + fn d4_mexican_hat_symmetric() { + let k = WeightingKernel::MexicanHat; + for radius in [1u32, 2, 3, 5, 8] { + for d in 1i32..=10 { + let w_pos = k.weight(d, radius); + let w_neg = k.weight(-d, radius); + assert!( + (w_pos - w_neg).abs() < 1e-6, + "MexicanHat asymmetric at d={d} r={radius}: \ + w(+d)={w_pos} w(-d)={w_neg}" + ); + } + } + // Gaussian also symmetric (same code path via |delta|). + let g = WeightingKernel::Gaussian; + for radius in [1u32, 5] { + for d in 1i32..=5 { + assert!( + (g.weight(d, radius) - g.weight(-d, radius)).abs() < 1e-6, + "Gaussian should also be symmetric" + ); + } + } + } + + /// 3. `MexicanHat` weight is monotone-decreasing in `|delta|` over the + /// radius. Crosses zero at `|delta| ≈ radius` (where d² = 1) and + /// stays in the negative tail past the radius — that is the + /// Mexican-hat shape. + #[test] + fn d4_mexican_hat_monotone_and_zero_crossing() { + let k = WeightingKernel::MexicanHat; + let radius: u32 = 5; + // Monotone decrease in [0, radius]. + let mut prev = k.weight(0, radius); + assert!(prev > 0.99, "focal weight should be ~1.0, got {prev}"); + for d in 1i32..=radius as i32 { + let cur = k.weight(d, radius); + assert!( + cur < prev, + "MexicanHat not monotone at d={d}: prev={prev} cur={cur}" + ); + prev = cur; + } + // At |delta| = radius, d² = 1 → (1 - 1) · exp(-0.5) = 0. + let edge = k.weight(radius as i32, radius); + assert!( + edge.abs() < 1e-6, + "MexicanHat zero-crossing should be at |delta|=radius, got {edge}" + ); + // Beyond the radius the kernel goes negative (the "hat brim"). + let beyond = k.weight((radius as i32) + 1, radius); + assert!( + beyond < 0.0, + "MexicanHat should be negative beyond radius, got {beyond}" + ); + } + + /// 4. `coherence_at` on a chain of identical fingerprints is ~1.0. + /// (Existing `coherence_high_for_self_chain` covers this; this + /// test re-verifies the D4 contract independently of the + /// pre-existing test.) + #[test] + fn d4_coherence_self_chain_is_one() { + let fp = mk_fp(0x0102_0304_0506_0708); + let chain = fill_chain_with(&fp); + for i in 0..CHAIN_LEN { + let c = chain.coherence_at(i); + assert!( + c > 0.99, + "self-chain coherence at {i} should be ~1.0, got {c}" + ); + } + let total = chain.total_coherence(); + assert!( + total > 0.99, + "self-chain total_coherence should be ~1.0, got {total}" + ); + } + + /// 5. `disambiguate` with two candidates where one matches the + /// surrounding chain → that one wins with non-zero margin and + /// `winner_index` points at it. + #[test] + fn d4_disambiguate_picks_matching_candidate() { + let base = mk_fp(0x9999_AAAA_BBBB_CCCC); + let mut chain = fill_chain_with(&base); + // Blank position 4 so we can replay alternatives in. + chain.fingerprints[4] = None; + + // Far-miss: fully inverted vs. base. + let far = match &base { + CrystalFingerprint::Binary16K(bits) => { + let mut inv = Box::new([0u64; 256]); + for (i, w) in bits.iter().enumerate() { + inv[i] = !w; + } + CrystalFingerprint::Binary16K(inv) + } + _ => unreachable!(), + }; + + // Order: [far, base] → if base wins, winner_index must be 1. + let res = chain.disambiguate(4, vec![far, base.clone()]); + assert_eq!(res.candidate_count, 2); + assert_eq!(res.winner_index, 1, "base was at iterator index 1"); + assert!( + res.margin > 0.0, + "matching candidate should have non-zero margin, got {}", + res.margin + ); + // `winner` and `chosen` agree by construction. + match (&res.winner, &res.chosen) { + (CrystalFingerprint::Binary16K(a), + CrystalFingerprint::Binary16K(b)) => { + assert_eq!(**a, **b, "winner and chosen must agree"); + } + _ => panic!("unexpected fingerprint variants"), + } + // Winner equals base. + match (&res.winner, &base) { + (CrystalFingerprint::Binary16K(a), + CrystalFingerprint::Binary16K(b)) => { + assert_eq!(**a, **b, "winner must be the matching base"); + } + _ => panic!("unexpected fingerprint variants"), + } + } + + /// 6. `disambiguate` with an empty candidate iterator returns the + /// documented sentinel result (no panic). `candidate_count = 0`, + /// `winner_index = usize::MAX`, `escalate_to_llm = true`. + #[test] + fn d4_disambiguate_empty_returns_sentinel() { + let chain = fill_chain_with(&mk_fp(0x1)); + let res: DisambiguationResult = + chain.disambiguate(0, Vec::::new()); + assert_eq!(res.candidate_count, 0); + assert_eq!(res.winner_index, usize::MAX); + assert!(res.escalate_to_llm, "empty must escalate"); + assert!(res.alternatives.is_empty()); + assert_eq!(res.coherence, 0.0); + assert_eq!(res.margin, 0.0); + assert_eq!(res.dispersion, 0.0); + // The placeholder fingerprint is a zeroed Binary16K. + match &res.winner { + CrystalFingerprint::Binary16K(bits) => { + assert!(bits.iter().all(|&w| w == 0), + "sentinel placeholder should be all-zero"); + } + _ => panic!("sentinel must be Binary16K placeholder"), + } + } + + /// `WeightingKernel::default()` is `MexicanHat` (D4 chose this as + /// the canonical kernel — focal-emphasizing with anticipation tail). + #[test] + fn d4_default_kernel_is_mexican_hat() { + let k: WeightingKernel = Default::default(); + assert_eq!(k, WeightingKernel::MexicanHat); + } } From 999318965c2b23c58a0939dc70652259ff1ac967 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 28 Apr 2026 23:08:41 +0000 Subject: [PATCH 4/8] B6: D7 thinking_styles awareness + 12 YAML starter configs --- .../assets/grammar_styles/analytical.yaml | 22 + .../assets/grammar_styles/convergent.yaml | 22 + .../assets/grammar_styles/creative.yaml | 22 + .../assets/grammar_styles/deliberate.yaml | 22 + .../assets/grammar_styles/diffuse.yaml | 22 + .../assets/grammar_styles/divergent.yaml | 22 + .../assets/grammar_styles/exploratory.yaml | 22 + .../assets/grammar_styles/focused.yaml | 22 + .../assets/grammar_styles/intuitive.yaml | 22 + .../assets/grammar_styles/metacognitive.yaml | 22 + .../assets/grammar_styles/peripheral.yaml | 22 + .../assets/grammar_styles/systematic.yaml | 22 + .../src/grammar/thinking_styles.rs | 499 ++++++++++++++++++ 13 files changed, 763 insertions(+) create mode 100644 crates/deepnsm/assets/grammar_styles/analytical.yaml create mode 100644 crates/deepnsm/assets/grammar_styles/convergent.yaml create mode 100644 crates/deepnsm/assets/grammar_styles/creative.yaml create mode 100644 crates/deepnsm/assets/grammar_styles/deliberate.yaml create mode 100644 crates/deepnsm/assets/grammar_styles/diffuse.yaml create mode 100644 crates/deepnsm/assets/grammar_styles/divergent.yaml create mode 100644 crates/deepnsm/assets/grammar_styles/exploratory.yaml create mode 100644 crates/deepnsm/assets/grammar_styles/focused.yaml create mode 100644 crates/deepnsm/assets/grammar_styles/intuitive.yaml create mode 100644 crates/deepnsm/assets/grammar_styles/metacognitive.yaml create mode 100644 crates/deepnsm/assets/grammar_styles/peripheral.yaml create mode 100644 crates/deepnsm/assets/grammar_styles/systematic.yaml diff --git a/crates/deepnsm/assets/grammar_styles/analytical.yaml b/crates/deepnsm/assets/grammar_styles/analytical.yaml new file mode 100644 index 00000000..86ebf96d --- /dev/null +++ b/crates/deepnsm/assets/grammar_styles/analytical.yaml @@ -0,0 +1,22 @@ +# (starter prior — tune empirically) +# Rule-clear, depth-first, English+Finnish morphology, low ambiguity tolerance. +style: analytical +nars: + primary: Deduction + fallback: Abduction +morphology: + tables: [english_svo, finnish_case_table] + agglutinative_mode: false +tekamolo: + priority: [temporal, lokal, kausal, modal] + require_fillable: true +markov: + radius: 5 + kernel: uniform + replay: forward +spo_causal: + pearl_mask: 0x01 + ambiguity_tolerance: 0.1 +coverage: + local_threshold: 0.90 + escalate_below: 0.85 diff --git a/crates/deepnsm/assets/grammar_styles/convergent.yaml b/crates/deepnsm/assets/grammar_styles/convergent.yaml new file mode 100644 index 00000000..44471404 --- /dev/null +++ b/crates/deepnsm/assets/grammar_styles/convergent.yaml @@ -0,0 +1,22 @@ +# (starter prior — tune empirically) +# Convergent: collapse alternatives quickly, deduce with high confidence. +style: convergent +nars: + primary: Deduction + fallback: Revision +morphology: + tables: [english_svo, finnish_case_table] + agglutinative_mode: false +tekamolo: + priority: [temporal, kausal, lokal, modal] + require_fillable: true +markov: + radius: 3 + kernel: uniform + replay: forward +spo_causal: + pearl_mask: 0x01 + ambiguity_tolerance: 0.05 +coverage: + local_threshold: 0.92 + escalate_below: 0.88 diff --git a/crates/deepnsm/assets/grammar_styles/creative.yaml b/crates/deepnsm/assets/grammar_styles/creative.yaml new file mode 100644 index 00000000..b07add6b --- /dev/null +++ b/crates/deepnsm/assets/grammar_styles/creative.yaml @@ -0,0 +1,22 @@ +# (starter prior — tune empirically) +# Creative: cross-domain synthesis, mexican-hat to suppress mid-distance noise. +style: creative +nars: + primary: Synthesis + fallback: CounterfactualSynthesis +morphology: + tables: [english_svo, finnish_case_table, japanese_particles] + agglutinative_mode: true +tekamolo: + priority: [modal, kausal, temporal, lokal] + require_fillable: false +markov: + radius: 5 + kernel: mexican_hat + replay: both_and_compare +spo_causal: + pearl_mask: 0x7F + ambiguity_tolerance: 0.35 +coverage: + local_threshold: 0.75 + escalate_below: 0.55 diff --git a/crates/deepnsm/assets/grammar_styles/deliberate.yaml b/crates/deepnsm/assets/grammar_styles/deliberate.yaml new file mode 100644 index 00000000..cc72623f --- /dev/null +++ b/crates/deepnsm/assets/grammar_styles/deliberate.yaml @@ -0,0 +1,22 @@ +# (starter prior — tune empirically) +# Deliberate: slow, methodical, full TEKAMOLO required, both-direction replay. +style: deliberate +nars: + primary: Deduction + fallback: Revision +morphology: + tables: [english_svo, finnish_case_table, german_case_table, russian_case_table] + agglutinative_mode: false +tekamolo: + priority: [temporal, kausal, modal, lokal] + require_fillable: true +markov: + radius: 5 + kernel: gaussian + replay: both_and_compare +spo_causal: + pearl_mask: 0x07 + ambiguity_tolerance: 0.10 +coverage: + local_threshold: 0.92 + escalate_below: 0.85 diff --git a/crates/deepnsm/assets/grammar_styles/diffuse.yaml b/crates/deepnsm/assets/grammar_styles/diffuse.yaml new file mode 100644 index 00000000..e4fd358c --- /dev/null +++ b/crates/deepnsm/assets/grammar_styles/diffuse.yaml @@ -0,0 +1,22 @@ +# (starter prior — tune empirically) +# Diffuse: wide context, induction-leaning, low fillable strictness. +style: diffuse +nars: + primary: Induction + fallback: Synthesis +morphology: + tables: [english_svo, finnish_case_table, russian_case_table, german_case_table] + agglutinative_mode: true +tekamolo: + priority: [modal, lokal, temporal, kausal] + require_fillable: false +markov: + radius: 5 + kernel: mexican_hat + replay: both_and_compare +spo_causal: + pearl_mask: 0x3F + ambiguity_tolerance: 0.40 +coverage: + local_threshold: 0.70 + escalate_below: 0.55 diff --git a/crates/deepnsm/assets/grammar_styles/divergent.yaml b/crates/deepnsm/assets/grammar_styles/divergent.yaml new file mode 100644 index 00000000..a9489ae1 --- /dev/null +++ b/crates/deepnsm/assets/grammar_styles/divergent.yaml @@ -0,0 +1,22 @@ +# (starter prior — tune empirically) +# Divergent: broad fan-out, counterfactuals welcome, full pearl mask. +style: divergent +nars: + primary: CounterfactualSynthesis + fallback: Synthesis +morphology: + tables: [english_svo, finnish_case_table, russian_case_table, turkish_aggl] + agglutinative_mode: true +tekamolo: + priority: [modal, kausal, lokal, temporal] + require_fillable: false +markov: + radius: 5 + kernel: mexican_hat + replay: both_and_compare +spo_causal: + pearl_mask: 0xFF + ambiguity_tolerance: 0.45 +coverage: + local_threshold: 0.65 + escalate_below: 0.45 diff --git a/crates/deepnsm/assets/grammar_styles/exploratory.yaml b/crates/deepnsm/assets/grammar_styles/exploratory.yaml new file mode 100644 index 00000000..4141b976 --- /dev/null +++ b/crates/deepnsm/assets/grammar_styles/exploratory.yaml @@ -0,0 +1,22 @@ +# (starter prior — tune empirically) +# Exploratory: counterfactual primary, agglutinative on, broad coverage tolerance. +style: exploratory +nars: + primary: CounterfactualSynthesis + fallback: Abduction +morphology: + tables: [english_svo, finnish_case_table, russian_case_table] + agglutinative_mode: true +tekamolo: + priority: [modal, kausal, lokal, temporal] + require_fillable: false +markov: + radius: 5 + kernel: mexican_hat + replay: both_and_compare +spo_causal: + pearl_mask: 0xFF + ambiguity_tolerance: 0.4 +coverage: + local_threshold: 0.70 + escalate_below: 0.50 diff --git a/crates/deepnsm/assets/grammar_styles/focused.yaml b/crates/deepnsm/assets/grammar_styles/focused.yaml new file mode 100644 index 00000000..f50dabbd --- /dev/null +++ b/crates/deepnsm/assets/grammar_styles/focused.yaml @@ -0,0 +1,22 @@ +# (starter prior — tune empirically) +# Focused: tight Markov radius, single morphology table, deduction-first. +style: focused +nars: + primary: Deduction + fallback: Revision +morphology: + tables: [english_svo] + agglutinative_mode: false +tekamolo: + priority: [temporal, lokal, kausal, modal] + require_fillable: true +markov: + radius: 2 + kernel: gaussian + replay: forward +spo_causal: + pearl_mask: 0x01 + ambiguity_tolerance: 0.05 +coverage: + local_threshold: 0.95 + escalate_below: 0.90 diff --git a/crates/deepnsm/assets/grammar_styles/intuitive.yaml b/crates/deepnsm/assets/grammar_styles/intuitive.yaml new file mode 100644 index 00000000..08f922d6 --- /dev/null +++ b/crates/deepnsm/assets/grammar_styles/intuitive.yaml @@ -0,0 +1,22 @@ +# (starter prior — tune empirically) +# Intuitive: leap to abduction, soft thresholds, agglutinative for surface tells. +style: intuitive +nars: + primary: Abduction + fallback: Synthesis +morphology: + tables: [english_svo, finnish_case_table, turkish_aggl] + agglutinative_mode: true +tekamolo: + priority: [modal, temporal, kausal, lokal] + require_fillable: false +markov: + radius: 4 + kernel: gaussian + replay: forward +spo_causal: + pearl_mask: 0x0F + ambiguity_tolerance: 0.30 +coverage: + local_threshold: 0.78 + escalate_below: 0.60 diff --git a/crates/deepnsm/assets/grammar_styles/metacognitive.yaml b/crates/deepnsm/assets/grammar_styles/metacognitive.yaml new file mode 100644 index 00000000..f1f2ade3 --- /dev/null +++ b/crates/deepnsm/assets/grammar_styles/metacognitive.yaml @@ -0,0 +1,22 @@ +# (starter prior — tune empirically) +# Metacognitive: revision-primary, both-direction replay, mid-tolerance for self-check. +style: metacognitive +nars: + primary: Revision + fallback: Abduction +morphology: + tables: [english_svo, finnish_case_table, russian_case_table] + agglutinative_mode: false +tekamolo: + priority: [kausal, modal, temporal, lokal] + require_fillable: false +markov: + radius: 5 + kernel: mexican_hat + replay: both_and_compare +spo_causal: + pearl_mask: 0x3F + ambiguity_tolerance: 0.25 +coverage: + local_threshold: 0.80 + escalate_below: 0.65 diff --git a/crates/deepnsm/assets/grammar_styles/peripheral.yaml b/crates/deepnsm/assets/grammar_styles/peripheral.yaml new file mode 100644 index 00000000..de413de1 --- /dev/null +++ b/crates/deepnsm/assets/grammar_styles/peripheral.yaml @@ -0,0 +1,22 @@ +# (starter prior — tune empirically) +# Peripheral: scan the edges, low-radius mexican-hat catches outliers. +style: peripheral +nars: + primary: Abduction + fallback: Extrapolation +morphology: + tables: [english_svo, finnish_case_table, japanese_particles] + agglutinative_mode: true +tekamolo: + priority: [lokal, temporal, modal, kausal] + require_fillable: false +markov: + radius: 4 + kernel: mexican_hat + replay: backward +spo_causal: + pearl_mask: 0x1F + ambiguity_tolerance: 0.35 +coverage: + local_threshold: 0.72 + escalate_below: 0.58 diff --git a/crates/deepnsm/assets/grammar_styles/systematic.yaml b/crates/deepnsm/assets/grammar_styles/systematic.yaml new file mode 100644 index 00000000..e7de802c --- /dev/null +++ b/crates/deepnsm/assets/grammar_styles/systematic.yaml @@ -0,0 +1,22 @@ +# (starter prior — tune empirically) +# Systematic: methodical traversal, full TEKAMOLO, gaussian-weighted Markov. +style: systematic +nars: + primary: Deduction + fallback: Induction +morphology: + tables: [english_svo, finnish_case_table, german_case_table] + agglutinative_mode: false +tekamolo: + priority: [temporal, kausal, modal, lokal] + require_fillable: true +markov: + radius: 5 + kernel: gaussian + replay: forward +spo_causal: + pearl_mask: 0x03 + ambiguity_tolerance: 0.15 +coverage: + local_threshold: 0.88 + escalate_below: 0.82 diff --git a/crates/lance-graph-contract/src/grammar/thinking_styles.rs b/crates/lance-graph-contract/src/grammar/thinking_styles.rs index e655eab2..13cc34e9 100644 --- a/crates/lance-graph-contract/src/grammar/thinking_styles.rs +++ b/crates/lance-graph-contract/src/grammar/thinking_styles.rs @@ -302,6 +302,327 @@ pub fn revise_truth(current: TruthValue, f_obs: f32, c_obs: f32) -> TruthValue { TruthValue::new(f_new.clamp(0.0, 1.0), c_new.clamp(0.0, 1.0)) } +// --------------------------------------------------------------------------- +// YAML loader (zero-dep, line-based) +// --------------------------------------------------------------------------- +// +// Supports the strict subset our `grammar_styles/