diff --git a/.claude/board/LATEST_STATE.md b/.claude/board/LATEST_STATE.md index 29e7ec78a..5d1b5eedc 100644 --- a/.claude/board/LATEST_STATE.md +++ b/.claude/board/LATEST_STATE.md @@ -1,3 +1,7 @@ +## 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`. + ## 2026-10-08 — RBAC hot-plug socket on branch `ccr-0455e606-wmtsor` (unmerged) - Contract delta (additive): new module `contract::rbac_plug::{RbacPlug, RbacAuthority, RbacBinding, RbacDrift, ActorSource, PluggedRbac, verify_concepts_against_mirror}`. Entry: `entries/2026-10-08-rbac-hotplug-socket.md`. diff --git a/.claude/board/entries/2026-10-08-hhtl-nars-moore-value-tenants.md b/.claude/board/entries/2026-10-08-hhtl-nars-moore-value-tenants.md new file mode 100644 index 000000000..6bbfb9ba0 --- /dev/null +++ b/.claude/board/entries/2026-10-08-hhtl-nars-moore-value-tenants.md @@ -0,0 +1,72 @@ +# 2026-10-08 — HHTL / NARS / Moore value tenants (ordinals 18–20) + +**Status:** physical layout RATIFIED and TEST-PINNED; MooreNars16 semantics +CANDIDATE. Branch `ccr-b2e415d9-4jfvyk-tenants`, commit `28ec7c0`. + +## What landed + +Three append-only value tenants after `Register1`. Layout-preserving: `NodeRow` +stays 512 B, tenants 0..17 and every CE64 field are unchanged, no +`ENVELOPE_LAYOUT_VERSION` bump. + +| ord | tenant | kind | row bytes | slab bytes | +|---|---|---|---|---| +| 18 | `Nars16x8` | U16 × 8, LE | [284,300) | [252,268) | +| 19 | `MoorePalettePairs` | U8 × 16 | [300,316) | [268,284) | +| 20 | `MooreNars16` | U16 × 8, LE | [316,332) | [284,300) | + +- Lane `i` = Moore slot `i`, order `NW,N,NE,W,E,SW,S,SE` (the probes' order). +- Pair byte `2i` = first Palette256 operand, `2i+1` = second; the law address + is `(first << 8) | second`, the #1336 `PairAddress` orientation. +- `MooreNars16` = `Pearl3 | Energy4 | Plasticity3 | Polarity1 | Epi5` + (bits 0..2, 3..6, 7..9, 10, 11..15), the #1406 reading. +- `ValueSchema::Full` carries all three; no narrower preset does, because no + consumer materialises them yet. Full total 252 -> 300 B. +- `BoardAggregates` reservation re-based 18 -> 21 (canonical_node comment and + the stale `band_reading.rs` note). + +`contract::moore_tenant::{MooreSlot, MooreNars16, DirectionReading, +DirectionRefusal, MooreRefusal, MooreTenantView, MooreTenantMut}`: lane access +goes through `ValueTenant::value_offset()`, no literal offset. + +## Refusals + +- A sign-triple consumer refuses a Moore direction: + `DirectionReading::require_sign_triple` returns `DirectionRefusal` for + `Moore { slot, polarity }`. +- `MooreTenantMut::lift_ce64` refuses mixed witnesses and Epi5 codes 24..31, + checking all eight lanes before writing any. +- The witness is not stored in these bytes; the lift returns it to the owner. + +## Tests (A–L) + +`moore_tenant.rs` (12 tests: A, B, C, D, E, G ×2, H, I, K, L); +`cognitive-shader-driver/tests/moore_palette_pairs_lut.rs` (F); +`lance-graph-planner/tests/moore_tenant_isa_equivalence.rs` (J, 2 tests: +256 tenants × 8 lanes × 5 ops, 0 divergences on every field except the sign +triple; restoring the wrong witness diverges). + +Disable runs, each red then restored: + +| disable | caught by | +|---|---| +| slot order E/W swapped | E, I | +| u16 lanes big-endian | D, I | +| pair bytes read swapped | F | +| witness check removed | I | +| refused lift writes a lane first | I | +| Moore direction passes as sign triple | H | +| energy read as 3 bits | J | +| `ce64_upper` drops the witness | J | +| `Nars16x8` descriptor U8 × 16 | A | +| `Full` drops `MooreNars16` | compile-time assert | + +## OPEN + +- The `Nars16x8` lane reading is not declared anywhere. +- No `SlabReading` variant declares these tenants, so `ResolvedReading` + cannot yet gate a consumer on them the way `bind_register128` does. +- No production writer or reader. The production Simpson detector still takes + bare edges and cannot refuse a Moore direction (pinned in #1406). +- J uses `forward` as it is on `main`; #1407 (unmerged) changes it to return + `Result`, and J follows when that lands. diff --git a/.claude/board/entries/README.md b/.claude/board/entries/README.md index 5bf0f170a..7823cee3e 100644 --- a/.claude/board/entries/README.md +++ b/.claude/board/entries/README.md @@ -25,7 +25,7 @@ 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. -268 entries, 2026-08-06 .. 2026-10-08. +269 entries, 2026-08-06 .. 2026-10-08. | date | entry id | finding | file | |---|---|---|---| @@ -34,6 +34,7 @@ exactly one of them, never both. | 2026-10-08 | `rbac-hotplug-socket` | | [2026-10-08-rbac-hotplug-socket.md](2026-10-08-rbac-hotplug-socket.md) | | 2026-10-08 | `moore-nars16-isa-visible-representation` | | [2026-10-08-moore-nars16-isa-visible-representation.md](2026-10-08-moore-nars16-isa-visible-representation.md) | | 2026-10-08 | `D-MOORE-NARS-0` | | [2026-10-08-moore-nars-0-recipe-learning-gomoku.md](2026-10-08-moore-nars-0-recipe-learning-gomoku.md) | +| 2026-10-08 | `hhtl-nars-moore-value-tenants` | | [2026-10-08-hhtl-nars-moore-value-tenants.md](2026-10-08-hhtl-nars-moore-value-tenants.md) | | 2026-10-08 | `coresearch-ce64-moore-masking-wiring` | | [2026-10-08-coresearch-ce64-moore-masking-wiring.md](2026-10-08-coresearch-ce64-moore-masking-wiring.md) | | 2026-10-07 | `tinker-janus-fold-harvest` | TinkerPop bulk = K (GroupReduce), ONE_BULK = S, GValue pinning = bundle invalidation; JanusGraph slice = OrderedLaneWitness→Range (30–38×); no new V4 op | [2026-10-07-tinker-janus-fold-harvest.md](2026-10-07-tinker-janus-fold-harvest.md) | | 2026-10-07 | `selector-seq-aware-coverage` | | [2026-10-07-selector-seq-aware-coverage.md](2026-10-07-selector-seq-aware-coverage.md) | diff --git a/.claude/v3/soa_layout/le-contract.md b/.claude/v3/soa_layout/le-contract.md index 5a620ad12..672a30260 100644 --- a/.claude/v3/soa_layout/le-contract.md +++ b/.claude/v3/soa_layout/le-contract.md @@ -319,6 +319,25 @@ reading is trusted in any downstream claim. This is the statistical mirror of the field-isolation matrix: layout tests prove bytes don't move; jc pillars prove the READING preserves the semantics. +### §3c Non-facet value tenants: Moore lanes (2026-10-08) + +Not every value tenant is a 4+12 facet. Register0/1 carry no classid, and +the three Moore tenants (`Nars16x8`, `MoorePalettePairs`, `MooreNars16`, +ordinals 18–20) are eight lanes each, one per Moore slot in canonical order +`NW,N,NE,W,E,SW,S,SE`. Their LE contract is the descriptor itself: + +- `Nars16x8` and `MooreNars16` are `ColumnKind::U16 × 8`, little-endian. +- `MoorePalettePairs` is `ColumnKind::U8 × 16`: byte `2i` is the first + Palette256 operand and `2i+1` the second. It is two bytes, not a `u16`. +- Offsets are only in `VALUE_TENANTS`; consumers use + `ValueTenant::value_offset()` (and `moore_tenant::MooreTenantView`), never + a literal. + +The physical layout is RATIFIED. The MooreNars16 field reading is CANDIDATE +and is a different reading from the CE64 register it lifts: direction is +`(slot, polarity)`, not the S/P/O sign triple, and the witness is +tenant-scoped. See `tenants.md` §2 rows 18–20. + ## §4 Relation to the CANON node and the envelope - The CANON node (`CLAUDE.md` § Minimal SoA node, locked 2026-06-13) diff --git a/.claude/v3/soa_layout/tenants.md b/.claude/v3/soa_layout/tenants.md index 1abba9290..080661fa7 100644 --- a/.claude/v3/soa_layout/tenants.md +++ b/.claude/v3/soa_layout/tenants.md @@ -1,4 +1,4 @@ -# Tenant Lanes — the value-slab catalogue (code ground truth 2026-07-02, refreshed 2026-08-22) +# Tenant Lanes — the value-slab catalogue (code ground truth 2026-07-02, refreshed 2026-10-08) > READ BY: v3-envelope-auditor (mandatory), anyone reading/writing a tenant > lane, adding a ValueSchema, or wiring a consumer. Byte atom + payload @@ -33,7 +33,7 @@ every downstream tenant offset. Every tenant below carries its OWN LE contract nested in the envelope's (le-contract.md §3b). -## §2 The 16 value tenants (`ValueTenant`, canonical_node.rs:828-943; `VALUE_TENANTS`, canonical_node.rs:935-1105) +## §2 The 21 value tenants (`ValueTenant` + `VALUE_TENANTS` in canonical_node.rs) Discriminant = FieldMask bit = VALUE_TENANTS index (compile-asserted). Offsets are FULL-ROW; subtract 32 for slab-relative. @@ -56,9 +56,21 @@ Offsets are FULL-ROW; subtract 32 for slab-relative. | 13 | Tekamolo | U8 × 16 | 16 B | [188,204) | TEKAMOLO facet lane — 16 B content-blind V3 4+12 facet (`classid(4) + 6×(u8:u8)`), read G4D3 as `temporal · kausal · modal · lokal` (when/why/how/where circumstance-frame). All-zero = unaddressed | | 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 | +| 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 | + +> **2026-10-08 refresh.** Rows 16–20 added. Register0/Register1 (#1336-era, +> D-LXC-29) had shipped without a row here — the same drift the 2026-07-28 +> note describes. Tenants 18–20 come with `moore_tenant.rs` (slot order, +> zero-copy lane access through `value_offset()`, the MooreNars16 reading +> and its refusals). The BoardAggregates reservation re-bases to **21**. +> Full now spans `[32,332)`: 300 B of 480, 180 B headroom. `ValueSchema::Full`'s `field_mask()` (canonical_node.rs:1132-1162, as read -2026-07-28) lists all 15 tenants 0–14 (Meta … `CausalWitness`), spanning row +2026-07-28) lists all 15 tenants 0–14 (Meta … `CausalWitness`) as of 2026-07-28, spanning row range `[32,220)` — 188 B of the 480-byte value slab consumed (`220 − 32`; `VALUE_SLAB_ROW_OFFSET = 32`, §1) — **292 B headroom, RESERVE-DON'T-RECLAIM** (compile-asserted ≤ 480, canonical_node.rs:1197). @@ -85,7 +97,7 @@ range `[32,220)` — 188 B of the 480-byte value slab consumed (`220 − 32`; | Bootstrap = 0 (default) | none (FieldMask::EMPTY) | zero-fallback ladder | | Cognitive = 1 | Meta, Qualia, Fingerprint, Energy, Plasticity, EntityType, Kanban (7) | thinking rows | | Compressed = 2 | Fingerprint, HelixResidue, TurbovecResidue, EntityType (4) | baked/search rows (q2 bakes) | -| Full = 3 | all 15 tenants 0–14 (Meta … `CausalWitness`) | superset; count compile-asserted `== VALUE_TENANTS.len()` | +| Full = 3 | all 21 tenants 0–20 (Meta … `MooreNars16`) | superset; count compile-asserted `== VALUE_TENANTS.len()`. Tenants 15–20 are in Full only | ## §4 The classid → tenant resolution (ReadMode registry) diff --git a/crates/cognitive-shader-driver/tests/moore_palette_pairs_lut.rs b/crates/cognitive-shader-driver/tests/moore_palette_pairs_lut.rs new file mode 100644 index 000000000..d77295440 --- /dev/null +++ b/crates/cognitive-shader-driver/tests/moore_palette_pairs_lut.rs @@ -0,0 +1,80 @@ +//! Test F: the `MoorePalettePairs` tenant addresses the #1336 Palette256 law +//! the same way `PairAddress` does. +//! +//! Byte `2i` of the tenant is the first operand and byte `2i + 1` the second, +//! so the law entry for slot `i` is `lut[(first << 8) | second]`. The test +//! reads each pair through the contract's tenant view and checks it against +//! the shipped `PaletteLut::at` and `Quad8::palette_pairs` on the same bytes. + +use cognitive_shader_driver::palette_perturbation::{ + PairAddress, PaletteLut, PaletteState, PALETTE_LUT_LEN, +}; +use cognitive_shader_driver::quad8::Quad8; +use lance_graph_contract::canonical_node::{ValueTenant, VALUE_SLAB_LEN}; +use lance_graph_contract::moore_tenant::{MooreSlot, MooreTenantMut, MooreTenantView}; + +/// An asymmetric law, so swapping the operands changes the answer. +fn law() -> Box<[u8; PALETTE_LUT_LEN]> { + let mut t = vec![0u8; PALETTE_LUT_LEN].into_boxed_slice(); + for a in 0..256usize { + for b in 0..256usize { + t[a * 256 + b] = (a * 31 + b * 7 + (a ^ b)) as u8; + } + } + t.try_into().expect("64 KiB") +} + +fn next(x: &mut u64) -> u8 { + *x = x + .wrapping_mul(6364136223846793005) + .wrapping_add(1442695040888963407); + (*x >> 56) as u8 +} + +#[test] +fn tenant_pairs_address_the_lut_like_pair_address() { + let entries = law(); + let lut = PaletteLut::new(&entries); + let mut seed = 0x1336u64; + let mut swapped_differs = 0; + for _ in 0..512 { + let mut slab = [0u8; VALUE_SLAB_LEN]; + let mut m = MooreTenantMut::new(&mut slab); + for slot in MooreSlot::ALL { + m.set_palette_pair(slot, next(&mut seed), next(&mut seed)); + } + let view = MooreTenantView::new(&slab); + for slot in MooreSlot::ALL { + let (first, second) = view.palette_pair(slot); + let addr = PairAddress::new(PaletteState(first), PaletteState(second)); + assert_eq!(addr.0, (u16::from(first) << 8) | u16::from(second)); + let via_tenant = entries[(usize::from(first) << 8) | usize::from(second)]; + assert_eq!( + lut.at(PaletteState(first), PaletteState(second)).0, + via_tenant + ); + assert_eq!(lut.at_address(addr).0, via_tenant); + if lut.at(PaletteState(second), PaletteState(first)).0 != via_tenant { + swapped_differs += 1; + } + } + // Two adjacent slots' four bytes, read as the #1336 Quad8, give the + // same two addresses the tenant gives. + let base = ValueTenant::MoorePalettePairs.value_offset(); + for k in [0usize, 2, 4, 6] { + let b = &slab[base + 2 * k..base + 2 * k + 4]; + let (p0, p1) = Quad8::from_bytes([b[0], b[1], b[2], b[3]]).palette_pairs(); + let (a0, a1) = ( + view.palette_pair(MooreSlot::ALL[k]), + view.palette_pair(MooreSlot::ALL[k + 1]), + ); + assert_eq!(p0, PairAddress::new(PaletteState(a0.0), PaletteState(a0.1))); + assert_eq!(p1, PairAddress::new(PaletteState(a1.0), PaletteState(a1.1))); + } + } + // Anti-vacuity: the orientation is load-bearing on this law. + assert!( + swapped_differs > 3000, + "swapped orientation differed only {swapped_differs} times" + ); +} diff --git a/crates/lance-graph-contract/src/band_reading.rs b/crates/lance-graph-contract/src/band_reading.rs index 2cfc0abbd..b598f834e 100644 --- a/crates/lance-graph-contract/src/band_reading.rs +++ b/crates/lance-graph-contract/src/band_reading.rs @@ -147,7 +147,7 @@ pub enum BandPresence { /// static-substrate attention lands as an alpha-layer entry, dynamic-substrate /// attention as the row's own Lance-version history — the discriminator names /// the kind either way. The Hole becomes a target here once `HoleV3` lands -/// (blocked on the `BoardAggregates = 15` mint). +/// (blocked on the `BoardAggregates` mint, reserved at discriminant 21). #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)] pub enum WitnessKind { /// No witness reference — grading stands alone. diff --git a/crates/lance-graph-contract/src/canonical_node.rs b/crates/lance-graph-contract/src/canonical_node.rs index f2489777e..d690b0fc2 100644 --- a/crates/lance-graph-contract/src/canonical_node.rs +++ b/crates/lance-graph-contract/src/canonical_node.rs @@ -1096,6 +1096,33 @@ pub enum ValueTenant { /// register, for readings that need more than 128 bits (e.g. bivariate /// statistics). Same contract as [`Register0`](Self::Register0). Register1 = 17, + /// **Nars16x8** — eight little-endian `u16` lanes (16 B), one per + /// [`MooreSlot`](crate::moore_tenant::MooreSlot) in canonical slot order + /// `NW,N,NE,W,E,SW,S,SE`. The physical layout is RATIFIED; the reading of + /// each `u16` is CANDIDATE and is not declared by this tenant. Kept + /// separate from [`MooreNars16`](Self::MooreNars16) by design: the two + /// tenants never share bytes or a reading. Accessors: + /// [`MooreTenantView`](crate::moore_tenant::MooreTenantView). Zero-fallback: + /// all-zero lanes are unasserted. + Nars16x8 = 18, + /// **MoorePalettePairs** — eight `(u8:u8)` Palette256 pairs (16 B), one per + /// Moore slot. Byte `2i` is the pair's FIRST operand and byte `2i + 1` its + /// SECOND, so the pair addresses a 256×256 law table as + /// `(first << 8) | second` (the #1336 `PairAddress` orientation). Two + /// separate bytes, never read as one widened `u16` by this tenant. + /// Zero-fallback: `(0, 0)` is the null pair. + MoorePalettePairs = 19, + /// **MooreNars16** — eight little-endian `u16` lanes (16 B), one per Moore + /// slot, each read as + /// [`MooreNars16`](crate::moore_tenant::MooreNars16): + /// `Pearl3 | Energy4 | Plasticity3 | Polarity1 | Epi5`. A Moore-local + /// reading of the ISA-visible subset of CE64 state: direction is + /// `(slot, polarity)`, never the canonical S/P/O sign triple, and the + /// witness is tenant-scoped (one W for all eight lanes, held by the + /// owner, not in these bytes). Physical layout RATIFIED, semantics + /// CANDIDATE. Zero-fallback: an all-zero lane decodes to the zero state + /// with `Epi5 = 0`. + MooreNars16 = 20, } impl ValueTenant { @@ -1276,6 +1303,33 @@ pub const VALUE_TENANTS: &[ColumnDescriptor] = &[ elems_per_row: 16, row_offset: 268, }, + // ── HHTL / NARS / Moore tenants: three 16 B lanes appended after Register1 + // at [284,300), [300,316) and [316,332) (value-slab [252,268), + // [268,284), [284,300)); additive, reserve-don't-reclaim, + // layout-preserving (Full now ends 332 ≤ 512, NODE_ROW_STRIDE unchanged + // → no ENVELOPE_LAYOUT_VERSION bump). These mints take discriminants + // 18, 19 and 20, so the BoardAggregates reservation RE-BASES to 21 by + // the same ordinal rule as above; its offset stays derived, never a + // literal. The u16 tenants are LE (`ColumnKind::U16`); the pair tenant + // is two separate bytes per slot (`ColumnKind::U8 × 16`). + ColumnDescriptor { + name_id: ValueTenant::Nars16x8 as u16, + kind: ColumnKind::U16, + elems_per_row: 8, + row_offset: 284, + }, + ColumnDescriptor { + name_id: ValueTenant::MoorePalettePairs as u16, + kind: ColumnKind::U8, + elems_per_row: 16, + row_offset: 300, + }, + ColumnDescriptor { + name_id: ValueTenant::MooreNars16 as u16, + kind: ColumnKind::U16, + elems_per_row: 8, + row_offset: 316, + }, ]; // Compile-time canon: VALUE_TENANTS is discriminant-ordered, contiguous within the @@ -1390,6 +1444,11 @@ impl ValueSchema { ValueTenant::EpisodicBasin as u8, ValueTenant::Register0 as u8, ValueTenant::Register1 as u8, + // HHTL / NARS / Moore tenants. Full only: no narrower preset + // has a consumer that materialises them yet. + ValueTenant::Nars16x8 as u8, + ValueTenant::MoorePalettePairs as u8, + ValueTenant::MooreNars16 as u8, ]), } } @@ -2673,8 +2732,8 @@ mod tests { assert!(prev_end <= NODE_ROW_STRIDE); assert_eq!( prev_end - VALUE_SLAB_ROW_OFFSET, - 252, - "current Full carve uses 252 of 480 B (kanban×Rubicon 8 + autopoiesis triangle 3×12=36 + TEKAMOLO facet 16 + CausalWitness facet 16 + episodic-basin rail 32 + Register128 rails 2×16)" + 300, + "current Full carve uses 300 of 480 B (kanban×Rubicon 8 + autopoiesis triangle 3×12=36 + TEKAMOLO facet 16 + CausalWitness facet 16 + episodic-basin rail 32 + Register128 rails 2×16 + HHTL/NARS/Moore tenants 3×16)" ); assert!(prev_end - VALUE_SLAB_ROW_OFFSET <= VALUE_SLAB_LEN); } @@ -2756,11 +2815,12 @@ mod tests { // Cognitive 58 + Kanban 8 = 66 (triangle + TEKAMOLO + CausalWitness + // episodic basin NOT in Cognitive — entity classes keep their carve); // Full 120 + 3×12 triangle + 16 TEKAMOLO facet + 16 CausalWitness facet - // + 32 episodic-basin rail + 2×16 Register128 rails = 252 (all additive — + // + 32 episodic-basin rail + 2×16 Register128 rails + 3×16 HHTL/NARS/ + // Moore tenants = 300 (all additive — // reserve-don't-reclaim, still ≤ 480, stride unchanged). assert_eq!(ValueSchema::Cognitive.tenant_bytes(), 66); assert_eq!(ValueSchema::Compressed.tenant_bytes(), 56); - assert_eq!(ValueSchema::Full.tenant_bytes(), 252); + assert_eq!(ValueSchema::Full.tenant_bytes(), 300); for s in [ ValueSchema::Bootstrap, ValueSchema::Cognitive, @@ -2877,8 +2937,8 @@ mod tests { VALUE_TENANTS.len(), "Full read-mode materialises every value tenant" ); - assert_eq!(rm.value_schema.tenant_bytes(), 252); - // The slab has room (252 ≤ 480) and the choice never grows the stride. + assert_eq!(rm.value_schema.tenant_bytes(), 300); + // The slab has room (300 ≤ 480) and the choice never grows the stride. assert!(rm.value_schema.tenant_bytes() <= VALUE_SLAB_LEN); assert!(rm.is_layout_preserving()); } diff --git a/crates/lance-graph-contract/src/lib.rs b/crates/lance-graph-contract/src/lib.rs index 649bb740e..2118896b8 100644 --- a/crates/lance-graph-contract/src/lib.rs +++ b/crates/lance-graph-contract/src/lib.rs @@ -124,6 +124,9 @@ pub mod legacy_outliers; pub mod literal_graph; pub mod mail; pub mod manifest; +/// The HHTL / NARS / Moore value tenants: slot order, byte access, and the +/// MooreNars16 reading with its direction and witness refusals. +pub mod moore_tenant; pub mod morton8x8; pub mod mul; pub mod nan_projection; diff --git a/crates/lance-graph-contract/src/moore_tenant.rs b/crates/lance-graph-contract/src/moore_tenant.rs new file mode 100644 index 000000000..6d15eca1c --- /dev/null +++ b/crates/lance-graph-contract/src/moore_tenant.rs @@ -0,0 +1,800 @@ +//! The HHTL / NARS / Moore value tenants: byte access and the MooreNars16 reading. +//! +//! Three append-only value tenants of the 480-byte slab, one lane per +//! [`MooreSlot`] in the canonical order `NW, N, NE, W, E, SW, S, SE`: +//! +//! | tenant | lane | reading | +//! |---|---|---| +//! | [`ValueTenant::Nars16x8`] | LE `u16` | CANDIDATE, not declared here | +//! | [`ValueTenant::MoorePalettePairs`] | `(u8:u8)` | Palette256 pair, first operand at byte `2i` | +//! | [`ValueTenant::MooreNars16`] | LE `u16` | [`MooreNars16`] | +//! +//! Every byte position comes from [`ValueTenant::value_offset`]; this module +//! writes no literal offset. The accessors borrow the slab and copy nothing +//! but the lane they return. +//! +//! **Status.** The physical layout (ordinals, widths, byte order, slot order) +//! is RATIFIED. The MooreNars16 field reading is CANDIDATE: it is the +//! representation PR #1406 measured as observationally equivalent to CE64 +//! under the shipped ISA operations, and it carries two refusals that a +//! consumer must honour: +//! +//! - **Direction.** A Moore lane's direction is `(slot, polarity)`. It has no +//! S/P/O sign triple, so [`DirectionReading::require_sign_triple`] refuses it. +//! - **Witness.** One witness covers all eight lanes. It is held by the owner, +//! not stored in these bytes, and [`MooreTenantMut::lift_ce64`] refuses +//! input whose lanes carry different witnesses. +//! +//! This module adds no truth formula and changes no CE64 field. + +use crate::canonical_node::{ValueTenant, VALUE_SLAB_LEN}; +use crate::epistemic_state5::{Epi5Gen, EpistemicState5}; + +/// Number of lanes in each Moore tenant. +pub const MOORE_LANES: usize = 8; + +/// One of the eight Moore neighbours, in canonical lane order. +/// +/// The order is the one the Moore probes use (`moore_plasticity_probe`, +/// `moore_nars16_probe`): row by row from the north-west, skipping the centre. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +#[repr(u8)] +pub enum MooreSlot { + /// `(-1, -1)` + Nw = 0, + /// `(0, -1)` + N = 1, + /// `(1, -1)` + Ne = 2, + /// `(-1, 0)` + W = 3, + /// `(1, 0)` + E = 4, + /// `(-1, 1)` + Sw = 5, + /// `(0, 1)` + S = 6, + /// `(1, 1)` + Se = 7, +} + +impl MooreSlot { + /// Every slot in lane order. `ALL[i] as usize == i`. + pub const ALL: [MooreSlot; MOORE_LANES] = [ + MooreSlot::Nw, + MooreSlot::N, + MooreSlot::Ne, + MooreSlot::W, + MooreSlot::E, + MooreSlot::Sw, + MooreSlot::S, + MooreSlot::Se, + ]; + + /// The slot at lane `index`, or `None` past the eighth lane. + #[must_use] + pub const fn from_index(index: usize) -> Option { + if index < MOORE_LANES { + Some(Self::ALL[index]) + } else { + None + } + } + + /// The lane index of this slot. + #[inline] + #[must_use] + pub const fn index(self) -> usize { + self as usize + } + + /// The neighbour's `(dx, dy)` offset from the centre, `y` growing south. + #[must_use] + pub const fn offset(self) -> (i8, i8) { + match self { + MooreSlot::Nw => (-1, -1), + MooreSlot::N => (0, -1), + MooreSlot::Ne => (1, -1), + MooreSlot::W => (-1, 0), + MooreSlot::E => (1, 0), + MooreSlot::Sw => (-1, 1), + MooreSlot::S => (0, 1), + MooreSlot::Se => (1, 1), + } + } +} + +/// A Moore-local reading of the ISA-visible CE64 state of one lane. +/// +/// ```text +/// bits 0..2 Pearl3 (CE64 40..42) +/// bits 3..6 Energy4 (CE64 46..49, the raw signed-mantissa nibble) +/// bits 7..9 Plasticity3 (CE64 50..52) +/// bit 10 Polarity1 (Moore-local; no CE64 home) +/// bits 11..15 Epi5 (CE64 59..63; codes 24..31 reserved) +/// ``` +/// +/// CE64 bits 43..45 (the S/P/O sign triple) and 53..58 (the witness) are not +/// here: direction is `(slot, polarity)` and the witness is tenant-scoped. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)] +#[repr(transparent)] +pub struct MooreNars16(pub u16); + +impl MooreNars16 { + const PEARL_SHIFT: u32 = 0; + const ENERGY_SHIFT: u32 = 3; + const PLASTICITY_SHIFT: u32 = 7; + const POLARITY_SHIFT: u32 = 10; + const EPI5_SHIFT: u32 = 11; + + /// Pack the five fields. Each input is masked to its width. + #[must_use] + pub const fn new(pearl: u8, energy: u8, plasticity: u8, polarity: bool, epi5: u8) -> Self { + Self( + ((pearl as u16 & 0x7) << Self::PEARL_SHIFT) + | ((energy as u16 & 0xF) << Self::ENERGY_SHIFT) + | ((plasticity as u16 & 0x7) << Self::PLASTICITY_SHIFT) + | ((polarity as u16) << Self::POLARITY_SHIFT) + | ((epi5 as u16 & 0x1F) << Self::EPI5_SHIFT), + ) + } + + /// Pearl mask, CE64 bits 40..42. + #[must_use] + pub const fn pearl(self) -> u8 { + ((self.0 >> Self::PEARL_SHIFT) & 0x7) as u8 + } + + /// The raw signed-mantissa nibble, CE64 bits 46..49. + #[must_use] + pub const fn energy(self) -> u8 { + ((self.0 >> Self::ENERGY_SHIFT) & 0xF) as u8 + } + + /// Plasticity triad, CE64 bits 50..52. + #[must_use] + pub const fn plasticity(self) -> u8 { + ((self.0 >> Self::PLASTICITY_SHIFT) & 0x7) as u8 + } + + /// `false` = outbound (centre to neighbour), `true` = inbound. + #[must_use] + pub const fn polarity(self) -> bool { + (self.0 >> Self::POLARITY_SHIFT) & 1 != 0 + } + + /// The raw 5-bit epistemic code, CE64 bits 59..63. + #[must_use] + pub const fn epi5_raw(self) -> u8 { + (self.0 >> Self::EPI5_SHIFT) as u8 + } + + /// The epistemic state, refusing the reserved codes 24..31. + /// + /// # Errors + /// + /// [`MooreRefusal::ReservedEpi5`] for a code outside the canonical codebook. + pub const fn epistemic(self, slot: MooreSlot) -> Result { + match EpistemicState5::decode(Epi5Gen::V1, self.epi5_raw()) { + Ok(s) => Ok(s), + Err(_) => Err(MooreRefusal::ReservedEpi5 { + slot, + code: self.epi5_raw(), + }), + } + } + + /// How this lane's direction is read: `(slot, polarity)`. + #[must_use] + pub const fn direction(self, slot: MooreSlot) -> DirectionReading { + DirectionReading::Moore { + slot, + polarity: self.polarity(), + } + } + + /// Read the lane from a CE64 register. Direction (43..45) and witness + /// (53..58) are not read; `polarity` supplies the Moore-local bit. + #[must_use] + pub const fn from_ce64(edge: u64, polarity: bool) -> Self { + Self::new( + ((edge >> 40) & 0x7) as u8, + ((edge >> 46) & 0xF) as u8, + ((edge >> 50) & 0x7) as u8, + polarity, + ((edge >> 59) & 0x1F) as u8, + ) + } + + /// CE64 bits 40..63 for this lane under `witness`. The direction bits + /// (43..45) are zero: the Moore reading carries no sign triple. Bits + /// 0..39 (S/P/O, F, C) are the caller's. + #[must_use] + pub const fn ce64_upper(self, witness: u8) -> u64 { + ((self.pearl() as u64) << 40) + | ((self.energy() as u64) << 46) + | ((self.plasticity() as u64) << 50) + | (((witness & 0x3F) as u64) << 53) + | ((self.epi5_raw() as u64) << 59) + } +} + +/// How a CE64 direction is to be read. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum DirectionReading { + /// Bits 43..45 are the canonical S/P/O sign triple. + SignTriple(u8), + /// The direction is the lane's slot and polarity; there is no sign triple. + Moore { + /// The lane's neighbour. + slot: MooreSlot, + /// `false` = outbound, `true` = inbound. + polarity: bool, + }, +} + +impl DirectionReading { + /// The S/P/O sign triple, for a consumer that reads one. + /// + /// # Errors + /// + /// [`DirectionRefusal`] for a Moore-local reading, which has no sign + /// triple. A sign-triple consumer must not reinterpret it. + pub const fn require_sign_triple(self) -> Result { + match self { + DirectionReading::SignTriple(bits) => Ok(bits & 0x7), + DirectionReading::Moore { slot, polarity } => Err(DirectionRefusal { slot, polarity }), + } + } +} + +/// A sign-triple consumer was handed a Moore-local direction. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct DirectionRefusal { + /// The lane the refused direction came from. + pub slot: MooreSlot, + /// Its polarity. + pub polarity: bool, +} + +/// Why a Moore tenant read or lift was refused. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum MooreRefusal { + /// An Epi5 code outside the canonical codebook (24..31). + ReservedEpi5 { + /// The offending lane. + slot: MooreSlot, + /// The code it held. + code: u8, + }, + /// The lanes of one tenant carry different witnesses. + MixedWitness { + /// The first lane that disagrees with lane 0. + slot: MooreSlot, + /// Lane 0's witness. + expected: u8, + /// That lane's witness. + found: u8, + }, +} + +/// Read access to the three tenants in a value slab. +#[derive(Debug, Clone, Copy)] +pub struct MooreTenantView<'a> { + value: &'a [u8; VALUE_SLAB_LEN], +} + +impl<'a> MooreTenantView<'a> { + /// Borrow a value slab. + #[must_use] + pub const fn new(value: &'a [u8; VALUE_SLAB_LEN]) -> Self { + Self { value } + } + + const fn u16_at(&self, tenant: ValueTenant, slot: MooreSlot) -> u16 { + let at = tenant.value_offset() + 2 * slot.index(); + u16::from_le_bytes([self.value[at], self.value[at + 1]]) + } + + /// The [`ValueTenant::Nars16x8`] lane for `slot`. + #[must_use] + pub const fn nars16(&self, slot: MooreSlot) -> u16 { + self.u16_at(ValueTenant::Nars16x8, slot) + } + + /// The [`ValueTenant::MoorePalettePairs`] pair for `slot`: `(first, second)`. + #[must_use] + pub const fn palette_pair(&self, slot: MooreSlot) -> (u8, u8) { + let at = ValueTenant::MoorePalettePairs.value_offset() + 2 * slot.index(); + (self.value[at], self.value[at + 1]) + } + + /// The [`ValueTenant::MooreNars16`] lane for `slot`. + #[must_use] + pub const fn moore_nars16(&self, slot: MooreSlot) -> MooreNars16 { + MooreNars16(self.u16_at(ValueTenant::MooreNars16, slot)) + } +} + +/// Write access to the three tenants in a value slab. Each setter touches +/// only its own lane's bytes. +#[derive(Debug)] +pub struct MooreTenantMut<'a> { + value: &'a mut [u8; VALUE_SLAB_LEN], +} + +impl<'a> MooreTenantMut<'a> { + /// Borrow a value slab mutably. + pub fn new(value: &'a mut [u8; VALUE_SLAB_LEN]) -> Self { + Self { value } + } + + /// Read access to the same slab. + #[must_use] + pub fn view(&self) -> MooreTenantView<'_> { + MooreTenantView::new(self.value) + } + + fn set_u16(&mut self, tenant: ValueTenant, slot: MooreSlot, v: u16) { + let at = tenant.value_offset() + 2 * slot.index(); + self.value[at..at + 2].copy_from_slice(&v.to_le_bytes()); + } + + /// Write the [`ValueTenant::Nars16x8`] lane for `slot`. + pub fn set_nars16(&mut self, slot: MooreSlot, v: u16) { + self.set_u16(ValueTenant::Nars16x8, slot, v); + } + + /// Write the [`ValueTenant::MoorePalettePairs`] pair for `slot`. + pub fn set_palette_pair(&mut self, slot: MooreSlot, first: u8, second: u8) { + let at = ValueTenant::MoorePalettePairs.value_offset() + 2 * slot.index(); + self.value[at] = first; + self.value[at + 1] = second; + } + + /// Write the [`ValueTenant::MooreNars16`] lane for `slot`. + pub fn set_moore_nars16(&mut self, slot: MooreSlot, lane: MooreNars16) { + self.set_u16(ValueTenant::MooreNars16, slot, lane.0); + } + + /// Project eight CE64 registers into the [`ValueTenant::MooreNars16`] + /// lanes and return their common witness. + /// + /// Everything is checked before anything is written, so a refusal leaves + /// the slab unchanged. + /// + /// # Errors + /// + /// - [`MooreRefusal::MixedWitness`] when the lanes' witnesses differ. + /// - [`MooreRefusal::ReservedEpi5`] when a lane holds Epi5 code 24..31. + pub fn lift_ce64( + &mut self, + edges: &[u64; MOORE_LANES], + polarity: [bool; MOORE_LANES], + ) -> Result { + let witness = ((edges[0] >> 53) & 0x3F) as u8; + let mut lanes = [MooreNars16(0); MOORE_LANES]; + for slot in MooreSlot::ALL { + let edge = edges[slot.index()]; + let w = ((edge >> 53) & 0x3F) as u8; + if w != witness { + return Err(MooreRefusal::MixedWitness { + slot, + expected: witness, + found: w, + }); + } + let lane = MooreNars16::from_ce64(edge, polarity[slot.index()]); + lane.epistemic(slot)?; + lanes[slot.index()] = lane; + } + for slot in MooreSlot::ALL { + self.set_moore_nars16(slot, lanes[slot.index()]); + } + Ok(witness) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::canonical_node::{ValueSchema, VALUE_TENANTS}; + use crate::soa_envelope::ColumnKind; + + const NEW: [ValueTenant; 3] = [ + ValueTenant::Nars16x8, + ValueTenant::MoorePalettePairs, + ValueTenant::MooreNars16, + ]; + + /// A. The three tenants sit at ordinals 18..20, row bytes 284..332, LE + /// u16 / u8 kinds, and in `Full`. + #[test] + fn a_layout() { + let expect = [ + (ValueTenant::Nars16x8, 18, ColumnKind::U16, 8, 284), + (ValueTenant::MoorePalettePairs, 19, ColumnKind::U8, 16, 300), + (ValueTenant::MooreNars16, 20, ColumnKind::U16, 8, 316), + ]; + for (t, ord, kind, elems, row) in expect { + assert_eq!(t as usize, ord); + let c = &VALUE_TENANTS[ord]; + assert_eq!(c.name_id as usize, ord); + assert_eq!(c.kind, kind); + assert_eq!(c.elems_per_row, elems); + assert_eq!(c.row_offset as usize, row); + assert_eq!(t.byte_len(), 16); + assert_eq!(t.value_offset(), row - 32); + assert!(ValueSchema::Full.has(t)); + for p in [ + ValueSchema::Bootstrap, + ValueSchema::Cognitive, + ValueSchema::Compressed, + ] { + assert!(!p.has(t), "{p:?} must not carry {t:?}"); + } + } + assert_eq!(VALUE_TENANTS.len(), 21); + assert_eq!(ValueSchema::Full.tenant_bytes(), 300); + } + + /// B. Tenants 0..17 keep their exact descriptors. + #[test] + fn b_existing_tenants_preserved() { + let pinned: [(ColumnKind, u16, u32); 18] = [ + (ColumnKind::U64, 1, 32), + (ColumnKind::U64, 1, 40), + (ColumnKind::U64, 4, 48), + (ColumnKind::U8, 32, 80), + (ColumnKind::U8, 6, 112), + (ColumnKind::U8, 16, 118), + (ColumnKind::F32, 1, 134), + (ColumnKind::U32, 1, 138), + (ColumnKind::U16, 1, 142), + (ColumnKind::U64, 1, 144), + (ColumnKind::U8, 12, 152), + (ColumnKind::U8, 12, 164), + (ColumnKind::U8, 12, 176), + (ColumnKind::U8, 16, 188), + (ColumnKind::U8, 16, 204), + (ColumnKind::U8, 32, 220), + (ColumnKind::U8, 16, 252), + (ColumnKind::U8, 16, 268), + ]; + for (i, (kind, elems, row)) in pinned.into_iter().enumerate() { + let c = &VALUE_TENANTS[i]; + assert_eq!(c.name_id as usize, i); + assert_eq!( + (c.kind, c.elems_per_row, c.row_offset), + (kind, elems, row), + "tenant {i}" + ); + } + } + + fn patterned() -> [u8; VALUE_SLAB_LEN] { + let mut v = [0u8; VALUE_SLAB_LEN]; + for (i, b) in v.iter_mut().enumerate() { + *b = (i as u8).wrapping_mul(37).wrapping_add(11); + } + v + } + + /// C. Writing one lane changes exactly that lane's two bytes. + #[test] + fn c_new_tenant_isolation() { + for t in NEW { + for slot in MooreSlot::ALL { + let before = patterned(); + let mut after = before; + { + let mut m = MooreTenantMut::new(&mut after); + let pick = before[t.value_offset() + 2 * slot.index()] ^ 0xFF; + match t { + ValueTenant::Nars16x8 => m.set_nars16(slot, u16::from(pick) * 257), + ValueTenant::MoorePalettePairs => m.set_palette_pair(slot, pick, pick), + _ => m.set_moore_nars16(slot, MooreNars16(u16::from(pick) * 257)), + } + } + let lane = t.value_offset() + 2 * slot.index(); + for i in 0..VALUE_SLAB_LEN { + if i == lane || i == lane + 1 { + assert_ne!(before[i], after[i], "{t:?} {slot:?} byte {i} not written"); + } else { + assert_eq!(before[i], after[i], "{t:?} {slot:?} wrote byte {i}"); + } + } + } + } + } + + /// D. Little-endian round trip with the fixed fixtures. + #[test] + fn d_le_round_trip() { + for v in [0x0102u16, 0x1234, 0xABCD, 0xFF00] { + for slot in MooreSlot::ALL { + let mut slab = [0u8; VALUE_SLAB_LEN]; + let mut m = MooreTenantMut::new(&mut slab); + m.set_nars16(slot, v); + m.set_moore_nars16(slot, MooreNars16(v)); + m.set_palette_pair(slot, (v >> 8) as u8, v as u8); + let r = m.view(); + assert_eq!(r.nars16(slot), v); + assert_eq!(r.moore_nars16(slot), MooreNars16(v)); + assert_eq!(r.palette_pair(slot), ((v >> 8) as u8, v as u8)); + let at = |t: ValueTenant| t.value_offset() + 2 * slot.index(); + let le = v.to_le_bytes(); + assert_eq!(slab[at(ValueTenant::Nars16x8)..][..2], le); + assert_eq!(slab[at(ValueTenant::MooreNars16)..][..2], le); + // The pair is two bytes in operand order, NOT a LE u16. + assert_eq!( + slab[at(ValueTenant::MoorePalettePairs)..][..2], + [(v >> 8) as u8, v as u8] + ); + } + } + } + + /// E. Slot identity: lane i is slot i, the offsets are the eight Moore + /// neighbours in row order, and lane i lives at byte 2i of each tenant. + #[test] + fn e_moore_slot_identity() { + let order = [ + (-1, -1), + (0, -1), + (1, -1), + (-1, 0), + (1, 0), + (-1, 1), + (0, 1), + (1, 1), + ]; + for (i, slot) in MooreSlot::ALL.into_iter().enumerate() { + assert_eq!(slot.index(), i); + assert_eq!(MooreSlot::from_index(i), Some(slot)); + assert_eq!(slot.offset(), order[i]); + } + assert_eq!(MooreSlot::from_index(8), None); + for t in NEW { + for slot in MooreSlot::ALL { + let mut slab = [0u8; VALUE_SLAB_LEN]; + let at = t.value_offset() + 2 * slot.index(); + slab[at] = 0x5A; + slab[at + 1] = 0xA5; + let r = MooreTenantView::new(&slab); + for other in MooreSlot::ALL { + let hit = match t { + ValueTenant::Nars16x8 => r.nars16(other) != 0, + ValueTenant::MoorePalettePairs => r.palette_pair(other) != (0, 0), + _ => r.moore_nars16(other).0 != 0, + }; + assert_eq!( + hit, + other == slot, + "{t:?}: byte 2*{} read by {other:?}", + slot.index() + ); + } + } + } + } + + /// G. Exhaustive: every u16 decodes to five fields that re-encode to the + /// same u16, and every field combination round-trips. + #[test] + fn g_exhaustive_moore_nars16_fields() { + for raw in 0..=u16::MAX { + let l = MooreNars16(raw); + let back = MooreNars16::new( + l.pearl(), + l.energy(), + l.plasticity(), + l.polarity(), + l.epi5_raw(), + ); + assert_eq!(back, l, "{raw:#06x}"); + } + let mut seen = 0u32; + for pearl in 0..8u8 { + for energy in 0..16u8 { + for plast in 0..8u8 { + for pol in [false, true] { + for epi in 0..32u8 { + let l = MooreNars16::new(pearl, energy, plast, pol, epi); + assert_eq!( + ( + l.pearl(), + l.energy(), + l.plasticity(), + l.polarity(), + l.epi5_raw() + ), + (pearl, energy, plast, pol, epi) + ); + seen += 1; + } + } + } + } + } + assert_eq!(seen, 1 << 16, "the five fields tile the u16 exactly"); + // Reserved Epi5 codes are refused; declared ones are not. + for epi in 0..32u8 { + let r = MooreNars16::new(0, 0, 0, false, epi).epistemic(MooreSlot::E); + assert_eq!(r.is_err(), epi >= 24, "epi5 {epi}"); + } + } + + /// G (CE64 side). `from_ce64` reads exactly bits 40..42, 46..52 and + /// 59..63, and `ce64_upper` writes them back with W and zero direction. + #[test] + fn g_ce64_bit_mapping() { + for bit in 0..64u32 { + let lane = MooreNars16::from_ce64(1u64 << bit, false); + let read = matches!(bit, 40..=42 | 46..=52 | 59..=63); + assert_eq!(lane.0 != 0, read, "CE64 bit {bit}"); + } + let mut x = 0x9E37_79B9_7F4A_7C15u64; + for _ in 0..4096 { + x = x + .wrapping_mul(6364136223846793005) + .wrapping_add(1442695040888963407); + let w = ((x >> 53) & 0x3F) as u8; + let upper = MooreNars16::from_ce64(x, true).ce64_upper(w); + let keep = !((1u64 << 40) - 1) & !(0b111u64 << 43); + assert_eq!(upper, x & keep); + } + } + + /// H. A Moore direction is refused by a sign-triple consumer; a sign + /// triple is passed through. + #[test] + fn h_direction_refusal() { + for slot in MooreSlot::ALL { + for pol in [false, true] { + let d = MooreNars16::new(0, 0, 0, pol, 0).direction(slot); + assert_eq!( + d, + DirectionReading::Moore { + slot, + polarity: pol + } + ); + assert_eq!( + d.require_sign_triple(), + Err(DirectionRefusal { + slot, + polarity: pol + }) + ); + } + } + for bits in 0..8u8 { + assert_eq!( + DirectionReading::SignTriple(bits).require_sign_triple(), + Ok(bits) + ); + } + } + + fn edge(w: u8, epi: u8, salt: u64) -> u64 { + let base = salt.wrapping_mul(0x9E37_79B9_7F4A_7C15) & !(0x3Fu64 << 53) & !(0x1Fu64 << 59); + base | (u64::from(w & 0x3F) << 53) | (u64::from(epi & 0x1F) << 59) + } + + /// I. Homogeneous witness lifts and returns W; any mixed lane or reserved + /// Epi5 is refused, and a refusal writes nothing. + #[test] + fn i_witness_ownership() { + let pol = [false, true, false, true, true, false, true, false]; + for w in [0u8, 1, 37, 63] { + let edges: [u64; 8] = + core::array::from_fn(|k| edge(w, (k as u8 * 3) % 24, k as u64 + 1)); + let mut slab = [0u8; VALUE_SLAB_LEN]; + let got = MooreTenantMut::new(&mut slab).lift_ce64(&edges, pol); + assert_eq!(got, Ok(w)); + let r = MooreTenantView::new(&slab); + for slot in MooreSlot::ALL { + assert_eq!( + r.moore_nars16(slot), + MooreNars16::from_ce64(edges[slot.index()], pol[slot.index()]) + ); + } + for k in 1..8 { + let mut mixed = edges; + let other = (w + 1) & 0x3F; + mixed[k] = edge(other, 0, 99); + let mut slab = patterned(); + let before = slab; + let got = MooreTenantMut::new(&mut slab).lift_ce64(&mixed, pol); + assert_eq!( + got, + Err(MooreRefusal::MixedWitness { + slot: MooreSlot::ALL[k], + expected: w, + found: other + }) + ); + assert_eq!(slab, before, "a refused lift must not write"); + } + let mut reserved = edges; + reserved[7] = edge(w, 24, 7); + let mut slab = patterned(); + let before = slab; + assert_eq!( + MooreTenantMut::new(&mut slab).lift_ce64(&reserved, pol), + Err(MooreRefusal::ReservedEpi5 { + slot: MooreSlot::Se, + code: 24 + }) + ); + assert_eq!(slab, before); + } + } + + /// K. An all-zero slab reads as zero lanes, null pairs, and a declared + /// (not reserved) Epi5 code. + #[test] + fn k_zero_fallback() { + let slab = [0u8; VALUE_SLAB_LEN]; + let r = MooreTenantView::new(&slab); + for slot in MooreSlot::ALL { + assert_eq!(r.nars16(slot), 0); + assert_eq!(r.palette_pair(slot), (0, 0)); + let l = r.moore_nars16(slot); + assert_eq!(l, MooreNars16::default()); + assert!(l.epistemic(slot).is_ok()); + assert!(!l.polarity()); + } + } + + /// L. Each codec mutation is caught by the exhaustive round trip. Pins + /// that every field width and position is load-bearing. + #[test] + fn l_codec_mutations_are_caught() { + type Enc = fn(u8, u8, u8, bool, u8) -> u16; + let mutants: [(&str, Enc); 5] = [ + ("energy 3 bits", |p, e, pl, po, ep| { + MooreNars16::new(p, e & 7, pl, po, ep).0 + }), + ("plasticity 2 bits", |p, e, pl, po, ep| { + MooreNars16::new(p, e, pl & 3, po, ep).0 + }), + ("epi5 dropped", |p, e, pl, po, _| { + MooreNars16::new(p, e, pl, po, 0).0 + }), + ("polarity over plasticity", |p, e, pl, po, ep| { + MooreNars16::new(p, e, (pl & !1) | u8::from(po), false, ep).0 + }), + ("pearl and plasticity swapped", |p, e, pl, po, ep| { + MooreNars16::new(pl, e, p, po, ep).0 + }), + ]; + for (name, enc) in mutants { + let mut caught = false; + 'all: for p in 0..8u8 { + for e in 0..16u8 { + for pl in 0..8u8 { + for po in [false, true] { + for ep in 0..32u8 { + let l = MooreNars16(enc(p, e, pl, po, ep)); + if ( + l.pearl(), + l.energy(), + l.plasticity(), + l.polarity(), + l.epi5_raw(), + ) != (p, e, pl, po, ep) + { + caught = true; + break 'all; + } + } + } + } + } + } + assert!(caught, "mutation {name} survived the round trip"); + } + } +} diff --git a/crates/lance-graph-planner/tests/moore_tenant_isa_equivalence.rs b/crates/lance-graph-planner/tests/moore_tenant_isa_equivalence.rs new file mode 100644 index 000000000..ac3061352 --- /dev/null +++ b/crates/lance-graph-planner/tests/moore_tenant_isa_equivalence.rs @@ -0,0 +1,111 @@ +//! Test J: ISA observational equivalence through the CANONICAL tenant. +//! +//! #1406 measured this with a probe-local lane type. This test runs the same +//! check through the shipped contract surface: eight CE64 edges are lifted +//! into `ValueTenant::MooreNars16` with `MooreTenantMut::lift_ce64`, read back +//! with `MooreTenantView`, and rebuilt as `payload40 | ce64_upper(witness)`. +//! The shipped ops (`forward` both ways, `learn` both ways, `syllogize`) must +//! give the same result on the rebuilt edge as on the original, on every +//! field except the S/P/O sign triple, which the Moore reading does not carry. + +use causal_edge::edge::CausalEdge64; +use lance_graph_contract::canonical_node::VALUE_SLAB_LEN; +use lance_graph_contract::moore_tenant::{MooreSlot, MooreTenantMut, MooreTenantView}; + +const DIRECTION: u64 = 0b111 << 43; +const W: u64 = 0x3F << 53; +const EPI5: u64 = 0x1F << 59; +const PAYLOAD40: u64 = (1 << 40) - 1; + +struct Rng(u64); +impl Rng { + fn next(&mut self) -> u64 { + self.0 = self.0.wrapping_add(0x9E37_79B9_7F4A_7C15); + let mut z = self.0; + z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9); + z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB); + z ^ (z >> 31) + } +} + +fn canonical(r: &mut Rng, w: u64) -> u64 { + (r.next() & !W & !EPI5) | (w << 53) | ((r.next() % 24) << 59) +} + +fn tables() -> [Box<[u8; 256 * 256]>; 3] { + let mk = |a: usize, b: usize| { + let mut t = vec![0u8; 256 * 256].into_boxed_slice(); + for x in 0..256 { + for y in 0..256 { + t[x * 256 + y] = (x * a + y * b) as u8; + } + } + t.try_into().expect("256*256") + }; + [mk(31, 17), mk(7, 13), mk(11, 29)] +} + +/// Results of the five shipped ops with `x` in each operand position. +fn ops(x: u64, y: u64, t: &[Box<[u8; 256 * 256]>; 3]) -> [u64; 5] { + let (x, y) = (CausalEdge64(x), CausalEdge64(y)); + let mut lx = x; + lx.learn(y, 0); + 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, + lx.0, + ly.0, + x.syllogize(y).expect("chain figure").conclusion.0, + ] +} + +/// Count divergent (lane, op) results over `rounds` random tenants when the +/// rebuilt edge uses `witness_of(true_witness)`. +fn divergences(rounds: usize, witness_of: fn(u8) -> u8) -> usize { + let t = tables(); + let mut r = Rng(0x1406); + let mut diff = 0; + for _ in 0..rounds { + let w = r.next() % 64; + let edges: [u64; 8] = std::array::from_fn(|_| canonical(&mut r, w)); + let pol: [bool; 8] = std::array::from_fn(|_| r.next() & 1 == 1); + let mut slab = [0u8; VALUE_SLAB_LEN]; + let got = MooreTenantMut::new(&mut slab) + .lift_ce64(&edges, pol) + .expect("homogeneous"); + assert_eq!(u64::from(got), w); + let view = MooreTenantView::new(&slab); + for slot in MooreSlot::ALL { + let k = slot.index(); + let lane = view.moore_nars16(slot); + assert_eq!(lane.polarity(), pol[k]); + let rebuilt = (edges[k] & PAYLOAD40) | lane.ce64_upper(witness_of(got)); + // A partner whose S equals this lane's O, so syllogize chains. + let pw = r.next() % 64; + let p = canonical(&mut r, pw); + let partner = (p & !0xFF) | (edges[k] >> 16 & 0xFF); + let a = ops(edges[k], partner, &t); + let b = ops(rebuilt, partner, &t); + for i in 0..5 { + if a[i] & !DIRECTION != b[i] & !DIRECTION { + diff += 1; + } + } + } + } + diff +} + +#[test] +fn j_tenant_lanes_are_isa_observationally_equivalent() { + assert_eq!(divergences(256, |w| w), 0); +} + +/// Anti-vacuity: restoring the wrong witness is visible to the ISA, so the +/// equivalence above is not true of every rebuild. +#[test] +fn j_wrong_witness_is_observable() { + assert!(divergences(64, |w| (w + 1) & 63) > 0); +}