Skip to content
227 changes: 227 additions & 0 deletions .claude/prompts/04a_merkle_eineindeutigkeit_addon.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,227 @@
# Addendum 04a: Merkle Root for Eineindeutigkeit Resolution

## Add to: `src/spo/clam_path.rs` (Phase 5, currently building)

## The Problem

Three queries converge on the same concept — semantic, explicit, and relational
all land in same σ-bands. That's Eineindeutigkeit (unique determination).
But WHERE does the concept live?

Without content-addressing, the address depends on which query found it first.
Three paths → three ClamPath addresses → three Redis keys → **duplication**.

## The Solution: ClamPath + MerkleRoot in word[0]

```
word[0] of CogRecord8K (u64, 64 bits):
┌────────────────────┬────────────────────────────────────┐
│ ClamPath (24 bits) │ MerkleRoot (40 bits) │
│ HOW you got here │ WHAT lives here │
│ navigation/lineage │ identity/canonical address │
└────────────────────┴────────────────────────────────────┘

ClamPath = structural path through B-tree (depth + split decisions)
MerkleRoot = truncated blake3 of content fingerprint → canonical identity
```

## Why Both

```
ClamPath alone: path-dependent → same concept gets different addresses
MerkleRoot alone: no lineage, no subtree range queries, no causality chains
Together: navigate via ClamPath, resolve identity via MerkleRoot
```

## MerkleRoot Derivation

The Merkle root is derived from the three-plane binary fingerprints.
This is deterministic: same concept = same fingerprint = same root.

```rust
use blake3;

/// Truncated Merkle root for content-addressed identity.
/// 40 bits = ~1 trillion collision space. Sufficient for BindSpace.
#[derive(Clone, Copy, PartialEq, Eq, Hash)]
pub struct MerkleRoot(pub u64); // only lower 40 bits used

impl MerkleRoot {
/// Derive from three-plane binary fingerprints.
/// The canonical address IS the content hash.
pub fn from_planes(
s_binary: &[u8; 2048], // 16384-bit S-plane
p_binary: &[u8; 2048], // 16384-bit P-plane
o_binary: &[u8; 2048], // 16384-bit O-plane
) -> Self {
// Hash each plane separately, then combine.
// This is a Merkle tree with 3 leaves.
let s_hash = blake3::hash(s_binary);
let p_hash = blake3::hash(p_binary);
let o_hash = blake3::hash(o_binary);

// Combine: hash(S || P || O) — order matters (S < P < O is canonical)
let mut hasher = blake3::Hasher::new();
hasher.update(s_hash.as_bytes());
hasher.update(p_hash.as_bytes());
hasher.update(o_hash.as_bytes());
let root = hasher.finalize();

// Truncate to 40 bits
let bytes = root.as_bytes();
let val = u64::from_le_bytes([
bytes[0], bytes[1], bytes[2], bytes[3], bytes[4], 0, 0, 0,
]);
MerkleRoot(val & 0xFF_FFFF_FFFF) // mask to 40 bits
}

/// Derive from single composite fingerprint (backward compat).
/// Used when planes aren't separated yet.
pub fn from_fingerprint(fp: &[u8; 2048]) -> Self {
let hash = blake3::hash(fp);
let bytes = hash.as_bytes();
let val = u64::from_le_bytes([
bytes[0], bytes[1], bytes[2], bytes[3], bytes[4], 0, 0, 0,
]);
MerkleRoot(val & 0xFF_FFFF_FFFF)
}

/// The Redis key for this concept's canonical address.
pub fn redis_key(&self) -> String {
format!("ada:bind:{:010x}", self.0)
}
}
```

## Packing into word[0]

```rust
impl ClamPath {
/// Pack ClamPath (24 bits) + MerkleRoot (40 bits) into a single u64.
pub fn pack_with_merkle(&self, root: MerkleRoot) -> u64 {
let clam_bits = self.to_u24() as u64; // 24 bits
let merkle_bits = root.0 & 0xFF_FFFF_FFFF; // 40 bits
(clam_bits << 40) | merkle_bits
}

/// Unpack from word[0].
pub fn unpack_with_merkle(word0: u64) -> (ClamPath, MerkleRoot) {
let clam_bits = (word0 >> 40) as u32; // upper 24 bits
let merkle_bits = word0 & 0xFF_FFFF_FFFF; // lower 40 bits
(ClamPath::from_u24(clam_bits), MerkleRoot(merkle_bits))
}
}
```

## Self-Healing Redis Address

This is the key insight for distributed operation:

```
1. Concept arrives → three-plane fingerprints computed
2. MerkleRoot derived deterministically: blake3(S || P || O)
3. Redis key = ada:bind:{merkle_root_hex}
4. ClamPath = structural location in B-tree

If Redis evicts the key:
- Recompute MerkleRoot from fingerprints (deterministic)
- Key is reconstructed: same content → same hash → same address
- Tree heals itself

If two paths find the same concept:
- Both compute same MerkleRoot (content-addressed)
- Both write to same Redis key
- Eineindeutigkeit: last-write-wins is fine because content is identical
- ClamPaths may differ (different navigation routes) — that's OK,
ClamPath is HOW, MerkleRoot is WHAT

If concept is modified (evidence updates the soaking register):
- MerkleRoot changes (new content → new hash)
- Old key naturally expires (TTL or eviction)
- New key created at new address
- No dangling references: anyone who had the old root will miss,
recompute from current fingerprints, find the new address
```

