diff --git a/.claude/board/AGENT_LOG.md b/.claude/board/AGENT_LOG.md index 5eac2840..e6ed1e03 100644 --- a/.claude/board/AGENT_LOG.md +++ b/.claude/board/AGENT_LOG.md @@ -1,3 +1,30 @@ +## [Fleet sprint-11-wave-b-qualia-i4] [IN PR] D-CSV-2 QualiaI4_16D + OQ-CSV-1 ratification (branch claude/sprint-11-wave-b-qualia-i4) + +**D-id:** D-CSV-2 — `QualiaI4_16D` type in `lance-graph-contract::qualia` + f32↔i4 migration helpers (~250 LOC actual vs ~180 estimate; the +70 over estimate is accessor + magnitude + 8 tests). + +**OQ-CSV-1 ratification (main-thread, autoattended):** Option α — keep the canonical convergence-observable vocab from `Qualia17D` / `QualiaVector` (arousal/valence/tension/warmth/clarity/boundary/depth/velocity/entropy/coherence/intimacy/presence/assertion/receptivity/groundedness/expansion/integration), drop dim 16 "integration" to fit 16 i4 lanes (recoverable on demand from valence + coherence + cycle-delta). Plan §7.2 proposed felt-qualia vocab (Wisdom/Trust/Hope/etc.) was a CONJECTURE per the plan footnote; cross-check against `crates/thinking-engine/src/qualia.rs` revealed the canonical surface is observables, not felt-qualia. Lower migration risk than vocab swap. + +**Worker:** W-B1 (Sonnet, single worker — D-CSV-2 alone since D-CSV-5 is blocked on PR #383 merge). + +**Files modified:** +- `crates/lance-graph-contract/src/qualia.rs` (+250 LOC): `QUALIA_I4_DIMS=16`, `QUALIA_I4_LABELS` (first 16 of `AXIS_LABELS`), `pub struct QualiaI4_16D(pub u64) #[repr(C, align(8))]`, get/set/with i4 signed accessors with `(raw << 4) >> 4` sign-extension, `from_f32_17d` / `to_f32_17d` migration helpers (asymmetric quantization: positive `× 7.0`, negative `× 8.0`), `magnitude()` = `coherence.saturating_mul(valence)` per §7.2 intent. +- `crates/lance-graph-contract/src/lib.rs`: re-exports `QualiaI4_16D`, `QUALIA_I4_DIMS`, `QUALIA_I4_LABELS`. + +**Tests:** 14 pass / 0 fail in `cargo test -p lance-graph-contract qualia` (8 new + 6 pre-existing). Contract crate remains zero-dep. + +**Coverage of the 8 new tests:** +- size invariant (8 bytes) +- zero default (all 16 dims = 0) +- signed roundtrip across [-8, -7, -1, 0, 1, 7] +- clamp on overflow (+100 → +7, -100 → -8) +- field isolation (set dim 5, dims 4 + 6 untouched) +- from_f32_17d ↔ to_f32_17d round-trip with dim 16 dropped +- label alignment with canonical AXIS_LABELS[0..16] +- magnitude saturating_mul on extremes + +**Outcome:** D-CSV-2 ready for merge. D-CSV-5 (QualiaColumn migration) blocked on PR #383 (D-CSV-1 v2 layout) merge AND requires `cognitive-shader-driver` crate which is referenced in CLAUDE.md but not in workspace members — investigation needed before Wave C spawn. + +**No P0 found in code review.** The asymmetric f32 quantization (`× 7.0` for positive vs `× 8.0` for negative) is intentional: it preserves sign-bit coverage of i4 (range −8..+7 has 7 positive slots and 8 negative slots, so f32 [0, 1] maps to 7 quanta and [-1, 0] maps to 8 quanta — symmetric in resolution per slot, asymmetric in mapping). Round-trip preserves sign and approximate magnitude within the i4 quantization envelope. ## [Fleet sprint-11-wave-a-impl] [IN PR] D-CSV-1 + D-CSV-3 + D-CSV-4 (branch claude/sprint-11-wave-a-impl, commit ab39d01) **D-id(s):** D-CSV-1 (causal-edge v2 layout), D-CSV-3 (signed-mantissa InferenceType expansion), D-CSV-4 (CollapseGateEmission in contract). diff --git a/.claude/board/STATUS_BOARD.md b/.claude/board/STATUS_BOARD.md index 23530ed5..e15eaefd 100644 --- a/.claude/board/STATUS_BOARD.md +++ b/.claude/board/STATUS_BOARD.md @@ -426,10 +426,10 @@ Consolidates sprint-10 architectural decisions before context dilution. | D-id | Title | Status | PR / Evidence | |---|---|---|---| -| D-CSV-1 | `causal-edge` crate v2 layout (signed mantissa, W-slot, lens, drop temporal) | **In PR** | branch `claude/sprint-11-wave-a-impl`, commit `ab39d01`; OQ-CSV-2 ratified to 6 bits (default) | -| D-CSV-2 | `QualiaI4_16D` type in `lance-graph-contract::qualia` + f32↔i4 migration helpers | **Queued** | blocked on OQ-CSV-1 (per-dim assignment) | -| D-CSV-3 | InferenceType signed-mantissa expansion (absorbs PR-LL-1 Intervention/Counterfactual into canonical edge enum) | **In PR** | branch `claude/sprint-11-wave-a-impl`, paired with D-CSV-1 in same crate | -| D-CSV-4 | `CollapseGateEmission` wire format spec + impl per plan §8 | **In PR** | branch `claude/sprint-11-wave-a-impl`, contract crate (Vec instead of SmallVec to preserve zero-dep) | +| D-CSV-1 | `causal-edge` crate v2 layout (signed mantissa, W-slot, lens, drop temporal) | **Shipped** | PR #383 merge `03bd175`; OQ-CSV-2 ratified to 6 bits (default) | +| D-CSV-2 | `QualiaI4_16D` type in `lance-graph-contract::qualia` + f32↔i4 migration helpers | **In PR** | branch `claude/sprint-11-wave-b-qualia-i4` (PR #384); OQ-CSV-1 ratified to Option α (canonical convergence-observable vocab; drop dim 16 "integration") | +| D-CSV-3 | InferenceType signed-mantissa expansion (absorbs PR-LL-1 Intervention/Counterfactual into canonical edge enum) | **Shipped** | PR #383 merge `03bd175`, paired with D-CSV-1 in same crate | +| D-CSV-4 | `CollapseGateEmission` wire format spec + impl per plan §8 | **Shipped** | PR #383 merge `03bd175`, contract crate (Vec instead of SmallVec to preserve zero-dep) | ### Phase B — Storage & dispatch path (sprint-11) diff --git a/crates/lance-graph-contract/src/lib.rs b/crates/lance-graph-contract/src/lib.rs index 2b75c84e..b6a4a060 100644 --- a/crates/lance-graph-contract/src/lib.rs +++ b/crates/lance-graph-contract/src/lib.rs @@ -65,6 +65,10 @@ pub mod plan; pub mod property; pub mod proprioception; pub mod qualia; +pub use qualia::{ + axis_index, axis_label, qualia_to_state, QualiaI4_16D, QualiaVector, AXIS_LABELS, MIDPOINT, + QUALIA_DIMS, QUALIA_I4_DIMS, QUALIA_I4_LABELS, ZERO, +}; pub mod reasoning; pub mod repository; pub mod scenario; diff --git a/crates/lance-graph-contract/src/qualia.rs b/crates/lance-graph-contract/src/qualia.rs index b480174b..03004a3e 100644 --- a/crates/lance-graph-contract/src/qualia.rs +++ b/crates/lance-graph-contract/src/qualia.rs @@ -131,6 +131,137 @@ pub const MIDPOINT: QualiaVector = [0.5; QUALIA_DIMS]; // Tests // ═══════════════════════════════════════════════════════════════════════════ +// ═══════════════════════════════════════════════════════════════════════════ +// i4-16D packed qualia vector (QualiaI4_16D) +// ═══════════════════════════════════════════════════════════════════════════ + +/// Dimensionality of the i4-16D packed qualia vector. +/// Dim 16 ("integration") from the canonical 17D is dropped to fit 16 lanes. +pub const QUALIA_I4_DIMS: usize = 16; + +/// Canonical labels for the i4-16D packed qualia vector, index-aligned with +/// [`QualiaI4_16D`]. Matches the first 16 entries of [`AXIS_LABELS`]; the +/// 17th ("integration") is omitted — recoverable on demand from valence + +/// coherence + cycle-delta if needed. +pub const QUALIA_I4_LABELS: [&str; QUALIA_I4_DIMS] = [ + "arousal", // 0 + "valence", // 1 + "tension", // 2 + "warmth", // 3 + "clarity", // 4 + "boundary", // 5 + "depth", // 6 + "velocity", // 7 + "entropy", // 8 + "coherence", // 9 + "intimacy", // 10 + "presence", // 11 + "assertion", // 12 + "receptivity", // 13 + "groundedness", // 14 + "expansion", // 15 +]; + +/// i4-16D signed packed qualia vector. 8 bytes / 16 dims / range −8..+7 per dim. +/// 9× compression vs `[f32; 18]` historical / `QualiaVector = [f32; 17]` canonical. +/// Per-dim semantics: see `QUALIA_I4_LABELS` (the canonical convergence-observable +/// vocab from `Qualia17D`/`QualiaVector`, with dim 16 "integration" dropped to fit +/// 16 lanes — recoverable on demand from valence + coherence + cycle-delta). +/// +/// Lane width: 32 i4 lanes per AVX-512 register; one `QualiaI4_16D` is half a lane group. +/// Magnitude is computed on demand: `coherence × valence → i8` (1 SIMD multiply per row). +#[repr(C, align(8))] +#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, Default)] +pub struct QualiaI4_16D(pub u64); + +impl QualiaI4_16D { + pub const ZERO: Self = Self(0); + + /// Read the signed i4 value at dim index 0..16. + /// Sign-extends 4-bit → i8 via arithmetic shift. + /// Out-of-range index returns 0 (defensive; bound check at call site preferred). + #[inline] + pub fn get(self, dim: usize) -> i8 { + if dim >= QUALIA_I4_DIMS { + return 0; + } + let raw = ((self.0 >> (dim * 4)) & 0xF) as i8; + (raw << 4) >> 4 // sign-extend 4 → 8 bits + } + + /// Set the signed i4 value at dim index. Clamps `value` to −8..+7. + /// Out-of-range index is a no-op (defensive). + #[inline] + pub fn set(&mut self, dim: usize, value: i8) { + if dim >= QUALIA_I4_DIMS { + return; + } + let v = value.clamp(-8, 7); + let nibble = (v as u8 & 0xF) as u64; + let mask = 0xFu64 << (dim * 4); + self.0 = (self.0 & !mask) | (nibble << (dim * 4)); + } + + /// Builder-shape variant. + #[inline] + pub fn with(self, dim: usize, value: i8) -> Self { + let mut copy = self; + copy.set(dim, value); + copy + } + + /// Convert a canonical 17D f32 qualia vector to packed i4-16D. + /// Each f32 dim in `[0.0, 1.0]` maps to i4 `[0, +7]` via `round(v * 7.0)`. + /// Each f32 dim in `[-1.0, 0.0)` maps to i4 `[-8, -1]` via `round(v * 8.0)`. + /// Dim 16 ("integration") from the 17D is DROPPED (recoverable on demand). + /// Out-of-range f32 values are clamped to [-1.0, 1.0] before quantization. + #[inline] + pub fn from_f32_17d(v: &QualiaVector) -> Self { + let mut out = Self(0); + for (dim, &f) in v.iter().take(QUALIA_I4_DIMS).enumerate() { + let clamped = f.clamp(-1.0, 1.0); + let i = if clamped >= 0.0 { + (clamped * 7.0).round() as i8 + } else { + (clamped * 8.0).round() as i8 + }; + out.set(dim, i); + } + out + } + + /// Convert a packed i4-16D back to a 17D f32 qualia vector. + /// Dim 16 ("integration") is zero-filled (the i4 lacks it; consumer should + /// recompute from valence + coherence if needed). + /// Reverse of `from_f32_17d`: positive i4 in [0, +7] → f32 in [0.0, 1.0]; + /// negative i4 in [-8, -1] → f32 in [-1.0, 0.0). + #[inline] + pub fn to_f32_17d(self) -> QualiaVector { + let mut out = [0.0f32; QUALIA_DIMS]; + for (dim, slot) in out.iter_mut().enumerate().take(QUALIA_I4_DIMS) { + let i = self.get(dim); + *slot = if i >= 0 { + i as f32 / 7.0 + } else { + i as f32 / 8.0 + }; + } + // dim 16 stays 0.0 (integration dropped) + out + } + + /// On-demand magnitude: `coherence × valence` as i8 (replaces the + /// historical [f32; 18] dim 13 "Magnitude" derived field). + /// Per plan L-4: coherence × valence (intensity × polarity). + /// One i8 saturating multiply (SIMD-friendly). + #[inline] + pub fn magnitude(self) -> i8 { + let coherence = self.get(9); // dim 9 in QUALIA_I4_LABELS + let valence = self.get(1); + coherence.saturating_mul(valence) + } +} + #[cfg(test)] mod tests { use super::*; @@ -182,4 +313,139 @@ mod tests { // wonder = sqrt(0.64 * 0.64) = 0.64 assert!((state[9] - 0.64).abs() < 1e-5); } + // ── QualiaI4_16D tests ────────────────────────────────────────────────── + + #[test] + fn test_qualia_i4_size_8b() { + use std::mem::size_of; + assert_eq!(size_of::(), 8); + } + + #[test] + fn test_qualia_i4_zero_default() { + let q = QualiaI4_16D::ZERO; + for dim in 0..QUALIA_I4_DIMS { + assert_eq!(q.get(dim), 0, "dim {} should be 0", dim); + } + } + + #[test] + fn test_qualia_i4_signed_roundtrip() { + let test_values: &[i8] = &[-8, -7, -1, 0, 1, 7]; + let test_dims: &[usize] = &[0, 1, 5, 10, 14, 15]; + for &val in test_values { + for &dim in test_dims { + let q = QualiaI4_16D::ZERO.with(dim, val); + assert_eq!( + q.get(dim), + val, + "roundtrip failed for val={} dim={}", + val, + dim + ); + } + } + } + + #[test] + fn test_qualia_i4_clamp() { + let mut q = QualiaI4_16D::ZERO; + q.set(3, 100); + assert_eq!(q.get(3), 7, "positive overflow should clamp to +7"); + q.set(3, -100); + assert_eq!(q.get(3), -8, "negative overflow should clamp to -8"); + } + + #[test] + fn test_qualia_i4_isolation() { + // Setting dim 5 to 7 must not affect adjacent dims 4 and 6 + let q = QualiaI4_16D::ZERO.with(5, 7); + assert_eq!(q.get(4), 0, "dim 4 must remain 0 after setting dim 5"); + assert_eq!(q.get(5), 7, "dim 5 must be 7"); + assert_eq!(q.get(6), 0, "dim 6 must remain 0 after setting dim 5"); + } + + #[test] + fn test_qualia_i4_from_f32_17d_roundtrip() { + // Build a representative 17D vector with varied values + let mut v: QualiaVector = [0.0; QUALIA_DIMS]; + v[0] = 0.8; // arousal + v[1] = 0.5; // valence + v[2] = 0.1; // tension + v[3] = 1.0; // warmth + v[4] = 0.0; // clarity + v[5] = 0.3; // boundary + v[6] = 0.6; // depth + v[7] = 0.7; // velocity + v[8] = 0.2; // entropy + v[9] = 0.9; // coherence + v[10] = 0.4; // intimacy + v[11] = 0.55; // presence + v[12] = 0.15; // assertion + v[13] = 0.85; // receptivity + v[14] = 0.45; // groundedness + v[15] = 0.65; // expansion + v[16] = 0.75; // integration — should be DROPPED + + let packed = QualiaI4_16D::from_f32_17d(&v); + let restored = packed.to_f32_17d(); + + // dim 16 must be zero in the round-trip output + assert_eq!( + restored[16], 0.0, + "dim 16 (integration) must be zero after round-trip" + ); + + // All other dims must be within quantization error + // Positive path: max error = 1/7 ≈ 0.143; negative path: max error = 1/8 = 0.125 + let epsilon = 1.0f32 / 7.0 + 1e-5; + for dim in 0..QUALIA_I4_DIMS { + let err = (restored[dim] - v[dim]).abs(); + assert!( + err <= epsilon, + "dim {} round-trip error {} exceeds epsilon {} (original={}, restored={})", + dim, + err, + epsilon, + v[dim], + restored[dim] + ); + } + } + + #[test] + fn test_qualia_i4_label_alignment() { + // All 16 i4 labels must match the first 16 canonical AXIS_LABELS + for i in 0..QUALIA_I4_DIMS { + assert_eq!( + QUALIA_I4_LABELS[i], AXIS_LABELS[i], + "label mismatch at index {}: i4='{}' axis='{}'", + i, QUALIA_I4_LABELS[i], AXIS_LABELS[i] + ); + } + } + + #[test] + fn test_qualia_i4_magnitude() { + // magnitude = coherence (dim 9) × valence (dim 1), saturating_mul + // Known values: coherence=3, valence=2 → 6 + let q = QualiaI4_16D::ZERO.with(9, 3).with(1, 2); + assert_eq!(q.magnitude(), 6); + + // Negative × positive: coherence=-4, valence=2 → -8 + let q2 = QualiaI4_16D::ZERO.with(9, -4).with(1, 2); + assert_eq!(q2.magnitude(), -8); + + // Saturation: coherence=7, valence=7 → saturating_mul → 49, clamped to i8::MAX=127 + let q3 = QualiaI4_16D::ZERO.with(9, 7).with(1, 7); + assert_eq!(q3.magnitude(), 7i8.saturating_mul(7)); + + // Extremes: coherence=-8, valence=-8 → saturating_mul(-8,-8)=64 (fits in i8) + let q4 = QualiaI4_16D::ZERO.with(9, -8).with(1, -8); + assert_eq!(q4.magnitude(), (-8i8).saturating_mul(-8)); + + // Zero magnitude when either is zero + let q5 = QualiaI4_16D::ZERO; + assert_eq!(q5.magnitude(), 0); + } }