Skip to content

Commit fc1bd86

Browse files
committed
feat: SPO stack hardening — bitmap widening, sparse fingerprints, integrity verification
Core fixes from PR #167/#168 review, rewritten from spec: - sparse.rs: Widen bitmap [u64;2] → [u64;BITMAP_WORDS=4] to cover all 256 positions - builder.rs: Fix invalid hex literal (0xCHA1_D15C → 0xC4A1_D15C), rewrite label_fp() to produce ~11% density sparse fingerprints via xorshift64 PRNG - store.rs: Add TruthGate epistemic filter (OPEN/STRONG/CERTAIN), fix scent/Hamming scale mismatch with separate scent_radius parameter, add gated query methods - geometry.rs: Add ContainerGeometry::Spo = 6 variant - mod.rs: Wire pub mod spo (was missing — ~2000 lines of dead code) - bind_space.rs: Add clam_merkle field to BindNode, stamp ClamPath + MerkleRoot on write_dn_node()/write_dn_path(), add verify_lineage() + Epoch snapshot All 61 tests pass (24 SPO + 32 ClamPath + 5 bind_space integrity). https://claude.ai/code/session_013JU8MRtxRRTtuc5217r6s5
1 parent a53d440 commit fc1bd86

7 files changed

Lines changed: 641 additions & 147 deletions

File tree

crates/ladybug-contract/src/geometry.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,12 @@ pub enum ContainerGeometry {
3434
/// Adjacency is implicit in position. Spine = XOR of children.
3535
/// Total record: (N+1) × 1 KB.
3636
Tree = 5,
37+
38+
/// 1 content container: three sparse axes (Subject × Predicate × Object)
39+
/// packed into a single Container. Forward, reverse, and relation queries
40+
/// are native axis scans — no join, no edge table.
41+
/// Total record: 2 × 2 KB = 4 KB (meta + content).
42+
Spo = 6,
3743
}
3844

3945
impl ContainerGeometry {
@@ -46,6 +52,7 @@ impl ContainerGeometry {
4652
ContainerGeometry::Extended => 2,
4753
ContainerGeometry::Chunked => 1, // just summary; chunks added later
4854
ContainerGeometry::Tree => 1, // root; children added later
55+
ContainerGeometry::Spo => 1, // three sparse axes in one container
4956
}
5057
}
5158

@@ -58,6 +65,7 @@ impl ContainerGeometry {
5865
3 => Some(ContainerGeometry::Extended),
5966
4 => Some(ContainerGeometry::Chunked),
6067
5 => Some(ContainerGeometry::Tree),
68+
6 => Some(ContainerGeometry::Spo),
6169
_ => None,
6270
}
6371
}

src/graph/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ pub use edge::{Edge, EdgeType};
1313
pub use traversal::Traversal;
1414

1515
pub mod avx_engine;
16+
17+
pub mod spo;
1618
pub use avx_engine::{
1719
FingerprintGraph, QueryMatch, avx512_available, batched_query, hamming_distance, simd_info,
1820
};

src/graph/spo/builder.rs

Lines changed: 39 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
//! Edges: X = BIND(src, permute(verb,1)), Y = verb, Z = BIND(tgt, permute(verb,2))
55
//! Meta: X = BUNDLE(chain X axes), Y = CHAIN_DISCOVERED, Z = BUNDLE(chain Z axes)
66
7-
use ladybug_contract::container::Container;
7+
use ladybug_contract::container::{Container, CONTAINER_WORDS};
88
use ladybug_contract::nars::TruthValue;
99
use ladybug_contract::record::CogRecord;
1010

