|
| 1 | +# SPO 3D: Three-Axis Content-Addressable Graph |
| 2 | + |
| 3 | +**Status:** Contract-ready. Implementation pending. |
| 4 | +**Date:** 2026-02-20 |
| 5 | +**Crate:** `ladybug-rs` → `src/graph/spo/` |
| 6 | +**Contract:** `crates/ladybug-contract/src/` (geometry, scent, spo_record extensions) |
| 7 | + |
| 8 | +--- |
| 9 | + |
| 10 | +## 1. PROBLEM |
| 11 | + |
| 12 | +CogRecord stores one content Container (1KB). Querying "who knows Ada?" requires scanning ALL records and testing each content fingerprint. No structural axis separation means forward, reverse, and relation queries all hit the same data. |
| 13 | + |
| 14 | +The existing `ContainerGeometry::Xyz` links 3 CogRecords via DN tree (what/where/how). This works but requires 3 separate Redis GETs and DN tree traversal to reconstitute. |
| 15 | + |
| 16 | +## 2. SOLUTION: SPO Geometry |
| 17 | + |
| 18 | +A new `ContainerGeometry::Spo` that uses **sparse containers** within a single 2KB CogRecord envelope. Three axes — Subject (X), Predicate (Y), Object (Z) — encoded as bitmap + non-zero words, co-located in one record. |
| 19 | + |
| 20 | +```text |
| 21 | +┌──────────────────────────────────────────────────────────┐ |
| 22 | +│ CogRecord (ContainerGeometry::Spo) │ |
| 23 | +│ │ |
| 24 | +│ meta: Container (1024 bytes) │ |
| 25 | +│ W0 DN address │ |
| 26 | +│ W1 type | geometry=Spo(6) | flags │ |
| 27 | +│ W2-3 timestamps, labels │ |
| 28 | +│ W4-7 NARS truth (freq, conf, pos_ev, neg_ev) │ |
| 29 | +│ W8-11 DN tree (parent, child, next_sib, prev_sib) │ |
| 30 | +│ W12-17 Scent (48 bytes: 3×16 nibble histograms) │ |
| 31 | +│ W18-33 Inline edge index (64 slots) │ |
| 32 | +│ W34-39 Sparse axis descriptors (bitmap offsets) │ |
| 33 | +│ W40-47 Bloom filter │ |
| 34 | +│ W48-55 Graph metrics │ |
| 35 | +│ W56-63 Qualia │ |
| 36 | +│ W64-79 Rung/RL history │ |
| 37 | +│ W80-95 Representation descriptor │ |
| 38 | +│ W96-111 Adjacency CSR │ |
| 39 | +│ W112-125 Reserved │ |
| 40 | +│ W126-127 Checksum + version │ |
| 41 | +│ │ |
| 42 | +│ content: Container (1024 bytes) — packed sparse axes │ |
| 43 | +│ [0..2] X bitmap (128 bits = 2 u64) │ |
| 44 | +│ [2..N] X non-zero words │ |
| 45 | +│ [N..N+2] Y bitmap │ |
| 46 | +│ [N+2..M] Y non-zero words │ |
| 47 | +│ [M..M+2] Z bitmap │ |
| 48 | +│ [M+2..K] Z non-zero words │ |
| 49 | +│ [K..128] padding / overflow │ |
| 50 | +│ │ |
| 51 | +│ Total: 2048 bytes (same as Cam geometry) │ |
| 52 | +└──────────────────────────────────────────────────────────┘ |
| 53 | +``` |
| 54 | + |
| 55 | +### Why Sparse Containers |
| 56 | + |
| 57 | +At 30% density (typical for real-world content): |
| 58 | +- Dense axis: 128 words = 1024 bytes |
| 59 | +- Sparse axis: 2 words bitmap + ~38 non-zero words = 320 bytes |
| 60 | +- Three sparse axes: 960 bytes ← fits in one content Container |
| 61 | + |
| 62 | +Three axes in one record. One Redis GET. Same 2KB envelope. |
| 63 | + |
| 64 | +## 3. KEY INSIGHT: Z→X CAUSAL CHAIN CORRELATION |
| 65 | + |
| 66 | +When Record A's Z axis (Object) resonates with Record B's X axis (Subject), a causal link exists: |
| 67 | + |
| 68 | +``` |
| 69 | +Record A: X(Jan) → Y(KNOWS) → Z(Rust) |
| 70 | +Record B: X(Rust) → Y(ENABLES) → Z(CAM) |
| 71 | +
|
| 72 | +hamming(A.z_dense, B.x_dense) ≈ 0 → A causally feeds B |
| 73 | +``` |
| 74 | + |
| 75 | +This is not a JOIN — it's a resonance test. The Hamming distance between Z₁ and X₂ IS the causal coherence score. The chain is valid iff each Z→X handoff resonates. |
| 76 | + |
| 77 | +### Meta-Awareness Stacking (Piaget Development) |
| 78 | + |
| 79 | +Each level's Object becomes the next level's Subject: |
| 80 | + |
| 81 | +``` |
| 82 | +Level 0: X(body) → Y(acts_on) → Z(world) |
| 83 | +Level 1: X(world) → Y(represented) → Z(symbols) |
| 84 | +Level 2: X(symbols) → Y(operate_on) → Z(logic) |
| 85 | +Level 3: X(logic) → Y(reflects_on) → Z(abstraction) |
| 86 | +Level 4: X(abstraction) → Y(aware_of) → Z(awareness) |
| 87 | +``` |
| 88 | + |
| 89 | +The meta-record observing a chain gets its own scent. The system recognizes its own epiphanies by their nibble histogram signature. The BUNDLE of all meta-levels should CONVERGE back toward the original content — this is the testable tsunami prediction. |
| 90 | + |
| 91 | +## 4. WHAT CHANGES |
| 92 | + |
| 93 | +### Contract Crate (`crates/ladybug-contract/`) |
| 94 | + |
| 95 | +| File | Change | |
| 96 | +|------|--------| |
| 97 | +| `geometry.rs` | Add `Spo = 6` variant | |
| 98 | +| `container.rs` | Add `SparseAxes` packed encoding within Container | |
| 99 | +| `scent.rs` (NEW) | 48-byte nibble histogram (`NibbleScent`) | |
| 100 | +| `spo_record.rs` (NEW) | `SpoView` / `SpoViewMut` — zero-copy axis access | |
| 101 | + |
| 102 | +### Implementation (`src/graph/spo/`) |
| 103 | + |
| 104 | +| File | Purpose | |
| 105 | +|------|---------| |
| 106 | +| `mod.rs` | Module root, re-exports | |
| 107 | +| `sparse.rs` | `SparseContainer` type + bitmap ops | |
| 108 | +| `axes.rs` | X/Y/Z axis construction (build_node, build_edge) | |
| 109 | +| `store.rs` | `SpoStore` with three-axis scanning | |
| 110 | +| `chain.rs` | Causal chain discovery (Z→X correlation) | |
| 111 | +| `tests.rs` | 6 ironclad tests | |
| 112 | + |
| 113 | +### What DOES NOT Change |
| 114 | + |
| 115 | +- `Container` type (128×u64, 8192 bits, 1KB) |
| 116 | +- `CogRecord` struct (meta + content = 2KB) |
| 117 | +- 5 RISC ops (BIND, BUNDLE, MATCH, PERMUTE, STORE/SCAN) |
| 118 | +- Codebook (4096 entries, deterministic generation) |
| 119 | +- Existing geometries (Cam, Xyz, Bridge, Extended, Chunked, Tree) |
| 120 | +- MetaView word layout (W0-W127) — we use reserved words |
| 121 | +- NARS truth value type and inference functions |
| 122 | +- All existing tests (1,267+) |
| 123 | + |
| 124 | +## 5. CONTRACTS |
| 125 | + |
| 126 | +See: `CONTRACTS.md` in this directory. |
| 127 | + |
| 128 | +## 6. SCHEMA |
| 129 | + |
| 130 | +See: `SCHEMA.md` in this directory. |
| 131 | + |
| 132 | +## 7. IMPLEMENTATION PHASES |
| 133 | + |
| 134 | +### Phase 1: Contract Types (Day 1) |
| 135 | +- Add `ContainerGeometry::Spo = 6` |
| 136 | +- Add `NibbleScent` (48-byte histogram) |
| 137 | +- Add `SparseAxes` (packed 3-axis encoding within Container) |
| 138 | +- Add `SpoView` / `SpoViewMut` (zero-copy axis access) |
| 139 | +- Tests: round-trip, packing invariants |
| 140 | + |
| 141 | +### Phase 2: Sparse Container (Day 1-2) |
| 142 | +- `SparseContainer` with bitmap + non-zero words |
| 143 | +- `to_dense()` / `from_dense()` / `hamming_sparse()` / `bind_sparse()` |
| 144 | +- Pack/unpack 3 sparse axes into one Container |
| 145 | +- Tests: density invariants, hamming equivalence |
| 146 | + |
| 147 | +### Phase 3: Axis Construction (Day 2-3) |
| 148 | +- `build_node(dn, labels, properties) → CogRecord` |
| 149 | +- `build_edge(dn, src_fp, verb, tgt_fp, nars) → CogRecord` |
| 150 | +- Scent computation: nibble histogram per axis |
| 151 | +- Tests: node round-trip, edge encoding |
| 152 | + |
| 153 | +### Phase 4: SPO Store (Day 3-4) |
| 154 | +- `SpoStore` wrapping `HashMap<u64, CogRecord>` |
| 155 | +- `query_forward(src_fp, verb_fp) → Vec<(u64, u32)>` — scan X+Y, return Z matches |
| 156 | +- `query_reverse(tgt_fp, verb_fp) → Vec<(u64, u32)>` — scan Z+Y, return X matches |
| 157 | +- `query_relation(src_fp, tgt_fp) → Vec<(u64, u32)>` — scan X+Z, return Y matches |
| 158 | +- Tests: forward, reverse, relation queries |
| 159 | + |
| 160 | +### Phase 5: Causal Chain (Day 4-5) |
| 161 | +- `causal_successors(record, radius) → Vec<(u64, u32)>` — Z→X scan |
| 162 | +- `causal_predecessors(record, radius) → Vec<(u64, u32)>` — X→Z scan |
| 163 | +- `chain_coherence(chain) → f32` — product of link coherences |
| 164 | +- Meta-awareness record construction |
| 165 | +- NARS truth propagation along chains |
| 166 | +- Tests: chain coherence, meta convergence |
| 167 | + |
| 168 | +### Phase 6: Lance Integration (Day 5+) |
| 169 | +- Columnar schema with per-axis columns |
| 170 | +- Sort key: (dn_prefix, scent_x, scent_y) |
| 171 | +- XOR delta compression within sorted groups |
| 172 | +- Production store replacing BTreeMap |
| 173 | + |
| 174 | +## 8. DECISION LOG |
| 175 | + |
| 176 | +| # | Decision | Rationale | |
| 177 | +|---|----------|-----------| |
| 178 | +| 1 | `Spo = 6` in ContainerGeometry | Natural extension, doesn't break existing variants | |
| 179 | +| 2 | Sparse axes packed in content Container | One Redis GET, same 2KB envelope | |
| 180 | +| 3 | 48-byte nibble histogram replaces 5-byte XOR-fold for SPO | Per-axis type discrimination, no structure loss | |
| 181 | +| 4 | Meta stays dense at W0-W127 | Identity/NARS/DN need fixed O(1) offsets | |
| 182 | +| 5 | BTreeMap for POC, LanceDB for production | Prove correctness first, optimize second | |
| 183 | +| 6 | Z→X Hamming distance = causal coherence | No explicit linking needed, geometry IS the test | |
| 184 | +| 7 | Meta-awareness as recursive SPO records | Epiphanies stack as Z_{n} → X_{n+1} chains | |
| 185 | +| 8 | Codebook slots 0-4095 unchanged | Instruction set is immutable | |
0 commit comments