Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions .claude/board/AGENT_LOG.md
Original file line number Diff line number Diff line change
@@ -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).
Expand Down
8 changes: 4 additions & 4 deletions .claude/board/STATUS_BOARD.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
4 changes: 4 additions & 0 deletions crates/lance-graph-contract/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
266 changes: 266 additions & 0 deletions crates/lance-graph-contract/src/qualia.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::*;
Expand Down Expand Up @@ -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::<QualiaI4_16D>(), 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);
}
}
Loading