From 148315431c0047c1f03fc5fc0b32de138921e41d Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 8 Oct 2026 09:17:50 +0000 Subject: [PATCH 1/5] contract: signed Register128 carvings (32xi4, 16xi8) with a per-family law Two new slab readings, RegisterI4x32 = 2 and RegisterI8x16 = 3, over the existing Register0/Register1 rails. The slab declares the carving; the binder declares the law (RelativeOffset, AxisPosition, Support) at ResolvedReading::bind_signed_register, once per population. Every read and write of SignedRegisterLanes checks rail, carving and law, and a refused write leaves the row unchanged. Out-of-range i4 values are refused, never saturated. bind_register128 (the unsigned 4xu32 words reading) is unchanged and refuses the signed carvings; bind_signed_register refuses Register128, Facet96 and undeclared slabs. EpistemicState5 has no law here: it stays on CE64 bits 59..63. I4x32 gains from_bytes/as_bytes so the i4 carving reuses its nibble layout. No layout change: the rails, NodeRow and ENVELOPE_LAYOUT_VERSION are unchanged. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01EFw2WdKr1oxvaKCJC2ua2R --- crates/lance-graph-contract/src/atoms.rs | 13 + crates/lance-graph-contract/src/hotplug.rs | 154 +++++- .../lance-graph-contract/src/register128.rs | 467 ++++++++++++++++++ 3 files changed, 630 insertions(+), 4 deletions(-) diff --git a/crates/lance-graph-contract/src/atoms.rs b/crates/lance-graph-contract/src/atoms.rs index 285ee4059..6799d8e96 100644 --- a/crates/lance-graph-contract/src/atoms.rs +++ b/crates/lance-graph-contract/src/atoms.rs @@ -79,6 +79,19 @@ impl I4x32 { /// The all-zero style vector (every lane neutral). pub const ZERO: Self = Self { bytes: [0u8; 16] }; + /// Wrap 16 already-packed bytes. No conversion: dim `2k` is the low + /// nibble of byte `k`, dim `2k+1` the high nibble. + #[must_use] + pub const fn from_bytes(bytes: [u8; 16]) -> Self { + Self { bytes } + } + + /// The 16 packed bytes, in the layout [`from_bytes`](Self::from_bytes) takes. + #[must_use] + pub const fn as_bytes(&self) -> &[u8; 16] { + &self.bytes + } + /// Pack 32 signed dims into the i4 CAM carrier, saturating to `[−8, 7]`. /// /// The carrier is a **sparse, deterministic 32×CAM address** (128-bit) — the non-zero diff --git a/crates/lance-graph-contract/src/hotplug.rs b/crates/lance-graph-contract/src/hotplug.rs index 2f649e162..4e615bf5b 100644 --- a/crates/lance-graph-contract/src/hotplug.rs +++ b/crates/lance-graph-contract/src/hotplug.rs @@ -188,9 +188,9 @@ impl Activation { /// A physical reading a slab declares for itself. /// -/// The variants are exactly the readings THIS build implements. A future -/// reading (a classid-free 128-bit register and its carvings) lands as a new -/// variant; which slabs use it is decided by the slab's own declaration, never +/// The variants are exactly the readings THIS build implements. A new +/// reading (as the classid-free 128-bit register and its signed carvings +/// did) lands as a new variant; which slabs use it is decided by the slab's own declaration, never /// by a per-concept table. The on-wire tag is a `u8` in the slab's metadata /// envelope, decoded by [`SlabReading::from_tag`], which refuses any tag this /// build does not implement. @@ -211,6 +211,15 @@ pub enum SlabReading { /// [`ResolvedReading::bind_register128`] requires before granting the /// register rails. Register128 = 1, + /// The same register rails, written as 32 signed `i4` values: dim `2k` + /// is the low nibble of byte `k`, dim `2k+1` the high nibble (the + /// [`crate::atoms::I4x32`] layout). Bound by + /// [`ResolvedReading::bind_signed_register`], never by + /// [`ResolvedReading::bind_register128`]. + RegisterI4x32 = 2, + /// The same register rails, written as 16 signed `i8` values, one per + /// byte. Bound by [`ResolvedReading::bind_signed_register`]. + RegisterI8x16 = 3, } impl SlabReading { @@ -224,6 +233,8 @@ impl SlabReading { match tag { 0 => Ok(SlabReading::Facet96), 1 => Ok(SlabReading::Register128), + 2 => Ok(SlabReading::RegisterI4x32), + 3 => Ok(SlabReading::RegisterI8x16), other => Err(ActivationDrift::UnknownSlabReading(other)), } } @@ -369,6 +380,56 @@ impl ResolvedReading { } Ok(crate::register128::RegisterLanes::new(self.concept, rails)) } + + /// Bind the register rails of this population as SIGNED values under + /// one declared law. Checked ONCE per population, like + /// [`bind_register128`](Self::bind_register128). + /// + /// The slab declares the carving (how the bytes were written: + /// [`SlabReading::RegisterI4x32`] or [`SlabReading::RegisterI8x16`]). + /// The caller declares the `law` (what the signed values mean for this + /// concept: a relative offset, a position on an axis, or support). Both + /// travel with the returned lanes, and every read and write checks the + /// law it is asked for against the bound one, so a consumer of one law + /// can never silently read another's values. + /// + /// # Errors + /// + /// - [`ActivationDrift::NotSignedRegister`] when the slab declared any + /// other reading, including the unsigned [`SlabReading::Register128`], + /// or nothing. + /// - [`ActivationDrift::RegisterRailAbsent`] when the value schema does + /// not materialise a requested rail. + pub fn bind_signed_register( + &self, + rails: crate::register128::RegisterRails, + law: crate::register128::RegisterLaw, + ) -> Result { + let carving = match self.slab { + Some(SlabReading::RegisterI4x32) => crate::register128::RegisterCarving::I4x32, + Some(SlabReading::RegisterI8x16) => crate::register128::RegisterCarving::I8x16, + other => { + return Err(ActivationDrift::NotSignedRegister { + concept: self.concept, + slab: other, + }) + } + }; + for &tenant in rails.tenants() { + if !self.read_mode.value_schema.has(tenant) { + return Err(ActivationDrift::RegisterRailAbsent { + concept: self.concept, + tenant: tenant as u8, + }); + } + } + Ok(crate::register128::SignedRegisterLanes::new( + self.concept, + rails, + carving, + law, + )) + } } /// Why an activation failed — each arm is one named bang. @@ -407,6 +468,16 @@ pub enum ActivationDrift { /// What the slab declared instead (`None`: nothing). slab: Option, }, + /// [`ResolvedReading::bind_signed_register`] was asked for signed + /// register rails of a population whose slab did not declare a signed + /// carving ([`SlabReading::RegisterI4x32`] or + /// [`SlabReading::RegisterI8x16`]). + NotSignedRegister { + /// The concept the reading was resolved under. + concept: u16, + /// What the slab declared instead (`None`: nothing). + slab: Option, + }, /// The resolved value schema does not materialise a requested register /// rail. RegisterRailAbsent { @@ -499,6 +570,11 @@ impl core::fmt::Display for ActivationDrift { "concept 0x{concept:04X}: slab declares {slab:?}, not Register128; \ no register rails are granted" ), + Self::NotSignedRegister { concept, slab } => write!( + f, + "concept 0x{concept:04X}: slab declares {slab:?}, not a signed register \ + carving; no signed register rails are granted" + ), Self::RegisterRailAbsent { concept, tenant } => write!( f, "concept 0x{concept:04X}: value schema does not materialise register tenant {tenant}" @@ -749,7 +825,9 @@ mod tests { fn an_unsupported_physical_reading_fails_closed() { assert_eq!(SlabReading::from_tag(0), Ok(SlabReading::Facet96)); assert_eq!(SlabReading::from_tag(1), Ok(SlabReading::Register128)); - for tag in [2u8, 3, 0x80, 0xFF] { + assert_eq!(SlabReading::from_tag(2), Ok(SlabReading::RegisterI4x32)); + assert_eq!(SlabReading::from_tag(3), Ok(SlabReading::RegisterI8x16)); + for tag in [4u8, 5, 0x80, 0xFF] { assert_eq!( SlabReading::from_tag(tag), Err(ActivationDrift::UnknownSlabReading(tag)) @@ -937,6 +1015,74 @@ mod tests { ); } + /// FAILS IF: signed rails are granted to a slab without a signed + /// carving (including the unsigned Register128 slab), the carving is + /// taken from anywhere but the slab, or a rail the schema lacks is + /// granted. + #[test] + fn signed_rails_are_granted_only_to_a_signed_carving() { + use crate::canonical_node::ValueTenant; + use crate::register128::{RegisterCarving, RegisterLaw, RegisterRails}; + let a = act(); + let carved = |reading, value_schema| SlabDeclaration { + reading, + value_schema, + layout_version: ENVELOPE_LAYOUT_VERSION, + }; + for (reading, carving) in [ + (SlabReading::RegisterI4x32, RegisterCarving::I4x32), + (SlabReading::RegisterI8x16, RegisterCarving::I8x16), + ] { + let r = a + .resolve_for_context(0x0901, Some(&carved(reading, ValueSchema::Full))) + .unwrap(); + let lanes = r + .bind_signed_register(RegisterRails::Two, RegisterLaw::AxisPosition) + .unwrap(); + assert_eq!(lanes.carving(), carving); + assert_eq!(lanes.law(), RegisterLaw::AxisPosition); + assert_eq!(lanes.concept(), 0x0901); + assert_eq!( + r.bind_register128(RegisterRails::One), + Err(ActivationDrift::NotRegister128 { + concept: 0x0901, + slab: Some(reading) + }), + "a signed slab is not an unsigned word register" + ); + let narrow = a + .resolve_for_context(0x0901, Some(&carved(reading, ValueSchema::Cognitive))) + .unwrap(); + assert_eq!( + narrow.bind_signed_register(RegisterRails::One, RegisterLaw::Support), + Err(ActivationDrift::RegisterRailAbsent { + concept: 0x0901, + tenant: ValueTenant::Register0 as u8, + }) + ); + } + let words = a + .resolve_for_context(0x0901, Some(®_decl(ValueSchema::Full))) + .unwrap(); + let facet = a + .resolve_for_context(0x0901, Some(&decl(ValueSchema::Full))) + .unwrap(); + let undeclared = a.resolve_for_context(0x0901, None).unwrap(); + for (r, slab) in [ + (words, Some(SlabReading::Register128)), + (facet, Some(SlabReading::Facet96)), + (undeclared, None), + ] { + assert_eq!( + r.bind_signed_register(RegisterRails::One, RegisterLaw::RelativeOffset), + Err(ActivationDrift::NotSignedRegister { + concept: 0x0901, + slab + }) + ); + } + } + /// FAILS IF: a register's semantic identity is read from its bytes. /// /// The same register payload — here deliberately holding the OTHER diff --git a/crates/lance-graph-contract/src/register128.rs b/crates/lance-graph-contract/src/register128.rs index 007197646..18d9e134c 100644 --- a/crates/lance-graph-contract/src/register128.rs +++ b/crates/lance-graph-contract/src/register128.rs @@ -181,8 +181,475 @@ impl RegisterLanes { } } +/// How the bytes of a signed register were written. Declared by the slab +/// ([`SlabReading::RegisterI4x32`](crate::hotplug::SlabReading::RegisterI4x32), +/// [`SlabReading::RegisterI8x16`](crate::hotplug::SlabReading::RegisterI8x16)), +/// never chosen by a reader. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum RegisterCarving { + /// 32 signed `i4`, range `-8..=7`. Dim `2k` is the low nibble of byte + /// `k`, dim `2k+1` the high nibble ([`crate::atoms::I4x32`]). + I4x32, + /// 16 signed `i8`, range `-128..=127`, dim `k` in byte `k`. + I8x16, +} + +impl RegisterCarving { + /// Number of dimensions in one register. + #[must_use] + pub const fn dims(self) -> usize { + match self { + Self::I4x32 => 32, + Self::I8x16 => 16, + } + } +} + +/// What the signed values of a bound population mean. Declared once, at +/// binding, for the concept the population was resolved under; checked on +/// every read and write. +/// +/// Each law is a different kind of quantity, so a value written under one +/// is not a value under another even when the bytes are identical. That is +/// the mixing an earlier 24×i4 accumulation got wrong, and why no law here +/// converts into another. +/// +/// Epistemic state is deliberately absent: it is defined on `CausalEdge64` +/// bits 59..63 (`EpistemicState5`) and is not re-encoded as a signed value. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum RegisterLaw { + /// A relative address in a local window: `-3` means "three positions + /// back". An address, not a strength. A referent outside the window is + /// not encoded here; it is reached through a basin or graph edge. + RelativeOffset, + /// A signed position on a declared semantic axis (e.g. `terrestrial`: + /// fox `+7`, whale `-5`). Independent of the `is_a` taxonomy: a negative + /// position never removes a class membership. + AxisPosition, + /// Evidence stance: positive supports, zero is unresolved, negative + /// falsifies. + Support, +} + +/// Why a signed register read or write was refused. A refused write leaves +/// the row unchanged. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum SignedRegisterRefusal { + /// The binding did not grant this rail. + RailAbsent { + /// The rail asked for. + rail: usize, + }, + /// The caller asked for a different carving than the slab declared. + CarvingMismatch { + /// The carving the slab declared. + bound: RegisterCarving, + /// The carving the caller asked for. + asked: RegisterCarving, + }, + /// The caller asked for a different law than the binding declared. + LawMismatch { + /// The law the population was bound under. + bound: RegisterLaw, + /// The law the caller asked for. + asked: RegisterLaw, + }, + /// A value does not fit the carving (an `i4` outside `-8..=7`). + OutOfRange { + /// The dimension holding it. + dim: usize, + /// The value. + value: i8, + }, +} + +/// The signed register rails of one bound population, produced ONCE by +/// [`ResolvedReading::bind_signed_register`](crate::hotplug::ResolvedReading::bind_signed_register). +/// +/// Carries the concept, the granted rails, the slab's carving and the +/// declared law. Reads decode one rail into an owned array of 16 or 32 +/// values; nothing else is copied. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct SignedRegisterLanes { + concept: u16, + rails: RegisterRails, + carving: RegisterCarving, + law: RegisterLaw, +} + +impl SignedRegisterLanes { + /// Built only by the binding, which has already checked the slab. + pub(crate) const fn new( + concept: u16, + rails: RegisterRails, + carving: RegisterCarving, + law: RegisterLaw, + ) -> Self { + Self { + concept, + rails, + carving, + law, + } + } + + /// The SPOG concept this population was resolved under. + #[must_use] + pub const fn concept(&self) -> u16 { + self.concept + } + + /// The rails this binding granted. + #[must_use] + pub const fn rails(&self) -> RegisterRails { + self.rails + } + + /// The carving the slab declared. + #[must_use] + pub const fn carving(&self) -> RegisterCarving { + self.carving + } + + /// The law this population was bound under. + #[must_use] + pub const fn law(&self) -> RegisterLaw { + self.law + } + + /// The value-slab byte range of `rail`, after checking rail, carving and + /// law in that order. + fn check( + &self, + rail: usize, + carving: RegisterCarving, + law: RegisterLaw, + ) -> Result, SignedRegisterRefusal> { + let tenants = self.rails.tenants(); + if rail >= tenants.len() { + return Err(SignedRegisterRefusal::RailAbsent { rail }); + } + if carving != self.carving { + return Err(SignedRegisterRefusal::CarvingMismatch { + bound: self.carving, + asked: carving, + }); + } + if law != self.law { + return Err(SignedRegisterRefusal::LawMismatch { + bound: self.law, + asked: law, + }); + } + let start = tenants[rail].value_offset(); + Ok(start..start + REGISTER_BYTES) + } + + /// Read `rail` as 32 signed `i4` under `law`. + /// + /// # Errors + /// + /// [`SignedRegisterRefusal`] when the rail is absent, the slab is not + /// carved as [`RegisterCarving::I4x32`], or `law` is not the bound law. + pub fn read_i4x32( + &self, + row: &NodeRow, + rail: usize, + law: RegisterLaw, + ) -> Result<[i8; 32], SignedRegisterRefusal> { + let r = self.check(rail, RegisterCarving::I4x32, law)?; + let mut b = [0u8; REGISTER_BYTES]; + b.copy_from_slice(&row.value[r]); + Ok(crate::atoms::I4x32::from_bytes(b).unpack()) + } + + /// Read `rail` as 16 signed `i8` under `law`. + /// + /// # Errors + /// + /// As [`read_i4x32`](Self::read_i4x32), for [`RegisterCarving::I8x16`]. + pub fn read_i8x16( + &self, + row: &NodeRow, + rail: usize, + law: RegisterLaw, + ) -> Result<[i8; 16], SignedRegisterRefusal> { + let r = self.check(rail, RegisterCarving::I8x16, law)?; + let bytes = &row.value[r]; + Ok(core::array::from_fn(|i| bytes[i] as i8)) + } + + /// Write `rail` as 32 signed `i4` under `law`. Every value must lie in + /// `-8..=7`; nothing is saturated, and nothing is written unless all + /// checks pass. + /// + /// # Errors + /// + /// As [`read_i4x32`](Self::read_i4x32), plus + /// [`SignedRegisterRefusal::OutOfRange`]. + pub fn write_i4x32( + &self, + row: &mut NodeRow, + rail: usize, + law: RegisterLaw, + values: &[i8; 32], + ) -> Result<(), SignedRegisterRefusal> { + let r = self.check(rail, RegisterCarving::I4x32, law)?; + if let Some((dim, &value)) = values + .iter() + .enumerate() + .find(|(_, v)| !(-8..=7).contains(*v)) + { + return Err(SignedRegisterRefusal::OutOfRange { dim, value }); + } + let packed = crate::atoms::I4x32::pack(values); + row.value[r].copy_from_slice(packed.as_bytes()); + crate::tenant_counter::tenant_update(self.rails.tenants()[rail]); + Ok(()) + } + + /// Write `rail` as 16 signed `i8` under `law`. + /// + /// # Errors + /// + /// As [`read_i8x16`](Self::read_i8x16). Nothing is written on refusal. + pub fn write_i8x16( + &self, + row: &mut NodeRow, + rail: usize, + law: RegisterLaw, + values: &[i8; 16], + ) -> Result<(), SignedRegisterRefusal> { + let r = self.check(rail, RegisterCarving::I8x16, law)?; + for (dst, &v) in row.value[r].iter_mut().zip(values.iter()) { + *dst = v as u8; + } + crate::tenant_counter::tenant_update(self.rails.tenants()[rail]); + Ok(()) + } +} + #[cfg(test)] mod tests { + fn patterned_row() -> NodeRow { + NodeRow { + key: crate::canonical_node::NodeGuid::new(0x0901_0000, 1, 2, 3, 0x66, 11), + edges: Default::default(), + value: core::array::from_fn(|i| (i % 251) as u8), + } + } + + const LAWS: [RegisterLaw; 3] = [ + RegisterLaw::RelativeOffset, + RegisterLaw::AxisPosition, + RegisterLaw::Support, + ]; + + /// FAILS IF: an i4 value does not round-trip, or the nibble layout + /// differs from `atoms::I4x32` (dim 2k low nibble of byte k). + #[test] + fn i4x32_round_trips_every_value_in_every_dim() { + let lanes = SignedRegisterLanes::new( + 0x0901, + RegisterRails::Two, + RegisterCarving::I4x32, + RegisterLaw::AxisPosition, + ); + for v in -8i8..=7 { + for dim in 0..32 { + let mut values = [0i8; 32]; + values[dim] = v; + values[(dim + 1) % 32] = -v.clamp(-7, 7); + let mut row = patterned_row(); + lanes + .write_i4x32(&mut row, 1, RegisterLaw::AxisPosition, &values) + .unwrap(); + assert_eq!( + lanes.read_i4x32(&row, 1, RegisterLaw::AxisPosition), + Ok(values) + ); + let r = lanes + .check(1, RegisterCarving::I4x32, RegisterLaw::AxisPosition) + .unwrap(); + let byte = row.value[r.start + dim / 2]; + let nibble = if dim % 2 == 0 { byte & 0xF } else { byte >> 4 }; + assert_eq!(nibble, (v as u8) & 0xF, "dim {dim} nibble"); + } + } + } + + /// FAILS IF: an i8 value does not round-trip, or dim k is not byte k. + #[test] + fn i8x16_round_trips_and_dim_k_is_byte_k() { + let lanes = SignedRegisterLanes::new( + 0x0901, + RegisterRails::Two, + RegisterCarving::I8x16, + RegisterLaw::RelativeOffset, + ); + for base in [-128i8, -100, -1, 0, 1, 77, 127] { + let values: [i8; 16] = + core::array::from_fn(|k| base.wrapping_add((k as i8).wrapping_mul(17))); + let mut row = patterned_row(); + lanes + .write_i8x16(&mut row, 1, RegisterLaw::RelativeOffset, &values) + .unwrap(); + assert_eq!( + lanes.read_i8x16(&row, 1, RegisterLaw::RelativeOffset), + Ok(values) + ); + let r = lanes.rails.tenants()[1].value_offset(); + for (k, &v) in values.iter().enumerate() { + assert_eq!(row.value[r + k] as i8, v); + } + } + } + + /// FAILS IF: the carving is not load-bearing: the same 16 bytes must + /// mean different values under the two carvings. + #[test] + fn the_same_bytes_differ_under_the_two_carvings() { + let mut row = patterned_row(); + let i8s = SignedRegisterLanes::new( + 0x0901, + RegisterRails::Two, + RegisterCarving::I8x16, + RegisterLaw::Support, + ); + let i4s = SignedRegisterLanes { + carving: RegisterCarving::I4x32, + ..i8s + }; + let mut v8 = [0i8; 16]; + v8[0] = 0xF1u8 as i8; // -15 as i8; nibbles (+1, -1) as i4 + i8s.write_i8x16(&mut row, 1, RegisterLaw::Support, &v8) + .unwrap(); + let as4 = i4s.read_i4x32(&row, 1, RegisterLaw::Support).unwrap(); + assert_eq!(v8[0], -15); + assert_eq!((as4[0], as4[1]), (1, -1)); + } + + /// FAILS IF: a read or write under a law other than the bound one is + /// accepted, or a refused write changes the row. + #[test] + fn a_different_law_is_refused_and_writes_nothing() { + for bound in LAWS { + for asked in LAWS { + for carving in [RegisterCarving::I4x32, RegisterCarving::I8x16] { + let lanes = + SignedRegisterLanes::new(0x0901, RegisterRails::Two, carving, bound); + let mut row = patterned_row(); + let before = row.value; + let (read_ok, write) = match carving { + RegisterCarving::I4x32 => ( + lanes.read_i4x32(&row, 1, asked).is_ok(), + lanes.write_i4x32(&mut row, 1, asked, &[3; 32]), + ), + RegisterCarving::I8x16 => ( + lanes.read_i8x16(&row, 1, asked).is_ok(), + lanes.write_i8x16(&mut row, 1, asked, &[3; 16]), + ), + }; + if asked == bound { + assert!(read_ok && write.is_ok()); + } else { + assert!(!read_ok); + assert_eq!( + write, + Err(SignedRegisterRefusal::LawMismatch { bound, asked }) + ); + assert_eq!(row.value, before, "a refused write wrote"); + } + } + } + } + } + + /// FAILS IF: a reader can choose a carving other than the slab's. + #[test] + fn a_different_carving_is_refused() { + let lanes = SignedRegisterLanes::new( + 0x0901, + RegisterRails::Two, + RegisterCarving::I4x32, + RegisterLaw::Support, + ); + let mut row = patterned_row(); + let before = row.value; + let refused = Err(SignedRegisterRefusal::CarvingMismatch { + bound: RegisterCarving::I4x32, + asked: RegisterCarving::I8x16, + }); + assert_eq!( + lanes.read_i8x16(&row, 1, RegisterLaw::Support).map(|_| ()), + refused + ); + assert_eq!( + lanes.write_i8x16(&mut row, 1, RegisterLaw::Support, &[1; 16]), + refused + ); + assert_eq!(row.value, before); + } + + /// FAILS IF: an out-of-range i4 is saturated or partly written instead + /// of refused. The bad value is in the LAST dim so a write that checks + /// as it goes would already have written the others. + #[test] + fn an_out_of_range_i4_is_refused_before_writing() { + let lanes = SignedRegisterLanes::new( + 0x0901, + RegisterRails::Two, + RegisterCarving::I4x32, + RegisterLaw::AxisPosition, + ); + for bad in [8i8, 9, 127, -9, -128] { + let mut values = [5i8; 32]; + values[31] = bad; + let mut row = patterned_row(); + let before = row.value; + assert_eq!( + lanes.write_i4x32(&mut row, 1, RegisterLaw::AxisPosition, &values), + Err(SignedRegisterRefusal::OutOfRange { + dim: 31, + value: bad + }) + ); + assert_eq!(row.value, before); + } + } + + /// FAILS IF: a one-rail binding reaches rail 1, or a write touches any + /// byte outside its rail. + #[test] + fn signed_writes_stay_inside_their_rail() { + let one = SignedRegisterLanes::new( + 0x0901, + RegisterRails::One, + RegisterCarving::I8x16, + RegisterLaw::Support, + ); + let mut row = patterned_row(); + assert_eq!( + one.write_i8x16(&mut row, 1, RegisterLaw::Support, &[1; 16]), + Err(SignedRegisterRefusal::RailAbsent { rail: 1 }) + ); + let two = SignedRegisterLanes { + rails: RegisterRails::Two, + ..one + }; + let before = row.value; + two.write_i8x16(&mut row, 1, RegisterLaw::Support, &[-1; 16]) + .unwrap(); + let r = two + .check(1, RegisterCarving::I8x16, RegisterLaw::Support) + .unwrap(); + for (i, (a, b)) in before.iter().zip(row.value.iter()).enumerate() { + if !r.contains(&i) { + assert_eq!(a, b, "byte {i} outside rail 1 changed"); + } + } + } + use super::*; use crate::canonical_node::VALUE_TENANTS; From b98d457304240b18051823e978ef7a3e0c5e792c Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 8 Oct 2026 09:19:13 +0000 Subject: [PATCH 2/5] board: Register128 signed readings entry and 24xi4 family inventory Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01EFw2WdKr1oxvaKCJC2ua2R --- .claude/board/LATEST_STATE.md | 4 ++ .../2026-10-08-register128-signed-readings.md | 68 +++++++++++++++++++ .claude/board/entries/README.md | 3 +- .claude/v3/soa_layout/tenants.md | 2 +- 4 files changed, 75 insertions(+), 2 deletions(-) create mode 100644 .claude/board/entries/2026-10-08-register128-signed-readings.md diff --git a/.claude/board/LATEST_STATE.md b/.claude/board/LATEST_STATE.md index 5d1b5eedc..ddce1f77d 100644 --- a/.claude/board/LATEST_STATE.md +++ b/.claude/board/LATEST_STATE.md @@ -1,3 +1,7 @@ +## 2026-10-08 — Register128 signed readings on branch `ccr-b2e415d9-4jfvyk-register-readings` (unmerged) + +- Contract delta (additive): `hotplug::SlabReading::{RegisterI4x32 = 2, RegisterI8x16 = 3}`, `ResolvedReading::bind_signed_register`, `ActivationDrift::NotSignedRegister`; `register128::{RegisterCarving, RegisterLaw, SignedRegisterLanes, SignedRegisterRefusal}`; `atoms::I4x32::{from_bytes, as_bytes}`. No layout change. Entry: `entries/2026-10-08-register128-signed-readings.md`. + ## 2026-10-08 — HHTL / NARS / Moore value tenants on branch `ccr-b2e415d9-4jfvyk-tenants` (unmerged) - Contract delta (additive): `ValueTenant::{Nars16x8 = 18, MoorePalettePairs = 19, MooreNars16 = 20}` at row [284,332), in `ValueSchema::Full` only; new module `contract::moore_tenant::{MooreSlot, MooreNars16, DirectionReading, DirectionRefusal, MooreRefusal, MooreTenantView, MooreTenantMut}`. BoardAggregates reservation re-based to 21. Entry: `entries/2026-10-08-hhtl-nars-moore-value-tenants.md`. diff --git a/.claude/board/entries/2026-10-08-register128-signed-readings.md b/.claude/board/entries/2026-10-08-register128-signed-readings.md new file mode 100644 index 000000000..6cee05972 --- /dev/null +++ b/.claude/board/entries/2026-10-08-register128-signed-readings.md @@ -0,0 +1,68 @@ +# 2026-10-08 — Register128 signed readings (32×i4, 16×i8) with a per-family law + +**Status:** CURRENT-CONTRACT, TEST-PINNED. Branch +`ccr-b2e415d9-4jfvyk-register-readings`, commit `14831543`. + +## What landed + +The carrier is unchanged: the `Register0` / `Register1` rails (16 B each), no +classid in the payload, no layout change. + +- **Carving, declared by the slab.** `SlabReading::RegisterI4x32 = 2` (32 × i4, + dim `2k` = low nibble of byte `k`, the `atoms::I4x32` layout) and + `SlabReading::RegisterI8x16 = 3` (16 × i8, dim `k` = byte `k`). +- **Law, declared at binding.** `RegisterLaw::{RelativeOffset, AxisPosition, + Support}`, passed to `ResolvedReading::bind_signed_register(rails, law)` once + per population. The returned `SignedRegisterLanes` carry concept, rails, + carving and law. +- **Checked on every access.** `read_i4x32` / `read_i8x16` / `write_i4x32` / + `write_i8x16` take the law the caller expects and refuse, in order, an absent + rail, a different carving, a different law. An i4 outside `-8..=7` is refused, + never saturated. A refused write leaves the row unchanged. +- **Separation from the unsigned reading.** `bind_register128` (4 × u32 words) + is unchanged and refuses the signed carvings; `bind_signed_register` refuses + `Register128`, `Facet96` and undeclared slabs. +- **EpistemicState5 has no law here.** It stays on CE64 bits 59..63. + +## Inventory of the 24×i4 families on `main` (2026-10-08) + +| family | on `main` | law it would bind | +|---|---|---| +| Temporal / anaphora | `CausalWitness` tenant 14, Facet96 read as G24N4 (24 i4 window pointers). EXPERIMENTAL. The only resident 24×i4 lane. | `RelativeOffset` | +| Evidence (Tarski) | Probe only: `probe_four_plane_causal_medium.rs` (G24N4 as support/falsifier). | `Support` | +| Taxonomy axes | Fixture only: `entries/2026-10-04-coresearch-evidence-stance-dependence.md` (Mammal: `terrestrial`, `placental`). | `AxisPosition` | +| Episodic basin | `EpisodicBasin` tenant 15: 32 B of references, not i4. | none (not a signed family) | +| 32×i4 carrier | `atoms::I4x32`; now also the i4 carving's layout. | — | +| 16×i8 | `ndarray` SIMD `I8x16` only. | — | + +`CausalWitness` is NOT moved onto a register by this change: it is a Facet96 +lane with its own classid, and re-homing it is a separate decision. + +## Tests and disable runs + +Hotplug: `signed_rails_are_granted_only_to_a_signed_carving`; tag test now +accepts 2 and 3. Register128: round trip of every i4 value in every dim with the +nibble layout pinned; i8 dim-k-is-byte-k; the same bytes differ under the two +carvings; law mismatch (all 3×3 pairs, both carvings) refused and writes +nothing; carving mismatch refused; out-of-range i4 in the last dim refused +before writing; writes stay inside their rail. + +| disable | caught by | +|---|---| +| law check removed | `a_different_law_is_refused_and_writes_nothing` | +| carving check removed | `a_different_carving_is_refused` | +| rail check removed | `signed_writes_stay_inside_their_rail` | +| i4 range check removed | `an_out_of_range_i4_is_refused_before_writing` | +| binding accepts `Register128` as i4 | `signed_rails_are_granted_only_to_a_signed_carving` | +| binding swaps the carvings | same | +| binding skips the schema check | same | + +## OPEN + +- The law is declared by the binder. There is no OGAR table mapping a concept + to its law yet, so a binder can still declare the wrong one; the contract + only guarantees readers cannot silently read another law's values. +- Lane or reading per family is undecided until measured: 32×i4 vs 16×i8 on + KJV anaphora and the mammal fixture. The rule "outside the local window → + basin edge" is not changed by `RelativeOffset` under `I8x16`. +- No production writer or reader. diff --git a/.claude/board/entries/README.md b/.claude/board/entries/README.md index b3c7f5359..d1a137647 100644 --- a/.claude/board/entries/README.md +++ b/.claude/board/entries/README.md @@ -25,10 +25,11 @@ index row, (3) no duplicate entry id. Checks 1 and 2 are deliberately opposite directions; the stranding this convention prevents shows up in exactly one of them, never both. -269 entries, 2026-08-06 .. 2026-10-08. +271 entries, 2026-08-06 .. 2026-10-08. | date | entry id | finding | file | |---|---|---|---| +| 2026-10-08 | `register128-signed-readings` | | [2026-10-08-register128-signed-readings.md](2026-10-08-register128-signed-readings.md) | | 2026-10-08 | `rbac-nested-scope-path` | | [2026-10-08-rbac-nested-scope-path.md](2026-10-08-rbac-nested-scope-path.md) | | 2026-10-08 | `rbac-membership-scope` | | [2026-10-08-rbac-membership-scope.md](2026-10-08-rbac-membership-scope.md) | | 2026-10-08 | `rbac-hotplug-socket` | | [2026-10-08-rbac-hotplug-socket.md](2026-10-08-rbac-hotplug-socket.md) | diff --git a/.claude/v3/soa_layout/tenants.md b/.claude/v3/soa_layout/tenants.md index 080661fa7..15f0d2c0f 100644 --- a/.claude/v3/soa_layout/tenants.md +++ b/.claude/v3/soa_layout/tenants.md @@ -57,7 +57,7 @@ Offsets are FULL-ROW; subtract 32 for slab-relative. | 14 | CausalWitness | U8 × 16 | 16 B | [204,220) | CausalWitness facet lane — 16 B content-blind V3 4+12 facet read as **G24N4** (24 signed i4 loci, a lane shape name, never a `CascadeShape` variant); each nibble is a context pointer (signed ±8 window offset), not a strength. Slots 16..24 reserved-zero. **Status: EXPERIMENTAL — not in the operator-locked §3 catalogue** (per its own doc-comment) | | 15 | EpisodicBasin | U8 × 32 | 32 B | [220,252) | **D-ACR-6 rail** — a promoted basin as REFERENCES: `subject` u16 · `member_count` u16 · `self_code` 12 B (Cam96 centroid) · `version_from`/`version_to` u64. Members are reached by following `(subject, [from,to))` into the triple stream, never inlined — the fat-concept guard `§3a` names. Width is NOT stored (recomputable through the references) | | 16 | Register0 | U8 × 16 | 16 B | [252,268) | **D-LXC-29** 128-bit working register, NO classid in the payload; read as `Register128` only when the slab declares `SlabReading::Register128` (`ResolvedReading::bind_register128` refuses otherwise). All-zero = empty accumulator | -| 17 | Register1 | U8 × 16 | 16 B | [268,284) | Second independent `Register128` rail, same contract as Register0 | +| 17 | Register1 | U8 × 16 | 16 B | [268,284) | Second independent `Register128` rail, same contract as Register0. Both rails can instead be declared signed: `SlabReading::RegisterI4x32` / `RegisterI8x16`, bound by `bind_signed_register` with a `RegisterLaw` (2026-10-08) | | 18 | Nars16x8 | U16 × 8 (LE) | 16 B | [284,300) | One `u16` per Moore slot (`NW,N,NE,W,E,SW,S,SE`). Layout RATIFIED; the `u16` reading is CANDIDATE and NOT declared by this tenant. Never shares bytes or a reading with MooreNars16 | | 19 | MoorePalettePairs | U8 × 16 | 16 B | [300,316) | One Palette256 `(u8:u8)` pair per Moore slot: byte `2i` = first operand, `2i+1` = second, addressing a 256×256 law as `(first << 8) \| second` (the #1336 `PairAddress` orientation). Two separate bytes, never one widened `u16` | | 20 | MooreNars16 | U16 × 8 (LE) | 16 B | [316,332) | One `MooreNars16` per Moore slot: `Pearl3 \| Energy4 \| Plasticity3 \| Polarity1 \| Epi5` (bits 0..2, 3..6, 7..9, 10, 11..15). Direction = `(slot, polarity)`, never the S/P/O sign triple (`DirectionReading::require_sign_triple` refuses it); one witness for all eight lanes, held by the owner, not in these bytes (`MooreTenantMut::lift_ce64` refuses mixed W). Epi5 24..31 reserved. Layout RATIFIED, semantics CANDIDATE | From 32a7ddf09684a7ac02c1483a3373f1e932bc456a Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 8 Oct 2026 08:59:42 +0000 Subject: [PATCH 3/5] planner: J follows the strict CE64 ISA from #1407 forward() now returns Result and refuses non-executable codes, so the fixture draws only the five executable mantissas and unwraps the result. Re-verified: the energy-3-bit disable still turns J red. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01EFw2WdKr1oxvaKCJC2ua2R --- .../tests/moore_tenant_isa_equivalence.rs | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/crates/lance-graph-planner/tests/moore_tenant_isa_equivalence.rs b/crates/lance-graph-planner/tests/moore_tenant_isa_equivalence.rs index ac3061352..d64a325ac 100644 --- a/crates/lance-graph-planner/tests/moore_tenant_isa_equivalence.rs +++ b/crates/lance-graph-planner/tests/moore_tenant_isa_equivalence.rs @@ -28,8 +28,15 @@ impl Rng { } } +const MANTISSA: u64 = 0xF << 46; + +/// The codes the CE64 ISA executes (Ded +1, Ind +2, Abd -1, Rev +4, Syn +5) +/// as raw 4-bit mantissa nibbles; `forward` refuses every other code. +const EXECUTABLE: [u64; 5] = [0x1, 0x2, 0xF, 0x4, 0x5]; + fn canonical(r: &mut Rng, w: u64) -> u64 { - (r.next() & !W & !EPI5) | (w << 53) | ((r.next() % 24) << 59) + let code = EXECUTABLE[(r.next() % 5) as usize]; + (r.next() & !W & !EPI5 & !MANTISSA) | (w << 53) | ((r.next() % 24) << 59) | (code << 46) } fn tables() -> [Box<[u8; 256 * 256]>; 3] { @@ -53,8 +60,12 @@ fn ops(x: u64, y: u64, t: &[Box<[u8; 256 * 256]>; 3]) -> [u64; 5] { let mut ly = y; ly.learn(x, 0); [ - x.forward(y, &t[0], &t[1], &t[2]).0, - y.forward(x, &t[0], &t[1], &t[2]).0, + x.forward(y, &t[0], &t[1], &t[2]) + .expect("executable code") + .0, + y.forward(x, &t[0], &t[1], &t[2]) + .expect("executable code") + .0, lx.0, ly.0, x.syllogize(y).expect("chain figure").conclusion.0, From 96304b88bc0683b74181341ddc50c9e7a81351e3 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 8 Oct 2026 11:05:16 +0000 Subject: [PATCH 4/5] contract: signed register law comes from concept and slab, never the binder Codex P2 on #1410: bind_signed_register took the law as a parameter, so a writer binding Support and a reader binding AxisPosition both succeeded over the same bytes. The binder no longer chooses a law. Two authorities must agree: - the slab records the law its bytes were written under, inside the reading: SlabReading::RegisterI4x32(law) / RegisterI8x16(law), tags 2..=7 (to_tag is the inverse of from_tag); - the Activation declares the law per concept (with_register_laws, register_law_for, fail-closed: NoRegisterLawFor). bind_signed_register(activation, rails) refuses a mismatch with RegisterLawMismatch. Activation::new is unchanged. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01EFw2WdKr1oxvaKCJC2ua2R --- crates/lance-graph-contract/src/hotplug.rs | 306 +++++++++++++----- .../lance-graph-contract/src/register128.rs | 10 +- 2 files changed, 241 insertions(+), 75 deletions(-) diff --git a/crates/lance-graph-contract/src/hotplug.rs b/crates/lance-graph-contract/src/hotplug.rs index 4e615bf5b..e5a494d44 100644 --- a/crates/lance-graph-contract/src/hotplug.rs +++ b/crates/lance-graph-contract/src/hotplug.rs @@ -129,6 +129,11 @@ pub struct Activation { /// now guarantees is that *asking* for an absent one bangs instead of /// quietly yielding a V1 tail. read_modes: Vec<(u16, crate::canonical_node::ReadMode)>, + /// `(concept id, RegisterLaw)` — what a signed register population of + /// each concept means. Empty unless the authority declares laws with + /// [`with_register_laws`](Activation::with_register_laws); a concept + /// with no declared law cannot bind signed registers at all. + register_laws: Vec<(u16, crate::register128::RegisterLaw)>, } impl Activation { @@ -145,9 +150,36 @@ impl Activation { concepts, capabilities, read_modes, + register_laws: Vec::new(), } } + /// Declare the register law of each concept. The authority side of + /// [`ResolvedReading::bind_signed_register`]: the binder never chooses a + /// law, it is resolved here and checked against the law the slab + /// recorded. + #[must_use] + pub fn with_register_laws(mut self, laws: Vec<(u16, crate::register128::RegisterLaw)>) -> Self { + self.register_laws = laws; + self + } + + /// The register law declared for `concept`. Fails closed, like + /// [`read_mode_for`](Activation::read_mode_for): there is no default law. + /// + /// # Errors + /// + /// [`ActivationDrift::NoRegisterLawFor`] if `concept` has no declared law. + pub fn register_law_for( + &self, + concept: u16, + ) -> Result { + self.register_laws + .iter() + .find_map(|(c, l)| (*c == concept).then_some(*l)) + .ok_or(ActivationDrift::NoRegisterLawFor(concept)) + } + /// The reading for one hot-plugged concept — the ONLY lookup, and it /// fails closed. /// @@ -213,13 +245,16 @@ pub enum SlabReading { Register128 = 1, /// The same register rails, written as 32 signed `i4` values: dim `2k` /// is the low nibble of byte `k`, dim `2k+1` the high nibble (the - /// [`crate::atoms::I4x32`] layout). Bound by + /// [`crate::atoms::I4x32`] layout), under the recorded + /// [`RegisterLaw`](crate::register128::RegisterLaw): the law the writer + /// actually wrote with. Bound by /// [`ResolvedReading::bind_signed_register`], never by /// [`ResolvedReading::bind_register128`]. - RegisterI4x32 = 2, + RegisterI4x32(crate::register128::RegisterLaw), /// The same register rails, written as 16 signed `i8` values, one per - /// byte. Bound by [`ResolvedReading::bind_signed_register`]. - RegisterI8x16 = 3, + /// byte, under the recorded law. Bound by + /// [`ResolvedReading::bind_signed_register`]. + RegisterI8x16(crate::register128::RegisterLaw), } impl SlabReading { @@ -233,11 +268,47 @@ impl SlabReading { match tag { 0 => Ok(SlabReading::Facet96), 1 => Ok(SlabReading::Register128), - 2 => Ok(SlabReading::RegisterI4x32), - 3 => Ok(SlabReading::RegisterI8x16), + 2..=7 => { + let carving = (tag - 2) / 3; + let law = match (tag - 2) % 3 { + 0 => crate::register128::RegisterLaw::RelativeOffset, + 1 => crate::register128::RegisterLaw::AxisPosition, + _ => crate::register128::RegisterLaw::Support, + }; + if carving == 0 { + Ok(SlabReading::RegisterI4x32(law)) + } else { + Ok(SlabReading::RegisterI8x16(law)) + } + } other => Err(ActivationDrift::UnknownSlabReading(other)), } } + + /// The envelope tag, the inverse of [`from_tag`](Self::from_tag). + /// + /// | tag | reading | + /// |---|---| + /// | 0 | `Facet96` | + /// | 1 | `Register128` | + /// | 2, 3, 4 | `RegisterI4x32` with `RelativeOffset`, `AxisPosition`, `Support` | + /// | 5, 6, 7 | `RegisterI8x16` with `RelativeOffset`, `AxisPosition`, `Support` | + #[must_use] + pub const fn to_tag(self) -> u8 { + const fn law_index(law: crate::register128::RegisterLaw) -> u8 { + match law { + crate::register128::RegisterLaw::RelativeOffset => 0, + crate::register128::RegisterLaw::AxisPosition => 1, + crate::register128::RegisterLaw::Support => 2, + } + } + match self { + SlabReading::Facet96 => 0, + SlabReading::Register128 => 1, + SlabReading::RegisterI4x32(law) => 2 + law_index(law), + SlabReading::RegisterI8x16(law) => 5 + law_index(law), + } + } } /// What a slab's metadata envelope DECLARES about its own, already written @@ -381,33 +452,43 @@ impl ResolvedReading { Ok(crate::register128::RegisterLanes::new(self.concept, rails)) } - /// Bind the register rails of this population as SIGNED values under - /// one declared law. Checked ONCE per population, like - /// [`bind_register128`](Self::bind_register128). + /// Bind the register rails of this population as SIGNED values. Checked + /// ONCE per population, like [`bind_register128`](Self::bind_register128). /// - /// The slab declares the carving (how the bytes were written: - /// [`SlabReading::RegisterI4x32`] or [`SlabReading::RegisterI8x16`]). - /// The caller declares the `law` (what the signed values mean for this - /// concept: a relative offset, a position on an axis, or support). Both - /// travel with the returned lanes, and every read and write checks the - /// law it is asked for against the bound one, so a consumer of one law - /// can never silently read another's values. + /// The carving and the law each have one authority, and the binder + /// chooses neither: + /// - the slab declares the carving AND the law its bytes were written + /// under ([`SlabReading::RegisterI4x32`] / [`SlabReading::RegisterI8x16`]); + /// - `activation` declares the law of this concept + /// ([`Activation::register_law_for`]). + /// + /// The two must agree. A writer and a reader that bind the same + /// population therefore always hold the same law, and every read and + /// write on the returned lanes checks the law it is asked for against it. /// /// # Errors /// /// - [`ActivationDrift::NotSignedRegister`] when the slab declared any /// other reading, including the unsigned [`SlabReading::Register128`], /// or nothing. + /// - [`ActivationDrift::NoRegisterLawFor`] when the concept has no + /// declared law. + /// - [`ActivationDrift::RegisterLawMismatch`] when the slab's recorded law + /// differs from the concept's. /// - [`ActivationDrift::RegisterRailAbsent`] when the value schema does /// not materialise a requested rail. pub fn bind_signed_register( &self, + activation: &Activation, rails: crate::register128::RegisterRails, - law: crate::register128::RegisterLaw, ) -> Result { - let carving = match self.slab { - Some(SlabReading::RegisterI4x32) => crate::register128::RegisterCarving::I4x32, - Some(SlabReading::RegisterI8x16) => crate::register128::RegisterCarving::I8x16, + let (carving, slab_law) = match self.slab { + Some(SlabReading::RegisterI4x32(law)) => { + (crate::register128::RegisterCarving::I4x32, law) + } + Some(SlabReading::RegisterI8x16(law)) => { + (crate::register128::RegisterCarving::I8x16, law) + } other => { return Err(ActivationDrift::NotSignedRegister { concept: self.concept, @@ -415,6 +496,14 @@ impl ResolvedReading { }) } }; + let concept_law = activation.register_law_for(self.concept)?; + if concept_law != slab_law { + return Err(ActivationDrift::RegisterLawMismatch { + concept: self.concept, + concept_law, + slab_law, + }); + } for &tenant in rails.tenants() { if !self.read_mode.value_schema.has(tenant) { return Err(ActivationDrift::RegisterRailAbsent { @@ -427,7 +516,7 @@ impl ResolvedReading { self.concept, rails, carving, - law, + concept_law, )) } } @@ -478,6 +567,19 @@ pub enum ActivationDrift { /// What the slab declared instead (`None`: nothing). slab: Option, }, + /// The authority declared no register law for this concept, so its + /// signed registers cannot be bound. + NoRegisterLawFor(u16), + /// The law a slab recorded for its signed registers differs from the + /// law the authority declares for the concept. + RegisterLawMismatch { + /// The concept the reading was resolved under. + concept: u16, + /// The law the authority declares. + concept_law: crate::register128::RegisterLaw, + /// The law the slab recorded. + slab_law: crate::register128::RegisterLaw, + }, /// The resolved value schema does not materialise a requested register /// rail. RegisterRailAbsent { @@ -575,6 +677,18 @@ impl core::fmt::Display for ActivationDrift { "concept 0x{concept:04X}: slab declares {slab:?}, not a signed register \ carving; no signed register rails are granted" ), + Self::NoRegisterLawFor(concept) => write!( + f, + "concept 0x{concept:04X}: no register law declared; signed registers are not bound" + ), + Self::RegisterLawMismatch { + concept, + concept_law, + slab_law, + } => write!( + f, + "concept 0x{concept:04X}: slab recorded {slab_law:?}, concept declares {concept_law:?}" + ), Self::RegisterRailAbsent { concept, tenant } => write!( f, "concept 0x{concept:04X}: value schema does not materialise register tenant {tenant}" @@ -825,9 +939,23 @@ mod tests { fn an_unsupported_physical_reading_fails_closed() { assert_eq!(SlabReading::from_tag(0), Ok(SlabReading::Facet96)); assert_eq!(SlabReading::from_tag(1), Ok(SlabReading::Register128)); - assert_eq!(SlabReading::from_tag(2), Ok(SlabReading::RegisterI4x32)); - assert_eq!(SlabReading::from_tag(3), Ok(SlabReading::RegisterI8x16)); - for tag in [4u8, 5, 0x80, 0xFF] { + for tag in 0u8..=7 { + let r = SlabReading::from_tag(tag).unwrap(); + assert_eq!(r.to_tag(), tag, "{r:?}"); + } + assert_eq!( + SlabReading::from_tag(3), + Ok(SlabReading::RegisterI4x32( + crate::register128::RegisterLaw::AxisPosition + )) + ); + assert_eq!( + SlabReading::from_tag(7), + Ok(SlabReading::RegisterI8x16( + crate::register128::RegisterLaw::Support + )) + ); + for tag in [8u8, 9, 0x80, 0xFF] { assert_eq!( SlabReading::from_tag(tag), Err(ActivationDrift::UnknownSlabReading(tag)) @@ -1016,70 +1144,104 @@ mod tests { } /// FAILS IF: signed rails are granted to a slab without a signed - /// carving (including the unsigned Register128 slab), the carving is - /// taken from anywhere but the slab, or a rail the schema lacks is - /// granted. + /// carving, the carving or law is taken from anywhere but the slab + /// and the authority, a slab law that differs from the concept's is + /// accepted, a concept without a law binds, or a rail the schema + /// lacks is granted. #[test] - fn signed_rails_are_granted_only_to_a_signed_carving() { + fn signed_rails_need_a_signed_carving_and_an_agreeing_law() { use crate::canonical_node::ValueTenant; use crate::register128::{RegisterCarving, RegisterLaw, RegisterRails}; - let a = act(); + const LAWS: [RegisterLaw; 3] = [ + RegisterLaw::RelativeOffset, + RegisterLaw::AxisPosition, + RegisterLaw::Support, + ]; let carved = |reading, value_schema| SlabDeclaration { reading, value_schema, layout_version: ENVELOPE_LAYOUT_VERSION, }; - for (reading, carving) in [ - (SlabReading::RegisterI4x32, RegisterCarving::I4x32), - (SlabReading::RegisterI8x16, RegisterCarving::I8x16), - ] { - let r = a - .resolve_for_context(0x0901, Some(&carved(reading, ValueSchema::Full))) - .unwrap(); - let lanes = r - .bind_signed_register(RegisterRails::Two, RegisterLaw::AxisPosition) - .unwrap(); - assert_eq!(lanes.carving(), carving); - assert_eq!(lanes.law(), RegisterLaw::AxisPosition); - assert_eq!(lanes.concept(), 0x0901); - assert_eq!( - r.bind_register128(RegisterRails::One), - Err(ActivationDrift::NotRegister128 { - concept: 0x0901, - slab: Some(reading) - }), - "a signed slab is not an unsigned word register" - ); + for concept_law in LAWS { + // 0x0901 declares `concept_law`; 0x0902 declares nothing. + let a = act().with_register_laws(vec![(0x0901, concept_law)]); + for slab_law in LAWS { + for (reading, carving) in [ + (SlabReading::RegisterI4x32(slab_law), RegisterCarving::I4x32), + (SlabReading::RegisterI8x16(slab_law), RegisterCarving::I8x16), + ] { + let r = a + .resolve_for_context(0x0901, Some(&carved(reading, ValueSchema::Full))) + .unwrap(); + let bound = r.bind_signed_register(&a, RegisterRails::Two); + if slab_law == concept_law { + let lanes = bound.unwrap(); + assert_eq!(lanes.carving(), carving); + assert_eq!(lanes.law(), concept_law); + assert_eq!(lanes.concept(), 0x0901); + } else { + assert_eq!( + bound, + Err(ActivationDrift::RegisterLawMismatch { + concept: 0x0901, + concept_law, + slab_law, + }) + ); + } + assert_eq!( + r.bind_register128(RegisterRails::One), + Err(ActivationDrift::NotRegister128 { + concept: 0x0901, + slab: Some(reading) + }), + "a signed slab is not an unsigned word register" + ); + let lawless = a + .resolve_for_context(0x0902, Some(&carved(reading, ValueSchema::Full))) + .unwrap(); + assert_eq!( + lawless.bind_signed_register(&a, RegisterRails::One), + Err(ActivationDrift::NoRegisterLawFor(0x0902)) + ); + } + } let narrow = a - .resolve_for_context(0x0901, Some(&carved(reading, ValueSchema::Cognitive))) + .resolve_for_context( + 0x0901, + Some(&carved( + SlabReading::RegisterI8x16(concept_law), + ValueSchema::Cognitive, + )), + ) .unwrap(); assert_eq!( - narrow.bind_signed_register(RegisterRails::One, RegisterLaw::Support), + narrow.bind_signed_register(&a, RegisterRails::One), Err(ActivationDrift::RegisterRailAbsent { concept: 0x0901, tenant: ValueTenant::Register0 as u8, }) ); - } - let words = a - .resolve_for_context(0x0901, Some(®_decl(ValueSchema::Full))) - .unwrap(); - let facet = a - .resolve_for_context(0x0901, Some(&decl(ValueSchema::Full))) - .unwrap(); - let undeclared = a.resolve_for_context(0x0901, None).unwrap(); - for (r, slab) in [ - (words, Some(SlabReading::Register128)), - (facet, Some(SlabReading::Facet96)), - (undeclared, None), - ] { - assert_eq!( - r.bind_signed_register(RegisterRails::One, RegisterLaw::RelativeOffset), - Err(ActivationDrift::NotSignedRegister { - concept: 0x0901, - slab - }) - ); + let words = a + .resolve_for_context(0x0901, Some(®_decl(ValueSchema::Full))) + .unwrap(); + let facet = a + .resolve_for_context(0x0901, Some(&decl(ValueSchema::Full))) + .unwrap(); + let undeclared = a.resolve_for_context(0x0901, None).unwrap(); + for (r, slab) in [ + (words, Some(SlabReading::Register128)), + (facet, Some(SlabReading::Facet96)), + (undeclared, None), + ] { + assert_eq!( + r.bind_signed_register(&a, RegisterRails::One), + Err(ActivationDrift::NotSignedRegister { + concept: 0x0901, + slab + }) + ); + } } } diff --git a/crates/lance-graph-contract/src/register128.rs b/crates/lance-graph-contract/src/register128.rs index 18d9e134c..74f67517a 100644 --- a/crates/lance-graph-contract/src/register128.rs +++ b/crates/lance-graph-contract/src/register128.rs @@ -205,9 +205,13 @@ impl RegisterCarving { } } -/// What the signed values of a bound population mean. Declared once, at -/// binding, for the concept the population was resolved under; checked on -/// every read and write. +/// What the signed values of a bound population mean. Two authorities, which +/// must agree: the concept's law, declared by the +/// [`Activation`](crate::hotplug::Activation) +/// ([`register_law_for`](crate::hotplug::Activation::register_law_for)), and the +/// law the slab recorded when its bytes were written +/// ([`SlabReading::RegisterI4x32`](crate::hotplug::SlabReading::RegisterI4x32)). +/// The binder chooses neither. Checked again on every read and write. /// /// Each law is a different kind of quantity, so a value written under one /// is not a value under another even when the bytes are identical. That is From 4c8dc638c23f973c7fa63e15d3ad2bb70822e8ec Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 8 Oct 2026 11:06:17 +0000 Subject: [PATCH 5/5] board: register law authority (concept + slab) in entry, LATEST_STATE, tenants Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01EFw2WdKr1oxvaKCJC2ua2R --- .claude/board/LATEST_STATE.md | 2 +- .../2026-10-08-register128-signed-readings.md | 36 ++++++++++++------- .claude/v3/soa_layout/tenants.md | 2 +- 3 files changed, 25 insertions(+), 15 deletions(-) diff --git a/.claude/board/LATEST_STATE.md b/.claude/board/LATEST_STATE.md index ddce1f77d..e7d55d470 100644 --- a/.claude/board/LATEST_STATE.md +++ b/.claude/board/LATEST_STATE.md @@ -1,6 +1,6 @@ ## 2026-10-08 — Register128 signed readings on branch `ccr-b2e415d9-4jfvyk-register-readings` (unmerged) -- Contract delta (additive): `hotplug::SlabReading::{RegisterI4x32 = 2, RegisterI8x16 = 3}`, `ResolvedReading::bind_signed_register`, `ActivationDrift::NotSignedRegister`; `register128::{RegisterCarving, RegisterLaw, SignedRegisterLanes, SignedRegisterRefusal}`; `atoms::I4x32::{from_bytes, as_bytes}`. No layout change. Entry: `entries/2026-10-08-register128-signed-readings.md`. +- Contract delta (additive): `hotplug::SlabReading::{RegisterI4x32(law), RegisterI8x16(law)}` (tags 2..=7, `to_tag`), `Activation::{with_register_laws, register_law_for}`, `ResolvedReading::bind_signed_register(&activation, rails)`, `ActivationDrift::{NotSignedRegister, NoRegisterLawFor, RegisterLawMismatch}`; `register128::{RegisterCarving, RegisterLaw, SignedRegisterLanes, SignedRegisterRefusal}`; `atoms::I4x32::{from_bytes, as_bytes}`. No layout change. Entry: `entries/2026-10-08-register128-signed-readings.md`. ## 2026-10-08 — HHTL / NARS / Moore value tenants on branch `ccr-b2e415d9-4jfvyk-tenants` (unmerged) diff --git a/.claude/board/entries/2026-10-08-register128-signed-readings.md b/.claude/board/entries/2026-10-08-register128-signed-readings.md index 6cee05972..44aae9c6c 100644 --- a/.claude/board/entries/2026-10-08-register128-signed-readings.md +++ b/.claude/board/entries/2026-10-08-register128-signed-readings.md @@ -1,20 +1,26 @@ # 2026-10-08 — Register128 signed readings (32×i4, 16×i8) with a per-family law **Status:** CURRENT-CONTRACT, TEST-PINNED. Branch -`ccr-b2e415d9-4jfvyk-register-readings`, commit `14831543`. +`ccr-b2e415d9-4jfvyk-register-readings`, commits `14831543` and `96304b88` +(law authority, after Codex P2 on #1410). ## What landed The carrier is unchanged: the `Register0` / `Register1` rails (16 B each), no classid in the payload, no layout change. -- **Carving, declared by the slab.** `SlabReading::RegisterI4x32 = 2` (32 × i4, - dim `2k` = low nibble of byte `k`, the `atoms::I4x32` layout) and - `SlabReading::RegisterI8x16 = 3` (16 × i8, dim `k` = byte `k`). -- **Law, declared at binding.** `RegisterLaw::{RelativeOffset, AxisPosition, - Support}`, passed to `ResolvedReading::bind_signed_register(rails, law)` once - per population. The returned `SignedRegisterLanes` carry concept, rails, - carving and law. +- **Carving and recorded law, declared by the slab.** + `SlabReading::RegisterI4x32(law)` (32 × i4, dim `2k` = low nibble of byte + `k`, the `atoms::I4x32` layout) and `SlabReading::RegisterI8x16(law)` (16 × + i8, dim `k` = byte `k`). Envelope tags 2..=7 (`to_tag` inverts `from_tag`). + The law is the one the writer actually wrote with. +- **Concept law, declared by the authority.** `Activation::with_register_laws` + / `register_law_for` (fail-closed: `NoRegisterLawFor`). +- **Binding checks agreement.** `ResolvedReading::bind_signed_register(&activation, + rails)` takes no law; it refuses `RegisterLawMismatch` when the slab's law and + the concept's differ. A writer and a reader of one population therefore hold + the same law. The returned `SignedRegisterLanes` carry concept, rails, carving + and law. `RegisterLaw::{RelativeOffset, AxisPosition, Support}`. - **Checked on every access.** `read_i4x32` / `read_i8x16` / `write_i4x32` / `write_i8x16` take the law the caller expects and refuse, in order, an absent rail, a different carving, a different law. An i4 outside `-8..=7` is refused, @@ -40,7 +46,7 @@ lane with its own classid, and re-homing it is a separate decision. ## Tests and disable runs -Hotplug: `signed_rails_are_granted_only_to_a_signed_carving`; tag test now +Hotplug: `signed_rails_need_a_signed_carving_and_an_agreeing_law`; tag test now accepts 2 and 3. Register128: round trip of every i4 value in every dim with the nibble layout pinned; i8 dim-k-is-byte-k; the same bytes differ under the two carvings; law mismatch (all 3×3 pairs, both carvings) refused and writes @@ -53,15 +59,19 @@ before writing; writes stay inside their rail. | carving check removed | `a_different_carving_is_refused` | | rail check removed | `signed_writes_stay_inside_their_rail` | | i4 range check removed | `an_out_of_range_i4_is_refused_before_writing` | -| binding accepts `Register128` as i4 | `signed_rails_are_granted_only_to_a_signed_carving` | +| binding accepts `Register128` as i4 | `signed_rails_need_a_signed_carving_and_an_agreeing_law` | | binding swaps the carvings | same | | binding skips the schema check | same | +| law mismatch check removed | `signed_rails_need_a_signed_carving_and_an_agreeing_law` | +| concept law ignored (slab law used) | same | +| `to_tag` shifts the I8x16 tags | `an_unsupported_physical_reading_fails_closed` | +| `from_tag` drops the law | same | ## OPEN -- The law is declared by the binder. There is no OGAR table mapping a concept - to its law yet, so a binder can still declare the wrong one; the contract - only guarantees readers cannot silently read another law's values. +- No OGAR authority populates `with_register_laws` yet (`lance-graph-ogar` + builds its `Activation` without laws), so in production every concept still + refuses to bind signed registers. That is fail-closed, not a gap in the check. - Lane or reading per family is undecided until measured: 32×i4 vs 16×i8 on KJV anaphora and the mammal fixture. The rule "outside the local window → basin edge" is not changed by `RelativeOffset` under `I8x16`. diff --git a/.claude/v3/soa_layout/tenants.md b/.claude/v3/soa_layout/tenants.md index 15f0d2c0f..1ff465246 100644 --- a/.claude/v3/soa_layout/tenants.md +++ b/.claude/v3/soa_layout/tenants.md @@ -57,7 +57,7 @@ Offsets are FULL-ROW; subtract 32 for slab-relative. | 14 | CausalWitness | U8 × 16 | 16 B | [204,220) | CausalWitness facet lane — 16 B content-blind V3 4+12 facet read as **G24N4** (24 signed i4 loci, a lane shape name, never a `CascadeShape` variant); each nibble is a context pointer (signed ±8 window offset), not a strength. Slots 16..24 reserved-zero. **Status: EXPERIMENTAL — not in the operator-locked §3 catalogue** (per its own doc-comment) | | 15 | EpisodicBasin | U8 × 32 | 32 B | [220,252) | **D-ACR-6 rail** — a promoted basin as REFERENCES: `subject` u16 · `member_count` u16 · `self_code` 12 B (Cam96 centroid) · `version_from`/`version_to` u64. Members are reached by following `(subject, [from,to))` into the triple stream, never inlined — the fat-concept guard `§3a` names. Width is NOT stored (recomputable through the references) | | 16 | Register0 | U8 × 16 | 16 B | [252,268) | **D-LXC-29** 128-bit working register, NO classid in the payload; read as `Register128` only when the slab declares `SlabReading::Register128` (`ResolvedReading::bind_register128` refuses otherwise). All-zero = empty accumulator | -| 17 | Register1 | U8 × 16 | 16 B | [268,284) | Second independent `Register128` rail, same contract as Register0. Both rails can instead be declared signed: `SlabReading::RegisterI4x32` / `RegisterI8x16`, bound by `bind_signed_register` with a `RegisterLaw` (2026-10-08) | +| 17 | Register1 | U8 × 16 | 16 B | [268,284) | Second independent `Register128` rail, same contract as Register0. Both rails can instead be declared signed: `SlabReading::RegisterI4x32(law)` / `RegisterI8x16(law)`, bound by `bind_signed_register`, which requires the slab's recorded `RegisterLaw` to equal the concept's (2026-10-08) | | 18 | Nars16x8 | U16 × 8 (LE) | 16 B | [284,300) | One `u16` per Moore slot (`NW,N,NE,W,E,SW,S,SE`). Layout RATIFIED; the `u16` reading is CANDIDATE and NOT declared by this tenant. Never shares bytes or a reading with MooreNars16 | | 19 | MoorePalettePairs | U8 × 16 | 16 B | [300,316) | One Palette256 `(u8:u8)` pair per Moore slot: byte `2i` = first operand, `2i+1` = second, addressing a 256×256 law as `(first << 8) \| second` (the #1336 `PairAddress` orientation). Two separate bytes, never one widened `u16` | | 20 | MooreNars16 | U16 × 8 (LE) | 16 B | [316,332) | One `MooreNars16` per Moore slot: `Pearl3 \| Energy4 \| Plasticity3 \| Polarity1 \| Epi5` (bits 0..2, 3..6, 7..9, 10, 11..15). Direction = `(slot, polarity)`, never the S/P/O sign triple (`DirectionReading::require_sign_triple` refuses it); one witness for all eight lanes, held by the owner, not in these bytes (`MooreTenantMut::lift_ce64` refuses mixed W). Epi5 24..31 reserved. Layout RATIFIED, semantics CANDIDATE |