@@ -118,8 +118,8 @@ impl SpoBuilder {
118118
let z_refs: Vec<&Container> = z_denses.iter().collect();
119119

120120
let x_dense = Container::bundle(&x_refs);
121-
// Y axis: deterministic "chain discovered" marker
122-
let y_dense = Container::random(0xCHA1_D15C); // CHAIN_DISCOVERED seed
121+
// Y axis: sparse deterministic "chain discovered" marker
122+
let y_dense = label_fp("CHAIN_DISCOVERED");
123123
let z_dense = Container::bundle(&z_refs);
124124

125125
let x = SparseContainer::from_dense(&x_dense);
@@ -187,15 +187,40 @@ impl SpoBuilder {
187187
// CONVENIENCE FUNCTIONS
188188
// ============================================================================
189189

190-
/// Create a deterministic fingerprint from a string label (for testing/seeding).
190+
/// Target density for label fingerprints (~11%).
191+
/// At 256 words, 28 non-zero words ≈ 11% density.
192+
/// Three such axes (28+28+28 + 12 bitmap = 96) fit comfortably in 256 words.
193+
const LABEL_FP_TARGET_WORDS: usize = 28;
194+
195+
/// Create a deterministic sparse fingerprint from a string label.
196+
///
197+
/// Produces ~11% density so three axes pack into one Container.
198+
/// Deterministic: same label always produces the same fingerprint.
191199
pub fn label_fp(label: &str) -> Container {
192-
// Simple hash: sum bytes with mixing
200+
// Hash the label to a seed
193201
let mut seed = 0u64;
194202
for (i, b) in label.bytes().enumerate() {
195203
seed ^= (b as u64).wrapping_mul(0x9e3779b97f4a7c15);
196204
seed = seed.rotate_left((i as u32) % 64);
197205
}
198-
Container::random(seed)
206+
207+
// Use the seed to deterministically select which words are non-zero
208+
// and what values they contain.
209+
let mut container = Container::zero();
210+
let mut rng = seed;
211+
for _ in 0..LABEL_FP_TARGET_WORDS {
212+
// Simple xorshift64 for deterministic PRNG
213+
rng ^= rng << 13;
214+
rng ^= rng >> 7;
215+
rng ^= rng << 17;
216+
let pos = (rng as usize) % CONTAINER_WORDS;
217+
// Generate a non-zero value
218+
rng ^= rng << 13;
219+
rng ^= rng >> 7;
220+
rng ^= rng << 17;
221+
container.words[pos] = rng | 1; // ensure non-zero
222+
}
223+
container
199224
}
200225

201226
/// Create a deterministic DN hash from a string (for testing).
@@ -254,9 +279,9 @@ mod tests {
254279

255280
#[test]
256281
fn test_build_edge_three_axes() {
257-
let src = Container::random(1); // Jan
258-
let verb = Container::random(2); // KNOWS
259-
let tgt = Container::random(3); // Ada
282+
let src = label_fp("Jan");
283+
let verb = label_fp("KNOWS");
284+
let tgt = label_fp("Ada");
260285

261286
let record = SpoBuilder::build_edge(
262287
dn_hash("jan_knows_ada"),
@@ -283,11 +308,11 @@ mod tests {
283308

284309
#[test]
285310
fn test_build_meta_awareness() {
286-
let src = Container::random(10);
287-
let verb1 = Container::random(20);
288-
let tgt1 = Container::random(30);
289-
let verb2 = Container::random(40);
290-
let tgt2 = Container::random(50);
311+
let src = label_fp("Alice");
312+
let verb1 = label_fp("KNOWS");
313+
let tgt1 = label_fp("Rust");
314+
let verb2 = label_fp("ENABLES");
315+
let tgt2 = label_fp("CAM");
291316

292317
let edge1 = SpoBuilder::build_edge(
293318
dn_hash("e1"), &src, &verb1, &tgt1,

src/graph/spo/mod.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ pub mod store;
1717
pub mod builder;
1818

1919
// Re-exports
20-
pub use sparse::{SparseContainer, SpoError, AxisDescriptors, pack_axes, unpack_axes};
20+
pub use sparse::{SparseContainer, SpoError, AxisDescriptors, pack_axes, unpack_axes, BITMAP_WORDS};
2121
pub use scent::NibbleScent;
22-
pub use store::{SpoStore, QueryHit, QueryAxis};
22+
pub use store::{SpoStore, QueryHit, QueryAxis, TruthGate};
2323
pub use builder::{SpoBuilder, label_fp, dn_hash};

0 commit comments

Comments
 (0)