## Merkle Tree for Subtree Integrity (Future)

When the hive goes multi-writer, extend the per-concept MerkleRoot
to a full Merkle tree over the CLAM subtree:

```
root_hash
/ \
left_hash right_hash
/ \ / \
leaf_0 leaf_1 leaf_2 leaf_3
(concept fingerprints)
```

Each CLAM split level has a hash that summarizes its children.
To verify a subtree after network partition:

```rust
/// Verify subtree integrity after reconnection.
/// Compare roots — if they match, entire subtree is consistent.
/// If they diverge, walk down to find the first differing leaf.
pub fn verify_subtree(
local_root: MerkleRoot,
remote_root: MerkleRoot,
clam_path: &ClamPath,
) -> SubtreeStatus {
if local_root == remote_root {
SubtreeStatus::Consistent
} else {
// Walk the Merkle tree to find divergence point
SubtreeStatus::Diverged { at_depth: /* compare level by level */ }
}
}
```

But this is ice cake 21. For now: per-concept MerkleRoot in word[0] is sufficient.

## Dependency

```toml
# Cargo.toml
blake3 = "1"
```

Blake3 is fast (~1GB/s on modern CPUs), deterministic, and the truncated
40-bit output is sufficient for ~1 trillion address space.

## Integration with Existing ClamPath

The existing `04_btree_clam_path_lineage.md` spec defines ClamPath as u16 (16 bits)
with depth in a separate u8. Repack as u24 to fit alongside MerkleRoot in word[0]:

```rust
impl ClamPath {
/// Pack bits (u16) + depth (u8) into 24 bits.
pub fn to_u24(&self) -> u32 {
((self.depth as u32) << 16) | (self.bits as u32)
}

/// Unpack from 24 bits.
pub fn from_u24(packed: u32) -> Self {
ClamPath {
bits: (packed & 0xFFFF) as u16,
depth: ((packed >> 16) & 0xFF) as u8,
}
}
}
```

## Summary

| Component | Bits | Purpose |
|-----------|------|---------|
| ClamPath.bits | 16 | B-tree split decisions (navigation) |
| ClamPath.depth | 8 | Valid bit count (confidence/resolution) |
| MerkleRoot | 40 | Content-addressed canonical identity |
| **Total** | **64** | **= word[0] of CogRecord8K** |

ClamPath = HOW you got here.
MerkleRoot = WHAT lives here.
Redis key = WHERE it's stored.
Self-healing = WHY it works distributed.
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,7 @@ chrono = { version = "0.4", features = ["serde"] }
hex = "0.4"
base64 = "0.22"
sha2 = "0.10"
blake3 = "1"
rand = "0.9"
num-traits = "0.2"
num_cpus = "1.17"
Expand Down
53 changes: 6 additions & 47 deletions src/container/migrate.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
//! Migration: 16K Fingerprint → 2×8K CogRecord.
//! Migration: 16K Fingerprint → CogRecord.
//!
//! With CONTAINER_WORDS = 256 a single Container holds the full 16K fingerprint.
//! The old "Extended" split into two 128-word halves is no longer needed.

use super::CONTAINER_WORDS;
use super::geometry::ContainerGeometry;
use super::meta::MetaViewMut;
use super::record::CogRecord;

