diff --git a/.claude/blackboard.md b/.claude/blackboard.md index 8d733430..0a6460bc 100644 --- a/.claude/blackboard.md +++ b/.claude/blackboard.md @@ -1,3 +1,10 @@ +## 2026-10-17 — `causal_diff::CausalEdge64` packs the canonical CE64 layout + +- **Change:** `CausalEdge64::pack` writes lance-graph's canonical regions (`causal_edge::layout`): S = block (0..7), P = projection code (8..15), O = verb code (16..23), NARS frequency u8 at 24..31, confidence u8 at 32..39; bits 40..63 stay 0. `block()`, `projection()`, `verb()`, `frequency()` and `confidence()` read those regions. Before, the word used its own layout (block at 58, projection at 54, verb at 52, row at 36, L1 at 20, 10-bit truth at 10/0), so the same type name meant two different words in the two repos. +- **Kept, not renamed:** `row_idx()` and `l1_distance()` keep their signatures, are `#[deprecated]` and return 0. Row index and L1 distance have no field in the canonical word and stay in `WeightEdge`. Truth precision drops from 10 to 8 bits; the round-trip test's 0.002 tolerance still holds. +- **Tests:** `causal_edge64_packs_the_canonical_regions`, `row_and_l1_do_not_change_the_word`. Disable runs, each red: block back at 58, row written into bits 40.., frequency at 10. +- **Sibling:** the jina `pack_edge` rewrite (#365) is the same restoration for `hpc::jina::causal`. Still to do: `p64_bridge::causal_edge_compat`, the `entropy_ladder` Pearl bit order and spare-bit docs, the four OGIT leaf `basinSignature` values. + ## 2026-10-14 — Haiku cards for supervised harvest and mapping - Five Haiku cards added (`harvest-scout`, `census-counter`, `quote-harvester`, `substrate-mapper`, `spec-editor`); rules in `.claude/rules/agent-model-tiers.md`, CLAUDE.md Agent Protocol item 7. No `guarded-executor`: only the orchestrator compiles here. diff --git a/src/hpc/causal_diff.rs b/src/hpc/causal_diff.rs index e2478021..eb70bc69 100644 --- a/src/hpc/causal_diff.rs +++ b/src/hpc/causal_diff.rs @@ -131,20 +131,19 @@ pub struct WeightEdge { // CausalEdge64 — compact u64 packed edge for L1/p64 operations // ============================================================================ -/// Compact causal edge packed into a single `u64`. -/// -/// Layout (64 bits): +/// Compact causal edge packed into a single `u64`, in the canonical +/// CausalEdge64 layout (lance-graph `causal_edge::layout`): /// ```text -/// [63:58] block 6 bits → 0–63 layers -/// [57:54] projection 4 bits → Projection enum (0–9) -/// [53:52] verb 2 bits → Verb enum (0–2) -/// [51:36] row_idx 16 bits → 0–65535 rows -/// [35:20] l1_dist 16 bits → L1 distance (capped at 65535) -/// [19:10] freq 10 bits → NARS frequency × 1023 -/// [ 9: 0] conf 10 bits → NARS confidence × 1023 +/// [ 7: 0] S block (layer) +/// [15: 8] P projection code (Projection enum, 0–9) +/// [23:16] O verb code (Becomes 0, Supports 1, Contradicts 2) +/// [31:24] frequency NARS frequency × 255 +/// [39:32] confidence NARS confidence × 255 +/// [63:40] contract left at its default (0) /// ``` /// -/// Total: 6 + 4 + 2 + 16 + 16 + 10 + 10 = 64 bits. +/// The row index and L1 distance have no field in that word; they stay in +/// the [`WeightEdge`] the edge was packed from. /// /// Fits in a register. Can be used as a p64 `Palette64` row key: /// `edge.0 & palette.rows[i]` → POPCNT → attention score. @@ -156,59 +155,61 @@ impl CausalEdge64 { /// Pack a WeightEdge into a compact u64. #[inline] pub fn pack(edge: &WeightEdge) -> Self { - let block = (edge.block.unwrap_or(0) & 0x3F) as u64; + let block = (edge.block.unwrap_or(0) & 0xFF) as u64; let proj = projection_to_u4(&edge.projection) as u64; let verb = match edge.verb { Verb::Becomes => 0u64, Verb::Supports => 1, Verb::Contradicts => 2, }; - let row = (edge.row_idx as u64) & 0xFFFF; - let l1 = (edge.l1_distance.min(65535) as u64) & 0xFFFF; - let freq = ((edge.truth.frequency * 1023.0).round() as u64) & 0x3FF; - let conf = ((edge.truth.confidence * 1023.0).round() as u64) & 0x3FF; + let freq = ((edge.truth.frequency.clamp(0.0, 1.0) * 255.0).round() as u64) & 0xFF; + let conf = ((edge.truth.confidence.clamp(0.0, 1.0) * 255.0).round() as u64) & 0xFF; - Self((block << 58) | (proj << 54) | (verb << 52) | (row << 36) | (l1 << 20) | (freq << 10) | conf) + Self(block | (proj << 8) | (verb << 16) | (freq << 24) | (conf << 32)) } /// Unpack block number. #[inline] pub fn block(self) -> u32 { - ((self.0 >> 58) & 0x3F) as u32 + (self.0 & 0xFF) as u32 } /// Unpack projection type. #[inline] pub fn projection(self) -> Projection { - u4_to_projection(((self.0 >> 54) & 0xF) as u8) + u4_to_projection(((self.0 >> 8) & 0xFF) as u8) } /// Unpack verb. #[inline] pub fn verb(self) -> Verb { - match (self.0 >> 52) & 0x3 { + match (self.0 >> 16) & 0xFF { 0 => Verb::Becomes, 1 => Verb::Supports, _ => Verb::Contradicts, } } - /// Unpack row index. + /// The canonical word has no row field, so this always returns 0. Read + /// `WeightEdge::row_idx`. + #[deprecated(note = "CausalEdge64 has no row field; read WeightEdge::row_idx")] #[inline] pub fn row_idx(self) -> u32 { - ((self.0 >> 36) & 0xFFFF) as u32 + 0 } - /// Unpack L1 distance. + /// The canonical word has no L1 field, so this always returns 0. Read + /// `WeightEdge::l1_distance`. + #[deprecated(note = "CausalEdge64 has no L1 field; read WeightEdge::l1_distance")] #[inline] pub fn l1_distance(self) -> u32 { - ((self.0 >> 20) & 0xFFFF) as u32 + 0 } /// Unpack NARS frequency. #[inline] pub fn frequency(self) -> f32 { - ((self.0 >> 10) & 0x3FF) as f32 / 1023.0 + ((self.0 >> 24) & 0xFF) as f32 / 255.0 } /// Unpack NARS confidence. #[inline] pub fn confidence(self) -> f32 { - (self.0 & 0x3FF) as f32 / 1023.0 + ((self.0 >> 32) & 0xFF) as f32 / 255.0 } /// Reconstruct NarsTruth. #[inline] @@ -1562,8 +1563,6 @@ mod tests { assert_eq!(packed.block(), 17); assert_eq!(packed.projection(), Projection::Q); assert_eq!(packed.verb(), Verb::Becomes); - assert_eq!(packed.row_idx(), 42); - assert_eq!(packed.l1_distance(), 1234); assert!((packed.frequency() - 0.72).abs() < 0.002); assert!((packed.confidence() - 0.95).abs() < 0.002); @@ -1571,6 +1570,39 @@ mod tests { assert_eq!(std::mem::size_of::(), 8); } + fn weight_edge(block: u32, row_idx: u32, l1_distance: u32, verb: Verb) -> WeightEdge { + WeightEdge { + tensor_name: String::new(), + row_idx, + block: Some(block), + projection: Projection::FfnGate, + layer_type: LayerType::Attention, + verb, + l1_distance, + truth: NarsTruth::new(0.72, 0.95), + } + } + + // The canonical CausalEdge64 regions (lance-graph `causal_edge::layout`): + // 0..23 S/P/O codes, 24..31 frequency, 32..39 confidence, 40..63 contract. + #[test] + fn causal_edge64_packs_the_canonical_regions() { + let w = CausalEdge64::pack(&weight_edge(17, 42, 1234, Verb::Contradicts)).0; + assert_eq!(w & 0xFF, 17, "S = block"); + assert_eq!((w >> 8) & 0xFF, 5, "P = projection code (FfnGate)"); + assert_eq!((w >> 16) & 0xFF, 2, "O = verb code (Contradicts)"); + assert_eq!((w >> 24) & 0xFF, (0.72f32 * 255.0).round() as u64, "F at 24..31"); + assert_eq!((w >> 32) & 0xFF, (0.95f32 * 255.0).round() as u64, "C at 32..39"); + assert_eq!(w >> 40, 0, "the 24-bit contract stays at its default"); + } + + #[test] + fn row_and_l1_do_not_change_the_word() { + let a = CausalEdge64::pack(&weight_edge(3, 1, 10, Verb::Becomes)); + let b = CausalEdge64::pack(&weight_edge(3, 60_000, 65_000, Verb::Becomes)); + assert_eq!(a, b, "row and L1 stay in WeightEdge, not in the word"); + } + #[test] fn test_causal_edge64_all_projections() { for (proj, val) in [