diff --git a/crates/causal-edge/src/edge.rs b/crates/causal-edge/src/edge.rs index 41e47ba14..324bf13a8 100644 --- a/crates/causal-edge/src/edge.rs +++ b/crates/causal-edge/src/edge.rs @@ -936,14 +936,50 @@ impl CausalEdge64 { ((self.0 >> TRUTH_SHIFT) & BITS2_MASK) as u8 } + /// Causal-topology lens: reads the identical two bits as `truth()` / + /// `truth_raw()` (bits 59-60), projected through + /// [`crate::layout::CausalTopology`] instead of + /// [`crate::layout::TrustTexture`]. Purely additive — no bits move. + /// + /// See [`crate::layout::CausalTopology`] for the full compatibility + /// statement: wire and ordinal compatibility with `TrustTexture` are + /// exact, the legacy behavioural projection onto old rows is + /// intentional, but the *historical factual provenance* of old rows is + /// **not** guaranteed — do not treat this as ground truth for a row + /// written before this accessor existed. + #[inline(always)] + pub fn topology(self) -> crate::layout::CausalTopology { + use crate::layout::{CausalTopology, BITS2_MASK, TRUTH_SHIFT}; + CausalTopology::from_bits_2(((self.0 >> TRUTH_SHIFT) & BITS2_MASK) as u8) + } + /// Spare 3-bit field (bits 61-63). Reserved for sprint-12+ use. - /// Returns 0 for ZERO edges and all v1-written edges (temporal MSBs were ≤ 0xFFF). + /// Returns 0 for ZERO edges. + /// + /// **CORRECTION (measured):** this line previously also claimed "and all + /// v1-written edges (temporal MSBs were <= 0xFFF)". That is false. Bits + /// 61-63 are v1 temporal bits **9-11**, so any v1 edge with + /// `temporal >= 512` reads a NON-ZERO spare. Apply a version gate on + /// edges of unknown provenance — the same rule `truth()` already states + /// for bits 59-60 at its own threshold (`temporal >= 128`). #[inline(always)] pub fn spare(self) -> u8 { use crate::layout::{BITS3_MASK, SPARE_SHIFT}; ((self.0 >> SPARE_SHIFT) & BITS3_MASK) as u8 } + /// Quantized reasoning-level projection: reads the identical three bits + /// as `spare()` (bits 61-63), projected through + /// [`crate::layout::ReasoningBand`]. Purely additive — no bits move, and + /// nothing auto-derives this value; see the type's doc comment for the + /// no-auto-derivation guarantee and the orthogonality notes against + /// `CausalMask` / the inference mantissa / `direction`. + #[inline(always)] + pub fn reasoning_band(self) -> crate::layout::ReasoningBand { + use crate::layout::{ReasoningBand, BITS3_MASK, SPARE_SHIFT}; + ReasoningBand::from_bits_3(((self.0 >> SPARE_SHIFT) & BITS3_MASK) as u8) + } + // ── Builder-Shape Setters (functional update, returns Self) ───────────── /// Return new edge with W slot set to `w` (0..=63). @@ -961,6 +997,19 @@ impl CausalEdge64 { Self((self.0 & !TRUTH_MASK) | ((t.to_bits_2() as u64 & BITS2_MASK) << TRUTH_SHIFT)) } + /// Return new edge with the causal-topology lens set (bits 59-60). + /// + /// Consuming builder, register-style (`edge.with_topology(t)`), matching + /// `with_truth`/`with_spare`. Shares storage with `truth`/`with_truth` + /// — the last writer of either view wins, by construction, since it is + /// one 2-bit register read through two lenses. See + /// [`crate::layout::CausalTopology`] for the compatibility statement. + #[inline] + pub fn with_topology(self, topo: crate::layout::CausalTopology) -> Self { + use crate::layout::{BITS2_MASK, TRUTH_MASK, TRUTH_SHIFT}; + Self((self.0 & !TRUTH_MASK) | ((topo.to_bits_2() as u64 & BITS2_MASK) << TRUTH_SHIFT)) + } + /// Return new edge with signed inference mantissa set (range −8..+7). /// /// Stored as 4-bit two's-complement in bits 46-49. Values outside −8..+7 @@ -981,6 +1030,34 @@ impl CausalEdge64 { Self((self.0 & !SPARE_MASK) | ((s as u64 & BITS3_MASK) << SPARE_SHIFT)) } + /// Return new edge with the texture-band projection set (bits 61-63). + /// + /// Consuming builder, register-style (`edge.with_reasoning_band(b)`), + /// matching `with_spare`/`with_topology`. Shares storage with + /// `spare`/`with_spare` — the last writer of either view wins, by + /// construction, since it is one 3-bit register read through two + /// lenses. See [`crate::layout::ReasoningBand`] for the full semantics, + /// # ⚠ Writing a non-zero band perturbs the deprecated `temporal()` + /// + /// `temporal()` reads bits **52..63**, a window that CONTAINS these three + /// bits (and the truth bits below them). Decomposed under v2 it is + /// `plast_bit2 | (w_slot << 1) | (truth << 7) | (band << 9)`. So a + /// non-zero band becomes the DOMINANT term of that composite. + /// + /// `Network::evidence_trail` sorts by `temporal()` and its comment claims + /// the sort "degrades to a stable no-op" under v2. That holds only while + /// bits 52..63 are all zero — it is already false for any edge carrying a + /// `w_slot` or a `truth`, and writing a band makes the ordering + /// band-dominated. That ordering is NOT meaningful. Nothing in-tree + /// writes a band today, so this is a trap for the first producer, not a + /// live defect. + /// the orthogonality notes, and the no-auto-derivation guarantee. + #[inline] + pub fn with_reasoning_band(self, band: crate::layout::ReasoningBand) -> Self { + use crate::layout::{BITS3_MASK, SPARE_MASK, SPARE_SHIFT}; + Self((self.0 & !SPARE_MASK) | ((band.to_bits_3() as u64 & BITS3_MASK) << SPARE_SHIFT)) + } + /// Set W-slot and truth-band in one mask-and-or operation (hot-path emit). /// /// Used by `MailboxSoA::dispatch_cycle()` when stamping routing onto emissions. @@ -1061,10 +1138,22 @@ impl CausalEdge64 { pub fn truth_raw(self) -> u8 { 0 } + /// V1 stub: additive `CausalTopology` lens is a no-op under v1 (matches + /// the `truth()` stub above — same bits, same "feature off" behaviour). + #[inline(always)] + pub fn topology(self) -> crate::layout::CausalTopology { + crate::layout::CausalTopology::Direct + } #[inline(always)] pub fn spare(self) -> u8 { 0 } + /// V1 stub: additive `ReasoningBand` lens is a no-op under v1 (matches + /// the `spare()` stub above — same bits, same "feature off" behaviour). + #[inline(always)] + pub fn reasoning_band(self) -> crate::layout::ReasoningBand { + crate::layout::ReasoningBand::Surface + } #[inline] pub fn with_w_slot(self, _w: u8) -> Self { self @@ -1074,6 +1163,10 @@ impl CausalEdge64 { self } #[inline] + pub fn with_topology(self, _topo: crate::layout::CausalTopology) -> Self { + self + } + #[inline] pub fn with_inference_mantissa(self, _m: i8) -> Self { self } @@ -1082,6 +1175,10 @@ impl CausalEdge64 { self } #[inline] + pub fn with_reasoning_band(self, _band: crate::layout::ReasoningBand) -> Self { + self + } + #[inline] pub fn with_routing(self, _w: u8, _t: crate::layout::TrustTexture) -> Self { self } diff --git a/crates/causal-edge/src/layout.rs b/crates/causal-edge/src/layout.rs index 8a158abdf..c06e528c8 100644 --- a/crates/causal-edge/src/layout.rs +++ b/crates/causal-edge/src/layout.rs @@ -55,10 +55,25 @@ pub const W_SHIFT: u32 = 53; /// Truth-band lens: 2-bit TrustTexture ordinal (bits 59-60). /// 0 = Crystalline. Per cognitive-substrate-convergence-v1.md L-7. +/// +/// Same two bits also carry an ADDITIVE factual view, [`CausalTopology`] +/// (below) — see its doc comment for the wire/ordinal/behavioural/ +/// provenance compatibility statement. `TrustTexture` remains the +/// canonical epistemic-trust reading; `CausalTopology` is a second, +/// orthogonal reading of the identical bits for producers that want to +/// record topology instead of (or alongside) trust texture. No bits move. pub const TRUTH_SHIFT: u32 = 59; /// Spare: 3-bit reserved for sprint-12+ (bits 61-63). /// Candidates: Rubicon-commit marker, Markov-decay quantum, I-NOISE-FLOOR-JIRAK threshold. +/// +/// Same three bits also carry an ADDITIVE quantized-projection view, +/// [`ReasoningBand`] (below). No auto-derivation: nothing writes this +/// field except an explicit `with_reasoning_band()` call. +/// +/// **v1 provenance:** bits 61-63 were temporal bits 9-11, so a v1 edge with +/// `temporal >= 512` reads a NON-ZERO band. Apply a version gate on edges of +/// unknown provenance — the same rule `truth()` states for bits 59-60. pub const SPARE_SHIFT: u32 = 61; // ── Common masks ───────────────────────────────────────────────────────────── @@ -105,10 +120,22 @@ const _LAYOUT_COVERAGE: () = { /// 0b11 = Murky | Contradiction| Loud | Compass (veto) /// ``` /// -/// NOTE: Local definition in causal-edge (zero-dep crate). The canonical -/// contract type is `lance_graph_contract::mul::TrustTexture`. -/// The 2-bit encoding is byte-compatible by construction. -/// Long-term: add `From for contract::TrustTexture` at the planner boundary. +/// NOTE: Local definition in causal-edge (zero-dep crate). +/// +/// **CORRECTION (measured):** an earlier version of this note claimed +/// `lance_graph_contract::mul::TrustTexture` is "the canonical contract type" +/// and "byte-compatible by construction". Both halves are false. That type's +/// variants are `Calibrated / Overconfident / Uncertain / Underconfident` — +/// a different ontology (felt-vs-demonstrated competence), with no semantic +/// mapping onto `Crystalline / Solid / Fuzzy / Murky`, and no `From` impl +/// exists in either direction. `docs/TYPE_DUPLICATION_MAP.md` rules the +/// opposite of the old note: **"Canonical: NONE — both are domain-correct +/// and should keep distinct names."** Do not build a cast on the old claim. +/// +/// This enum is the LEGACY/COMPATIBILITY projection of bits 59-60; the +/// factual view over the same bits is [`CausalTopology`]. Not deprecated — +/// nothing is ready to move, and the wider rename is owned by the existing +/// TrustTexture-duplication debt item. #[derive(Copy, Clone, Eq, PartialEq, Debug, Default)] #[repr(u8)] pub enum TrustTexture { @@ -141,3 +168,229 @@ impl TrustTexture { self as u8 } } + +/// Two-bit CAUSAL TOPOLOGY lens — an ADDITIVE factual view over the same +/// two bits (59-60) that [`TrustTexture`] occupies. No bits move; this is +/// a second reading of the identical register, not a new field and not a +/// layout revision (no CE64 v3, no `ENVELOPE_LAYOUT_VERSION` bump). +/// +/// `TrustTexture` reads those bits as an EPISTEMIC-TRUST texture (how much +/// to trust the edge: mastered/calibrated/uncertain/contradiction). +/// `CausalTopology` reads the identical bits as a FACTUAL TOPOLOGY +/// classification (how the edge's causal path is structured — direct vs. +/// mediated, known vs. unknown intermediates). +/// +/// ```text +/// 0b00 = Direct +/// 0b01 = IndirectKnownIntermediates +/// 0b10 = IndirectUnknownIntermediates +/// 0b11 = Unknown +/// ``` +/// +/// ## Migration contract — the ordinal identity is deliberate, not coincidence +/// +/// `Crystalline == Direct == 0`, `Solid == IndirectKnownIntermediates == 1`, +/// `Fuzzy == IndirectUnknownIntermediates == 2`, `Murky == Unknown == 3`. +/// +/// Four compatibility axes, stated explicitly because they are NOT the same +/// claim and must not be conflated: +/// +/// ```text +/// wire compatibility: exact +/// ordinal compatibility: exact +/// legacy behavioural projection: intentional +/// historical factual provenance: NOT guaranteed for old rows +/// ``` +/// +/// - **Wire compatibility is exact** — same two bits (59-60), same shift, +/// same mask, same byte layout. Nothing about the CE64 wire changes. +/// - **Ordinal compatibility is exact** — `TrustTexture as u8 == +/// CausalTopology as u8` for every one of the four variants (verified by +/// test). +/// - **Legacy behavioural projection is intentional.** Reading an existing +/// row's bits 59-60 through [`CausalEdge64::topology`] reproduces exactly +/// the ordinal a `TrustTexture` reader would have gotten from the same +/// bits. That projection is BY DESIGN, so a consumer that has not moved to +/// `CausalTopology` yet is unaffected by rows a `CausalTopology`-aware +/// writer produces, and vice versa — this is a staged migration, not a +/// flag day. +/// - **Historical factual provenance is NOT guaranteed for old rows.** A row +/// written before this change had its bits 59-60 stamped with +/// TRUST-TEXTURE semantics (how confident the writer was in the edge), not +/// TOPOLOGY semantics (how the causal path is shaped). The two concepts +/// are correlated in practice but they are not the same fact, and nothing +/// in this change infers, repairs, or backfills the topology of a +/// pre-existing row. **Do not treat `old_edge.topology()` as ground truth +/// about that row's actual causal topology** — it is only the +/// same-ordinal projection through the new lens. Source-authoritative +/// topology begins only when a later producer explicitly writes +/// `CausalTopology` via [`CausalEdge64::with_topology`]. +/// +/// [`CausalEdge64::ZERO`] therefore reads `CausalTopology::Direct` under +/// this view exactly as it reads `TrustTexture::Crystalline` under the old +/// one — that is intentional for this staged migration (the all-zero +/// default), not a sentinel asserting "known to be direct." +/// +/// [`CausalEdge64::ZERO`]: super::edge::CausalEdge64::ZERO +/// [`CausalEdge64::topology`]: super::edge::CausalEdge64::topology +/// [`CausalEdge64::with_topology`]: super::edge::CausalEdge64::with_topology +#[derive(Copy, Clone, Eq, PartialEq, Debug, Default)] +#[repr(u8)] +pub enum CausalTopology { + /// Direct causal edge, no intermediates. Default. + /// Ordinal-identical to `TrustTexture::Crystalline`. + #[default] + Direct = 0, + /// Indirect, with known/named intermediate nodes on the causal path. + /// Ordinal-identical to `TrustTexture::Solid`. + IndirectKnownIntermediates = 1, + /// Indirect, but the intermediate nodes are unknown/unnamed. + /// Ordinal-identical to `TrustTexture::Fuzzy`. + IndirectUnknownIntermediates = 2, + /// Topology not established. Ordinal-identical to `TrustTexture::Murky`. + Unknown = 3, +} + +impl CausalTopology { + /// Construct from the raw 2-bit field value (bits masked automatically). + #[inline] + pub fn from_bits_2(v: u8) -> Self { + match v & 0b11 { + 0 => Self::Direct, + 1 => Self::IndirectKnownIntermediates, + 2 => Self::IndirectUnknownIntermediates, + _ => Self::Unknown, + } + } + + /// Return the raw 2-bit value (0..=3). + #[inline] + pub fn to_bits_2(self) -> u8 { + self as u8 + } +} + +/// Three-bit quantized reasoning-level projection over the SPARE bits +/// (61-63) — the same three bits [`super::edge::CausalEdge64::spare`] / +/// [`super::edge::CausalEdge64::with_spare`] expose as a raw, uninterpreted +/// 3-bit scalar. ADDITIVE only: no bits move, no layout revision. +/// +/// ```text +/// 0b000 = Surface +/// 0b001 = Association +/// 0b010 = Relation +/// 0b011 = Causal +/// 0b100 = Counterfactual +/// 0b101 = Perspective +/// 0b110 = Meta +/// 0b111 = Transcendent +/// ``` +/// +/// This is a QUANTIZED HOT-PATH PROJECTION of a potentially richer future +/// texture model — 8 ordinals is what fits in 3 bits on the hot register, +/// not a claim that reasoning has exactly 8 levels. Treat it as a coarse, +/// register-resident classifier only, never as that richer model itself. +/// +/// ## Deliberately orthogonal to existing fields with colliding names +/// +/// The words above collide with existing `CausalEdge64` vocabulary. Every +/// collision below is INTENTIONAL and the two meanings are ORTHOGONAL — +/// setting one never implies, derives, or requires the other: +/// +/// - `Causal` = the cognition currently operating at the causal reasoning +/// level. This is **not** [`super::pearl::CausalMask`] (bits 40-42), +/// which says WHICH Pearl/SPO projection (S/P/O planes) is represented. +/// An edge may carry `ReasoningBand::Causal` under any `CausalMask`. +/// - `Counterfactual` = the reasoning CONTEXT is counterfactual. This is +/// **not** the signed inference mantissa's −6 slot (bits 46-49; see +/// `InferenceType::Counterfactual::to_mantissa() == -6`), which names one +/// specific NARS operation as counterfactual. An edge may carry +/// `ReasoningBand::Counterfactual` while its mantissa encodes any NARS +/// rule, and an edge with mantissa −6 need not carry +/// `ReasoningBand::Counterfactual`. +/// - `Perspective` = perspective/decentration reasoning (I/Thou/It, +/// Self/Other/World). This is **not** +/// [`super::edge::CausalEdge64::direction`] (bits 43-45), the +/// pathology-per-plane sign triad. +/// - `Meta` = reasoning ABOUT reasoning/evidence/revision (meta-cognition), +/// independent of every other field on the edge. +/// - `Transcendent` = mechanically, the highest ordinal this 3-bit +/// projection can express — the topmost cross-frame reasoning level in +/// this band. Nothing more: no mystical or philosophical behaviour is +/// implied or triggered by this value anywhere in this crate. +/// +/// ## No auto-derivation +/// +/// Nothing in this crate derives this field from `CausalMask`, +/// `InferenceType`, NARS frequency/confidence, MUL, ReasoningGap, +/// potholes, or `ThinkingStyle`. It is set ONLY by an explicit +/// `with_reasoning_band()` call, and reads whatever the SPARE bits already +/// hold otherwise (0 / `Surface` for `CausalEdge64::ZERO` and for every row +/// produced by this crate's own constructors, since `spare()` already +/// defaults to 0 there — but not guaranteed for a raw `u64` from elsewhere). +/// # Why NOT "TextureBand" +/// +/// The name was settled by a workspace vocabulary audit, not by preference. +/// "Texture" is the most collided word in this stack's cognitive vocabulary: +/// FOUR distinct `TrustTexture` enums exist (this crate's, the contract's +/// `mul::TrustTexture`, the planner's 5-variant `mul::trust::TrustTexture`, +/// and AriGraph's 3-variant orchestrator one), that duplication is already +/// booked debt whose recorded remedy is a RENAME, and an operator ruling +/// holds "Texture = binding topology, not polarity" — i.e. Texture is +/// deliberately NOT an ordinal. Naming a 3-bit ordinal `TextureBand`, in +/// this file, directly beneath `TrustTexture` and over the adjacent bits, +/// would read as one 5-bit widening of it. It is an unrelated field. +/// +/// # NOT `RungLevel`, despite four shared variant names +/// +/// This band shares four variant NAMES with +/// `lance_graph_contract::cognitive_shader::RungLevel` — `Surface`, +/// `Counterfactual`, `Meta`, `Transcendent` — at DIFFERENT ordinals +/// (0 / 6 / 7 / 9 there vs 0 / 4 / 6 / 7 here). The two are unrelated enums: +/// never cast, compare, or map between them by ordinal. +#[derive(Copy, Clone, Eq, PartialEq, Debug, Default)] +#[repr(u8)] +pub enum ReasoningBand { + /// Surface-level reasoning. Default. + #[default] + Surface = 0, + /// Association-level reasoning. + Association = 1, + /// Relation-level reasoning. + Relation = 2, + /// Causal-level reasoning. See the orthogonality note re: `CausalMask`. + Causal = 3, + /// Counterfactual reasoning context. See the orthogonality note re: the + /// inference mantissa's −6 slot. + Counterfactual = 4, + /// Perspective/decentration reasoning. See the orthogonality note re: + /// `direction`. + Perspective = 5, + /// Meta-cognitive reasoning (reasoning about reasoning/evidence/revision). + Meta = 6, + /// Highest ordinal in this band. Mechanical only — see note above. + Transcendent = 7, +} + +impl ReasoningBand { + /// Construct from the raw 3-bit field value (bits masked automatically). + #[inline] + pub fn from_bits_3(v: u8) -> Self { + match v & 0b111 { + 0 => Self::Surface, + 1 => Self::Association, + 2 => Self::Relation, + 3 => Self::Causal, + 4 => Self::Counterfactual, + 5 => Self::Perspective, + 6 => Self::Meta, + _ => Self::Transcendent, + } + } + + /// Return the raw 3-bit value (0..=7). + #[inline] + pub fn to_bits_3(self) -> u8 { + self as u8 + } +} diff --git a/crates/causal-edge/src/lib.rs b/crates/causal-edge/src/lib.rs index 334c0c1f1..0b72ba8ed 100644 --- a/crates/causal-edge/src/lib.rs +++ b/crates/causal-edge/src/lib.rs @@ -72,3 +72,10 @@ pub use syllogism::{Figure, Syllogism}; // Re-export v2 layout types under cfg for downstream consumers #[cfg(feature = "causal-edge-v2-layout")] pub use layout::TrustTexture; + +// Additive factual/quantized lenses over the same TrustTexture (bits 59-60) +// and spare (bits 61-63) registers — see layout.rs doc comments for the +// full wire/ordinal/behavioural/provenance compatibility statement. No bits +// move; these are new readings of existing storage, not a new layout. +#[cfg(feature = "causal-edge-v2-layout")] +pub use layout::{CausalTopology, ReasoningBand}; diff --git a/crates/causal-edge/src/v2_layout_tests.rs b/crates/causal-edge/src/v2_layout_tests.rs index 224403c7f..2aa4fd662 100644 --- a/crates/causal-edge/src/v2_layout_tests.rs +++ b/crates/causal-edge/src/v2_layout_tests.rs @@ -19,7 +19,7 @@ #[cfg(feature = "causal-edge-v2-layout")] mod v2_layout_tests { use crate::edge::{CausalEdge64, InferenceType}; - use crate::layout::TrustTexture; + use crate::layout::{CausalTopology, ReasoningBand, TrustTexture}; use crate::pearl::CausalMask; use crate::plasticity::PlasticityState; @@ -474,4 +474,478 @@ mod v2_layout_tests { "pack(Intervention) under v2 must decode back to Intervention" ); } + + // ═══════════════════════════════════════════════════════════════════ + // CausalTopology (bits 59-60, additive factual view over TrustTexture) + // + ReasoningBand (bits 61-63, additive quantized view over `spare`) + // ═══════════════════════════════════════════════════════════════════ + // + // These fields do NOT move any bits and do NOT introduce a new layout + // version — they are second readings of the identical TrustTexture / + // spare registers. See layout.rs doc comments for the full + // wire/ordinal/behavioural/provenance compatibility statement. + + // ── 1. Raw u64 fixtures round-trip byte-for-byte unchanged ───────────── + + #[test] + fn test_raw_fixtures_round_trip_byte_for_byte_via_topology_and_reasoning_band() { + // Arbitrary fixtures covering varied bit patterns across the whole + // word, including every combination of the two shared registers + // (bits 59-60, 61-63). + let fixtures: [u64; 6] = [ + 0x0000_0000_0000_0000, + 0xFFFF_FFFF_FFFF_FFFF, + 0x1234_5678_9ABC_DEF0, + 0xDEAD_BEEF_CAFE_F00D, + 0x8000_0000_0000_0001, + 0x5555_5555_5555_5555, + ]; + for &raw in &fixtures { + let edge = CausalEdge64(raw); + // Reading a lens then writing back exactly what was read must be + // a complete no-op on the raw word — the new code touches no bit + // it did not read. + let via_topology = edge.with_topology(edge.topology()); + assert_eq!( + via_topology.0, raw, + "read-then-write-back through CausalTopology changed the raw word for {raw:#018x}" + ); + let via_texture = edge.with_reasoning_band(edge.reasoning_band()); + assert_eq!( + via_texture.0, raw, + "read-then-write-back through ReasoningBand changed the raw word for {raw:#018x}" + ); + } + } + + // ── 2. truth() returns exactly what it did before, all four ordinals ─── + + #[test] + fn test_truth_unaffected_by_new_topology_lens_for_all_four_ordinals() { + for t in [ + TrustTexture::Crystalline, + TrustTexture::Solid, + TrustTexture::Fuzzy, + TrustTexture::Murky, + ] { + let edge = CausalEdge64::ZERO.with_truth(t); + assert_eq!(edge.truth(), t, "truth() must be unchanged for {t:?}"); + } + } + + // ── 3. topology() reads the same two raw bits as truth_raw() ─────────── + + #[test] + fn test_topology_reads_the_same_two_bits_as_truth_raw() { + for raw2 in 0u8..=3 { + let edge = CausalEdge64::ZERO.with_truth(TrustTexture::from_bits_2(raw2)); + assert_eq!(edge.truth_raw(), raw2, "truth_raw setup mismatch"); + assert_eq!( + edge.topology().to_bits_2(), + edge.truth_raw(), + "topology() must read the identical raw bits as truth_raw() for raw={raw2}" + ); + } + } + + // ── Bonus: topology() round-trips via with_topology(), all 4 ordinals ── + + #[test] + fn test_topology_roundtrip() { + for t in [ + CausalTopology::Direct, + CausalTopology::IndirectKnownIntermediates, + CausalTopology::IndirectUnknownIntermediates, + CausalTopology::Unknown, + ] { + let edge = CausalEdge64::ZERO.with_topology(t); + assert_eq!(edge.topology(), t, "topology round-trip failed for {t:?}"); + } + } + + // ── 4. All four ordinal aliases are exact (pairwise `as u8`) ──────────── + + #[test] + fn test_causal_topology_ordinals_are_exactly_trust_texture_ordinals() { + assert_eq!( + TrustTexture::Crystalline as u8, + CausalTopology::Direct as u8, + "Crystalline/Direct must alias to the same ordinal" + ); + assert_eq!( + TrustTexture::Solid as u8, + CausalTopology::IndirectKnownIntermediates as u8, + "Solid/IndirectKnownIntermediates must alias to the same ordinal" + ); + assert_eq!( + TrustTexture::Fuzzy as u8, + CausalTopology::IndirectUnknownIntermediates as u8, + "Fuzzy/IndirectUnknownIntermediates must alias to the same ordinal" + ); + assert_eq!( + TrustTexture::Murky as u8, + CausalTopology::Unknown as u8, + "Murky/Unknown must alias to the same ordinal" + ); + } + + // ── 5. with_topology changes ONLY bits 59-60 (exact XOR-diff-mask form) ─ + + #[test] + fn test_with_topology_changes_only_bits_59_60_exact_mask_diff() { + // Fixture with every OTHER field non-zero/non-default; truth-bits + // (and therefore topology) left at the pack_v2 default of 0. Going + // 0 -> Unknown (0b11, the field's max) makes the XOR diff mask equal + // exactly TRUTH_MASK if and only if with_topology touches nothing + // else in the word. + let base = CausalEdge64::pack_v2( + 0xAA, + 0xBB, + 0xCC, + 0xDD, + 0xEE, + CausalMask::SPO, + 0b111, + PlasticityState::ALL_HOT, + ) + .with_w_slot(63) + .with_inference_mantissa(-7) + .with_spare(0b111); + assert_eq!( + base.topology(), + CausalTopology::Direct, + "fixture truth-bits must start at 0 for the min->max diff to be exact" + ); + + let after = base.with_topology(CausalTopology::Unknown); + let diff = base.0 ^ after.0; + assert_eq!( + diff, + crate::layout::TRUTH_MASK, + "with_topology(min->max) must flip exactly the TRUTH_MASK bits, nothing else" + ); + + // Named-field cross-check (belt and suspenders): every other field + // survives untouched. + assert_eq!(after.s_idx(), 0xAA, "S disturbed by with_topology"); + assert_eq!(after.p_idx(), 0xBB, "P disturbed by with_topology"); + assert_eq!(after.o_idx(), 0xCC, "O disturbed by with_topology"); + assert_eq!( + after.frequency_u8(), + 0xDD, + "frequency disturbed by with_topology" + ); + assert_eq!( + after.confidence_u8(), + 0xEE, + "confidence disturbed by with_topology" + ); + assert_eq!( + after.causal_mask(), + CausalMask::SPO, + "causal_mask disturbed by with_topology" + ); + assert_eq!( + after.direction(), + 0b111, + "direction disturbed by with_topology" + ); + assert_eq!( + after.inference_mantissa(), + -7, + "inference_mantissa disturbed by with_topology" + ); + assert_eq!( + after.plasticity(), + PlasticityState::ALL_HOT, + "plasticity disturbed by with_topology" + ); + assert_eq!(after.w_slot(), 63, "w_slot disturbed by with_topology"); + assert_eq!(after.spare(), 0b111, "spare disturbed by with_topology"); + } + + // ── 6. with_reasoning_band changes ONLY bits 61-63 (exact XOR-diff-mask) ── + + #[test] + fn test_with_reasoning_band_changes_only_bits_61_63_exact_mask_diff() { + let base = CausalEdge64::pack_v2( + 0xAA, + 0xBB, + 0xCC, + 0xDD, + 0xEE, + CausalMask::SPO, + 0b111, + PlasticityState::ALL_HOT, + ) + .with_w_slot(63) + .with_truth(TrustTexture::Murky) + .with_inference_mantissa(-7); + assert_eq!( + base.reasoning_band(), + ReasoningBand::Surface, + "fixture spare-bits must start at 0 for the min->max diff to be exact" + ); + + let after = base.with_reasoning_band(ReasoningBand::Transcendent); + let diff = base.0 ^ after.0; + assert_eq!( + diff, + crate::layout::SPARE_MASK, + "with_reasoning_band(min->max) must flip exactly the SPARE_MASK bits, nothing else" + ); + + assert_eq!(after.s_idx(), 0xAA, "S disturbed by with_reasoning_band"); + assert_eq!(after.p_idx(), 0xBB, "P disturbed by with_reasoning_band"); + assert_eq!(after.o_idx(), 0xCC, "O disturbed by with_reasoning_band"); + assert_eq!( + after.frequency_u8(), + 0xDD, + "frequency disturbed by with_reasoning_band" + ); + assert_eq!( + after.confidence_u8(), + 0xEE, + "confidence disturbed by with_reasoning_band" + ); + assert_eq!( + after.causal_mask(), + CausalMask::SPO, + "causal_mask disturbed by with_reasoning_band" + ); + assert_eq!( + after.direction(), + 0b111, + "direction disturbed by with_reasoning_band" + ); + assert_eq!( + after.inference_mantissa(), + -7, + "inference_mantissa disturbed by with_reasoning_band" + ); + assert_eq!( + after.plasticity(), + PlasticityState::ALL_HOT, + "plasticity disturbed by with_reasoning_band" + ); + assert_eq!( + after.w_slot(), + 63, + "w_slot disturbed by with_reasoning_band" + ); + assert_eq!( + after.truth(), + TrustTexture::Murky, + "truth disturbed by with_reasoning_band" + ); + } + + // ── 7. spare() stays consistent with reasoning_band() after its write ──── + + #[test] + fn test_spare_stays_consistent_with_reasoning_band_after_a_reasoning_band_write() { + for band in [ + ReasoningBand::Surface, + ReasoningBand::Association, + ReasoningBand::Relation, + ReasoningBand::Causal, + ReasoningBand::Counterfactual, + ReasoningBand::Perspective, + ReasoningBand::Meta, + ReasoningBand::Transcendent, + ] { + let edge = CausalEdge64::ZERO.with_reasoning_band(band); + assert_eq!( + edge.spare(), + band.to_bits_3(), + "spare() must still report the identical 3-bit value reasoning_band() encodes for {band:?}" + ); + } + } + + // ── 8. All eight texture-band values round-trip ───────────────────────── + + #[test] + fn test_reasoning_band_all_eight_values_round_trip() { + for band in [ + ReasoningBand::Surface, + ReasoningBand::Association, + ReasoningBand::Relation, + ReasoningBand::Causal, + ReasoningBand::Counterfactual, + ReasoningBand::Perspective, + ReasoningBand::Meta, + ReasoningBand::Transcendent, + ] { + let edge = CausalEdge64::ZERO.with_reasoning_band(band); + assert_eq!( + edge.reasoning_band(), + band, + "reasoning_band round-trip failed for {band:?}" + ); + } + } + + // ── 9. Field-isolation matrix: composed setters, every other field ───── + + #[test] + fn test_field_isolation_matrix_survives_both_new_setters_composed() { + // One shared fixture with every field at a distinct, non-zero value, + // then BOTH new setters applied together (a composable builder + // chain, matching the intended call-site pattern: + // `edge.with_topology(...).with_reasoning_band(...)`). + let base = CausalEdge64::pack_v2( + 11, // S + 22, // P + 33, // O + 44, // frequency + 55, // confidence + CausalMask::SO, + 0b110, // direction + PlasticityState::P_HOT, + ) + .with_w_slot(17) + .with_inference_mantissa(5); + + let edge = base + .with_topology(CausalTopology::IndirectUnknownIntermediates) + .with_reasoning_band(ReasoningBand::Causal); + + assert_eq!(edge.s_idx(), 11, "S disturbed by new setters"); + assert_eq!(edge.p_idx(), 22, "P disturbed by new setters"); + assert_eq!(edge.o_idx(), 33, "O disturbed by new setters"); + assert_eq!( + edge.frequency_u8(), + 44, + "frequency disturbed by new setters" + ); + assert_eq!( + edge.confidence_u8(), + 55, + "confidence disturbed by new setters" + ); + assert_eq!( + edge.causal_mask(), + CausalMask::SO, + "causal_mask disturbed by new setters" + ); + assert_eq!( + edge.direction(), + 0b110, + "direction disturbed by new setters" + ); + assert_eq!( + edge.inference_mantissa(), + 5, + "inference_mantissa disturbed by new setters" + ); + assert_eq!( + edge.plasticity(), + PlasticityState::P_HOT, + "plasticity disturbed by new setters" + ); + assert_eq!(edge.w_slot(), 17, "w_slot disturbed by new setters"); + + // And the two new fields landed correctly, composed. + assert_eq!( + edge.topology(), + CausalTopology::IndirectUnknownIntermediates + ); + assert_eq!(edge.reasoning_band(), ReasoningBand::Causal); + } + + // ── 10. Counterfactual mantissa (-6) round-trips independent of band ─── + + #[test] + fn test_counterfactual_mantissa_round_trips_independently_of_reasoning_band() { + let base = CausalEdge64::pack_v2( + 1, + 2, + 3, + 200, + 200, + CausalMask::SPO, + 0, + PlasticityState::ALL_FROZEN, + ) + .with_inference_mantissa(InferenceType::Counterfactual.to_mantissa()); + assert_eq!( + base.inference_mantissa(), + -6, + "fixture must carry mantissa -6" + ); + + for band in [ + ReasoningBand::Surface, + ReasoningBand::Meta, + ReasoningBand::Transcendent, + ] { + let edge = base.with_reasoning_band(band); + assert_eq!( + edge.inference_mantissa(), + -6, + "counterfactual mantissa must survive a reasoning_band write for {band:?}" + ); + assert_eq!(edge.reasoning_band(), band); + } + } + + // ── 11. ReasoningBand::Counterfactual does NOT imply mantissa == -6 ─────── + + #[test] + fn test_reasoning_band_counterfactual_does_not_imply_mantissa_minus_six() { + // ReasoningBand::Counterfactual set, mantissa left at a DIFFERENT + // value — proves the two are orthogonal, never derived from one + // another. + let edge = CausalEdge64::ZERO + .with_reasoning_band(ReasoningBand::Counterfactual) + .with_inference_mantissa(3); + assert_eq!(edge.reasoning_band(), ReasoningBand::Counterfactual); + assert_eq!( + edge.inference_mantissa(), + 3, + "mantissa must NOT be derived from reasoning_band" + ); + assert_ne!(edge.inference_mantissa(), -6); + + // And the converse: mantissa == -6 (the Counterfactual InferenceType + // slot) with a DIFFERENT reasoning_band. + let edge2 = CausalEdge64::ZERO + .with_inference_mantissa(InferenceType::Counterfactual.to_mantissa()) + .with_reasoning_band(ReasoningBand::Meta); + assert_eq!(edge2.inference_mantissa(), -6); + assert_eq!( + edge2.reasoning_band(), + ReasoningBand::Meta, + "reasoning_band must NOT be derived from inference_mantissa" + ); + assert_ne!(edge2.reasoning_band(), ReasoningBand::Counterfactual); + } + + // ── 12. CausalTopology::Direct does not imply any NARS confidence ────── + + #[test] + fn test_causal_topology_direct_does_not_imply_any_nars_confidence() { + // Direct topology (0) coexists with every confidence level — no + // auto-derivation from/to NARS confidence in either direction. + for conf in [0u8, 1, 128, 254, 255] { + let edge = CausalEdge64::pack_v2( + 1, + 2, + 3, + 100, + conf, + CausalMask::None, + 0, + PlasticityState::ALL_FROZEN, + ) + .with_topology(CausalTopology::Direct); + assert_eq!(edge.topology(), CausalTopology::Direct); + assert_eq!( + edge.confidence_u8(), + conf, + "CausalTopology::Direct must not constrain or derive confidence={conf}" + ); + } + } }