/// Convert a 16K Fingerprint ([u64; 256]) to a 2-container CogRecord (Cam geometry).
/// Convert a 16K Fingerprint ([u64; 256]) to a CogRecord (Cam geometry).
///
/// The first 128 words (highest signal density) become content container 1.
/// All 256 words fit into the single content container (CONTAINER_WORDS = 256).
/// If the old vector had schema sidecar data in words 208-255, it is extracted
/// and placed into the metadata container using the new layout.
pub fn migrate_16k(old: &[u64; 256]) -> CogRecord {
Expand Down Expand Up @@ -40,57 +43,13 @@ pub fn migrate_16k(old: &[u64; 256]) -> CogRecord {
record
}

/// Convert a 16K Fingerprint to Extended geometry (two linked records).
/// Returns the primary record (words 0..127) and secondary record (words 128..255).
/// With single-content CogRecord, Extended geometry uses linked records.
pub fn migrate_16k_extended(old: &[u64; 256]) -> (CogRecord, CogRecord) {
let mut primary = CogRecord::new(ContainerGeometry::Extended);
primary.content
.words
.copy_from_slice(&old[..CONTAINER_WORDS]);

{
let mut meta = MetaViewMut::new(&mut primary.meta.words);
meta.set_schema_version(1);
meta.set_container_count(2); // this record = meta + content
meta.set_geometry(ContainerGeometry::Extended);
meta.update_checksum();
}

let mut secondary = CogRecord::new(ContainerGeometry::Extended);
secondary.content
.words
.copy_from_slice(&old[CONTAINER_WORDS..2 * CONTAINER_WORDS]);

{
let mut meta = MetaViewMut::new(&mut secondary.meta.words);
meta.set_schema_version(1);
meta.set_container_count(2);
meta.set_geometry(ContainerGeometry::Extended);
meta.update_checksum();
}

(primary, secondary)
}

/// Convert a single CogRecord back to 16K Fingerprint ([u64; 256]).
/// The content fills words 0..127; words 128..255 are zero.
/// For Extended/Xyz with linked records, use `to_16k_linked` instead.
pub fn to_16k(record: &CogRecord) -> [u64; 256] {
let mut out = [0u64; 256];
out[..CONTAINER_WORDS].copy_from_slice(&record.content.words);
out
}

/// Convert a pair of linked CogRecords (Extended geometry) back to 16K Fingerprint.
/// Primary content → words 0..127, secondary content → words 128..255.
pub fn to_16k_linked(primary: &CogRecord, secondary: &CogRecord) -> [u64; 256] {
let mut out = [0u64; 256];
out[..CONTAINER_WORDS].copy_from_slice(&primary.content.words);
out[CONTAINER_WORDS..2 * CONTAINER_WORDS].copy_from_slice(&secondary.content.words);
out
}

/// Extract known fields from old 16K sidecar (32 words) into new metadata.
fn extract_sidecar_to_meta(sidecar: &[u64], meta_words: &mut [u64; CONTAINER_WORDS]) {
// Old block 14 layout (words 224-239 of 16K, mapped to sidecar[0..15]):
Expand Down
11 changes: 4 additions & 7 deletions src/container/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -887,18 +887,15 @@ fn test_migration_16k_to_container() {
}

#[test]
fn test_migration_extended_roundtrip() {
fn test_migration_roundtrip() {
// With CONTAINER_WORDS=256, a single CogRecord holds the full fingerprint.
let mut old = [0u64; 256];
for i in 0..256 {
old[i] = (i as u64 + 1).wrapping_mul(0x0101_0101_0101_0101);
}

let (primary, secondary) = migrate::migrate_16k_extended(&old);
assert_eq!(primary.geometry(), ContainerGeometry::Extended);
assert_eq!(secondary.geometry(), ContainerGeometry::Extended);

// Roundtrip using linked records
let back = migrate::to_16k_linked(&primary, &secondary);
let record = migrate::migrate_16k(&old);
let back = migrate::to_16k(&record);
assert_eq!(back, old);
}

Expand Down
7 changes: 1 addition & 6 deletions src/learning/feedback.rs
Original file line number Diff line number Diff line change
Expand Up @@ -393,13 +393,8 @@ pub fn apply_feedback(

const W_EXT_NARS_BASE: usize = 160;

// Update decay rate (W160 hi)
// Update decay rate in W160 hi (lo = horizon, hi = decay)
let decay_bits = signal.decay_rate.to_bits() as u64;
words[W_EXT_NARS_BASE] =
(words[W_EXT_NARS_BASE] & !(!0u64 << 32 << 0)) // keep hi bits
| (words[W_EXT_NARS_BASE] & 0xFFFF_FFFF) // keep lo bits
;
// Actually: W160 lo = horizon, W160 hi = decay
words[W_EXT_NARS_BASE] =
(words[W_EXT_NARS_BASE] & 0xFFFF_FFFF) | (decay_bits << 32);

Expand Down
30 changes: 30 additions & 0 deletions src/nars/truth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

use std::fmt;

use rustynum_bnn::causal_trajectory::NarsTruth;

/// NARS truth value: <frequency, confidence>
///
/// - **frequency** (f): Proportion of positive evidence (0.0 - 1.0)
Expand Down Expand Up @@ -249,6 +251,34 @@ impl Default for TruthValue {
}
}

// =============================================================================
// NarsTruth ↔ TruthValue bridge (R6 fix: compile-time guaranteed conversion)
// =============================================================================

impl From<NarsTruth> for TruthValue {
fn from(nars: NarsTruth) -> Self {
Self::new(nars.f.clamp(0.0, 1.0), nars.c.clamp(0.0, 1.0))
}
}

impl From<&NarsTruth> for TruthValue {
fn from(nars: &NarsTruth) -> Self {
Self::new(nars.f.clamp(0.0, 1.0), nars.c.clamp(0.0, 1.0))
}
}

impl From<TruthValue> for NarsTruth {
fn from(truth: TruthValue) -> Self {
NarsTruth::new(truth.frequency, truth.confidence)
}
}

impl From<&TruthValue> for NarsTruth {
fn from(truth: &TruthValue) -> Self {
NarsTruth::new(truth.frequency, truth.confidence)
}
}

#[cfg(test)]
mod tests {
use super::*;
Expand Down
Loading
Loading