diff --git a/.claude/board/AGENT_LOG.md b/.claude/board/AGENT_LOG.md index bd12da88..c31bd9dc 100644 --- a/.claude/board/AGENT_LOG.md +++ b/.claude/board/AGENT_LOG.md @@ -1,3 +1,106 @@ +## [Main thread / Opus] arcuate connector — the Broca↔Wernicke cable carries signal (E-ARCUATE-CONDUCTION, first fix) + +**Branch:** claude/jolly-cori-clnf9. **Cargo:** deepnsm lib 99 green (+4 `arcuate`) + 4+8+1; `arcuate.rs` default-clippy-clean. User: "okay" → build the connector seam. + +**Shipped:** NEW `crates/deepnsm/src/arcuate.rs` + `lib.rs` mod decl. `Arcuate{MarkovBundler + ContextChain}`: `feed(WindowedSentence)→Option` pushes to the bundler and, on emit, sign-binarizes the projection and **slides** it into the ±5 ring (`fingerprints.remove(0)+push`); `chain()` exposes the ring; `disambiguate(candidates)` delegates to `ContextChain::disambiguate_with` at the focal index. + +**Why:** closes the conduction-aphasia diagnosis IN ISOLATION — `MarkovBundler::push` now has a caller, and the projection flows into the evidence ring. The contract `ContextChain` provides fill + coherence + replay but NO streaming advance — the connector owns the ring-slide (deepnsm-side, via the chain's pub `fingerprints`). + +**Scope/firewall (anti-spaghetti):** separate seam, **NOT** wired into `pipeline.rs`'s live 512-bit `ContextWindow` (coexistence = a distinct decision, deferred). Only `Binary16K` crosses into the contract; no COCA; no new dep (deepnsm already deps contract via `disambiguator_glue`). + +**OQ-ARC-WINDOW (new):** double-windowing — bundler ±radius + chain ±5 → the ring holds windowed-projection fps; per-sentence (radius-0) fps may be preferable. **Next:** the pipeline-coexistence decision; then feed per-sentence projections. + +--- + +## [Main thread / Opus] full language-network map + conduction-aphasia diagnosis (E-ARCUATE-CONDUCTION) + +**Branch:** claude/jolly-cori-clnf9. Design-only (map + diagnosis; no code). User extended Broca/Wernicke/Hippocampus to the full distributed language network (10 landmarks). + +**Captured:** grail doc § "full language network" (region→component table + mapped diagram + honest N/A modality boundary) + EPIPHANIES `E-ARCUATE-CONDUCTION`. + +**Diagnosis (the payoff):** the stack has CONDUCTION APHASIA — `disambiguator_glue` IS the arcuate fasciculus (`Trajectory`→`context_chain`, shipped) but `MarkovBundler::push` is never called by `pipeline.rs` → the cable carries no signal. Production + comprehension intact in isolation; repetition (connecting them) fails. Fix = the next wire: pipeline→push→`Trajectory`→glue→`context_chain`(±5)→comprehension router. + +**Grounded `context_chain` (arcuate target):** `ContextChain{fingerprints: 11-slot ±5 ring, focal@5}`; `disambiguate_with(i, candidates, DisambiguateOpts{kernel, sentinel_fp})` → `DisambiguationResult{winner,margin,escalate_to_llm}`; replay re-scans with each candidate pinned, NARS-coherent branch wins; `sentinel_fp` = the existing deepnsm injection point. + +**Other placements:** PFC = MUL + free-energy + global_context (WIRED planner-side, NOT connected to the language faculty); temporal-semantic = COCA 4096² + DOLCE; angular = `vocabulary` + `nsm_primes`; metaphor = aerial cross-cohort. **N/A (text-only modality boundary, do NOT build):** auditory / motor / supramarginal-phonology. + +**Next:** build the arcuate connector as its OWN seam (owns the `ContextChain` ±5 ring + feeds `MarkovBundler`), offline-testable + firewall-clean — WITHOUT rewriting `pipeline.rs`'s live 512-bit `ContextWindow` (that coexistence is a separate decision; conflating them = spaghetti). + +--- + +## [Main thread / Opus] E-BROCA-WERNICKE-HIPPO — separate projection (Broca) from resolution (Wernicke); router moved off the projection carrier + +**Branch:** claude/jolly-cori-clnf9. **Cargo:** `cargo test --manifest-path crates/deepnsm/Cargo.toml` → lib 95 green (arcs 2 + comprehension 4) + 4+8+1; both files default-clippy-clean (crate bar; pedantic `doc_markdown` doc-prose deferred, consistent with the crate). Autonomous (user: drive it, no pop-ups). + +**User correction (anti-spaghetti):** "Markov bundler should be separate as the projection, while the sentence resolution is literal text comprehension with ambiguity resolution without tokens … Broca/Wernicke/hippocampus." The first slice (`9af7f15`) fused the fact/story router onto `Trajectory` (the projection carrier). Corrected. + +**Refactor:** `arcs.rs` → projection-only (`split_arcs` + `BasinArc`/`LiteralArc`; removed `temporal_energy`/`threads_story`/`landing`). NEW `comprehension.rs` (Wernicke) → `Landing{fact,story}` + `SentenceStructure::{is_temporal,triple_landing,landings}`, reading the **comprehended, tokenless** structure (`temporals: Vec<(usize,u16)>`, per-triple) — NOT the VSA band. `lib.rs` declares both faculties with the boundary in the comment. + +**Capture:** EPIPHANIES `E-BROCA-WERNICKE-HIPPO` (prepend) + grail doc § three faculties. The genuinely-new piece: the `WitnessTable` lifecycle (`spo_fact_ref None→Some→tombstone`) IS hippocampal→neocortical **consolidation** — an aged story crystallises into a DOLCE fact. So fact-landing has two sources: the input fork AND consolidation (±500 story → fact). `OQ-CONSOLIDATION` net-new. + +**Firewall:** Broca+Wernicke = deepnsm (English); Hippocampus+neocortex = downstream/agnostic; only the `Landing{fact,story}` bit crosses (boolean, not COCA). + +--- + +## [Main thread / Opus] E-ENGLISH-BIFURCATES first wire — split_arcs + temporal fact/story router (deepnsm) + +**Branch:** claude/jolly-cori-clnf9. **Commit:** 9af7f15. **Cargo:** `cargo test --manifest-path crates/deepnsm/Cargo.toml` → 94+4+8+1 green (+5 new `arcs`); `arcs.rs` clippy-clean at pedantic+nursery (crate-wide pedantic has pre-existing debt → TD-DEEPNSM-CLIPPY-195). Autonomous (user: "drive it, no pop-ups"; both gating OQs resolved from source, not asked). + +**Shipped:** `crates/deepnsm/src/arcs.rs` + `lib.rs` mod decl. `Trajectory::split_arcs(&[u16]) -> (BasinArc, LiteralArc)` (the language↔meaning duality as typed Rust at the `disambiguator_glue` seam) + `temporal_energy()`/`threads_story(threshold)`/`landing(threshold) -> Landing{fact,story}` (the fact/story router reading the TEMPORAL band [9000..9200)). + +**Two OQs auto-resolved from source (grounded, not deferential):** +- **OQ-ARC-PRODUCER → 16384-dim role-indexed `Trajectory` is canonical** (not the 512-bit `ContextWindow`): it carries the TEMPORAL router band + already bridges to contract `context_chain` (`disambiguator_glue.rs:65`). "Dead" = producer gap (`MarkovBundler::push` uncalled), not wrong-substrate. +- **OQ-ROUTER-SIGNAL → FORK not switch**: fact universal, story additive when temporal. `Landing{fact:true, story:temporal>τ}`. + +**Firewall held:** both arcs English-side; f32 upstream-only (sign-binarized/opaque before the agnostic graph); literals stay as prunable witnesses (prune lifecycle is contract `WitnessTable`, not here). + +**Remaining wires (net-new, not built):** pipeline→`MarkovBundler::push`→`Trajectory` (close the producer gap); ±5→±500 tier; commit routed landings into `EpisodicEdges64`/DOLCE. Promoting probe (English-SPO locality vs #444 98.6%) unrun. Doc: `english-fact-story-bifurcation-grail-v1.md` (§ Session update). + +--- + +## [Main thread / Opus] world-spine capstone — the English-bifurcation grail (fact-landing vs story-arc) synthesized + captured + +**Branch:** claude/jolly-cori-clnf9. **Design-only** (no code; net-new routing is CONJECTURE per user "needs more research"). **Spans:** the basin/literal duality thread → DeepNSM grounding (background agent, 5-point surface map, deepnsm 102 tests green) → the splat-as-literal→basin-resolver reconnection → the user's keystone ("English can become both fact-landings and story-arcs … enough moving parts to create the holy Grail"). + +**Shipped (docs + board, no code):** `.claude/knowledge/english-fact-story-bifurcation-grail-v1.md` (capstone assembly map — 4 moving parts + the temporal router + 3 resolver scales + the E-EPISODIC-CLOSURE three-lifecycle reconciliation + firewall + 3 missing wires + first slice + promoting probe); EPIPHANIES `E-ENGLISH-BIFURCATES` (prepend); this entry. + +**The synthesis:** English SPO bifurcates by `SentenceStructure.temporals` (WIRED, `parser.rs:57-66`, unread) → atemporal=FACT (aerial 10000² splat → DOLCE frozen identity) / temporal=STORY (±5 `context_chain` → `EpisodicEdges64` → `WitnessTable` prune). The splat is the literal→basin resolver (similarity proposes / CAM confirms; jc ρ=0.9973 offline). Maps onto `E-EPISODIC-CLOSURE`'s three structures: FACT→frozen, story-recent→CLAM ±5, story-old→append-index ±500. Firewall held (language upstream, basins agnostic, float offline, 4096-basins≠COCA-4096). + +**DeepNSM grounding (background agent, HIGH conf, file:line-cited):** grammar templates ABSENT (one hardcoded 5-state FSM, not a 200–500 registry); SPO emission WIRED (`SpoTriple{packed:u64}`, 3×12-bit COCA); Markov arc = TWO disconnected mechanisms (512-bit `ContextWindow` LIVE `pipeline.rs:199` / 16384-dim `MarkovBundler` DEAD, `content_fp` test-only); COCA literal/meaning FUSED (one `u16` rank); story-arc/basin ABSENT in deepnsm (contract-side only). The accumulate→prune lifecycle already ships in `WitnessTable` (`spo_fact_ref None→Some→tombstone`); ±5 replay already ships in `context_chain`. + +**OQ slate:** OQ-ARC-PRODUCER (dead-16384-MarkovBundler vs live-512-ContextWindow — which is canonical; blocks wire #1), OQ-WINDOW-500 (tiered vs grown), OQ-ROUTER-SIGNAL (temporals alone, or also FSM tense/aspect — a clause may be fact AND story = fork not switch), OQ-BASIN-COUNT (4096≠COCA, confirmed distinct), OQ-GRAMMAR-TEMPLATES (200–500 net-new, orthogonal). + +**Next (offered, not built):** first wire = `Trajectory::split_arcs → (BasinArc, LiteralArc)` in deepnsm (firewall-safe; gives dead `MarkovBundler` a producer); OR resolve OQ-ARC-PRODUCER first. Probe to promote CONJECTURE→FINDING: temporal-routed English-SPO landing reproduces #444 locality (98.6%) on the fact path. + +--- + +## [Main thread / Opus] episodic-RISC-spine wave — EpisodicEdges64 + ViewAngle (D-EW64-1, D-VIEW-1) + +**Branch:** claude/jolly-cori-clnf9. Autonomous (full authorization, self-resolved). **Cargo:** cargo test -p lance-graph-contract -> 527 green; both files clippy pedantic+nursery clean. + +**Shipped (contract, zero-dep):** D-EW64-1 episodic_edges::{EpisodicEdges64(u64), EdgeRef} (AriGraph episodic edges; 4x[4-bit family|12-bit local]; intra inherited / cross = 4-bit nibble->OGIT palette; identities inherited). D-VIEW-1 view_angle::ViewAngle (4-bit view-schema selector; presence-bitmask-as-attention). Plan: episodic-risc-spine-v1.md. Finding: EPIPHANIES E-EPISODIC-CLOSURE. **Incident (self-resolved):** initial episodic commits (bc6a29f/ac2d9cd) pushed broken (E0432 + E0658 + a garbled-edit duplication cascade); repaired via clean restore+rewrite, gated on 527-green. **CI-gated next:** D-EW64-2 SoA columns, D-STORY-1 CLAM clusterer, D-STORY-2 session index, D-STORY-3 archetypes, D-HORIZON-1 stopping rule. + +--- + + +## [Main thread / Opus] grounding wave (4 agents) → VersionScheduler slice (D-MBX-9-IN) + +**Branch:** claude/jolly-cori-clnf9 (reset onto merged main `b6e3cc6` = #444+#445/lance7). **Spans:** the "wire all loose ends" agent wave — 4 read-only grounding agents → synthesis → first verifiable slice. **Firewall KEPT (user ratified):** EW64+markov_soa is the particle→wave; the old `Vsa16kF32` singleton is hunted, never re-materialized. + +**Cargo:** `cargo test -p lance-graph-contract` → **509 green** (+6 scheduler); `scheduler.rs` clippy-clean (pedantic+nursery). Core/world-spine slices stay CI-gated (no protoc offline). + +**Grounding map (board-vs-code, HIGH confidence):** +- *Reactive seam:* contract-traits-only; no concrete `MailboxSoaOwner` impl; `MailboxSoA` lacks a `phase` column AND still carries the deprecated `cycle` carrier (retire together); OUT/IN halves real but unjoined (`VersionedGraph::versions()`, callcenter `LanceVersionWatcher`); planner `KanbanMove` emit = honest dead-store (`style_strategy.rs:148`). +- *Thinking/JIT:* StyleStrategy L1-3 WIRED, L4 emit deferred (P3b/OQ-11.7); `ExecTarget` = inert tag (no router); JIT cache real, `JitEngine` adapter (D1.1b) Queued; head2head = `a2a_blackboard` has `support[4]`+`dissonance`, no executor. +- *World-spine:* DeepNSM emits SPO English-by-construction (no mode switch — correct); aerial codebook/ontology WIRED standalone; markov_soa WIRED-unverified-offline, NOT code-connected to aerial; keyframe(radix)+delta(CLAM) = design-only (`radix_register`/`DeltaCard` 0 hits); #444 locality PASSED. +- *Hot-path:* `WitnessTable<64>`/`WitnessEntry` shipped; EW64 = 0 code symbols; Hebbian spreader = design (OQ-11.1); A3 `witness_arc` MISSING. **Bindspace hunt: 0 singletons, 12 LEGIT ephemeral bundles, exactly 1 RETIRE (`FingerprintColumns::cycle`, 4 sites).** + +**Shipped — D-MBX-9-IN:** `contract::scheduler::{DatasetVersion, VersionScheduler, NextPhaseScheduler}` (IN-direction dual of `MailboxSoaOwner`; Lance `versions()` tick → next legal `KanbanMove`; read-only, zero-dep, 6 tests). + +**OQ slate raised:** OQ-EW64-LAYOUT, OQ-11.1 (plasticity radius/decay), OQ-11.2 (witness-arc W), OQ-MARKOV-AERIAL, OQ-FANOUT-FREEZE, OQ-HEAD2HEAD-CRIT. OQ-11.6 partly resolved by surreal #32. **Debt:** stale lance pins in board text (cited 4.0.0/6.0.0; now lance 7 via #445) — sweep owed. + +--- + ## [Main thread / Opus + W1/W2 wave] world-spine vision + probe wave + markov_soa SoC + EW64-as-AriGraph **Branch:** claude/jolly-cori-clnf9-worldspine (local, 21 commits ahead of origin/main) | **Spans:** the agnostic-lazy-world-spine + delta-card integration map vision docs; the W1+W2 autoattended wave; the markov_soa SoC re-home; the EW64-as-AriGraph note; the locality probe RUN. diff --git a/.claude/board/EPIPHANIES.md b/.claude/board/EPIPHANIES.md index c72479ca..c8c822a6 100644 --- a/.claude/board/EPIPHANIES.md +++ b/.claude/board/EPIPHANIES.md @@ -1,3 +1,66 @@ +## 2026-05-31 — E-ARCUATE-CONDUCTION — the stack has conduction aphasia: Broca+Wernicke intact, the arcuate cable (disambiguator_glue) carries no signal (the producer gap) — closing it IS the next wire + +**Status:** FINDING (diagnosis, grounded in source). Extends `E-BROCA-WERNICKE-HIPPO` to the full distributed language network (doc § "the full language network"). Names the single highest-value wire. + +**The diagnosis:** `disambiguator_glue` IS the **arcuate fasciculus** — the Broca↔Wernicke cable (`Trajectory` → contract `context_chain`, `disambiguator_glue.rs:65`). It is *shipped*. But `MarkovBundler::push` is never called by `pipeline.rs`, so no `Trajectory` is produced → the cable carries no signal. Broca (projection: `parser`→SPO + `markov_bundle`) and Wernicke (comprehension: `comprehension.rs` + COCA similarity) each work in isolation; only the connection between them is dead. **Clinical signature matches conduction aphasia exactly:** production + comprehension intact, *repetition* (routing production through to comprehension) fails. This is not a missing organ — it is a severed-but-present cable. + +**The fix (next wire):** `pipeline → MarkovBundler::push → Trajectory → disambiguator_glue → context_chain (±5 replay) → comprehension router`. Closes the producer gap (`OQ-ARC-PRODUCER` already resolved the substrate = 16384-dim role-indexed `Trajectory`) AND the ±5 ambiguity-resolution wire in one flow. + +**Other landmarks placed (full map in doc):** PFC = MUL + free-energy gate + global_context (WIRED planner-side, **not connected to the language faculty**); temporal-lobe semantic = COCA 4096² distance + DOLCE; angular gyrus = `vocabulary` + `nsm_primes` (word↔concept; metaphor = aerial cross-cohort). **Modality boundary (honest N/A):** auditory cortex / motor cortex / supramarginal phonology have no counterpart — DeepNSM is text+COCA, not audio. Do NOT build phonology. Cross-ref: `E-BROCA-WERNICKE-HIPPO`, `E-ENGLISH-BIFURCATES`, `disambiguator_glue.rs`, `context_chain.rs`, three-Markovs (#2 = the MarkovBundler wave). + +--- + +## 2026-05-31 — E-BROCA-WERNICKE-HIPPO — the language stack is THREE separable faculties (projection ≠ comprehension ≠ memory); the witness lifecycle IS consolidation (a story aging into a fact) + +**Status:** FINDING (architecture SoC; the faculty separation is enforced in code as of this commit). The consolidation arc (story→fact) within it is CONJECTURE (unbuilt/unmeasured). User-stated 2026-05-31 ("Markov bundler should be separate as the projection, while the sentence resolution is literal text comprehension with ambiguity resolution without tokens … we're sitting on a Broca and Wernicke and hippocampus"). Refines `E-ENGLISH-BIFURCATES`; doc § "the three faculties". + +**Three faculties, never fused:** +- **Broca = projection / syntax:** PoS-FSM → SPO (`parser.rs`) + the role-superposed MarkovBundler **wave** (`markov_bundle.rs`→`Trajectory`); the basin/literal split (`arcs.rs::split_arcs`). *Assembles + projects structure.* +- **Wernicke = comprehension / resolution:** literal text comprehension over the **tokenless** COCA distributional space (4096 ranks + 4096² distance, NOT BPE); ambiguity resolution (±5 = contract `context_chain`); the fact/story router (`comprehension.rs`, reads `SentenceStructure` per-triple). *Resolves meaning.* +- **Hippocampus = episodic memory + consolidation:** the story-arc (`EpisodicEdges64`, ±5→±500) + crystallisation into semantic (neocortex = DOLCE). *Remembers + consolidates.* + +**The spaghetti this corrects (concrete):** the first slice (`9af7f15`) put the fact/story router as a method on `Trajectory` — fusing the Wernicke decision onto the Broca projection carrier. Corrected here: the router moved to `comprehension.rs` reading the **comprehended** `SentenceStructure` (tokenless, per-triple); `Trajectory` keeps only `split_arcs`. Projection and resolution never share a carrier. deepnsm lib 95 green (arcs 2 + comprehension 4), default-clippy-clean. + +**The consolidation insight (genuinely new — refines the bifurcation):** `WitnessTable`'s `spo_fact_ref None→Some→tombstone` IS hippocampal→neocortical **systems consolidation**. A story-arc witness accumulates in episodic memory, crystallises (`Some` = committed fact), then the episodic witness prunes (tombstone). **An aged story becomes a fact.** So fact-landing has TWO sources: the input fork (atemporal SPO → DOLCE) AND consolidation (a temporal story aged over ±500 → DOLCE). The bifurcation is not only an input switch — it is also a maturation path. `OQ-CONSOLIDATION`: is ±500 the trigger and `None→Some` the crystallisation? (net-new, unbuilt). + +**Firewall:** Broca+Wernicke = deepnsm (English, upstream); Hippocampus+neocortex = downstream/agnostic. Only the `Landing{fact,story}` bit crosses — a boolean, not COCA. Cross-ref: `E-ENGLISH-BIFURCATES`, `E-EPISODIC-CLOSURE` (the three lifecycle structures the hippocampus owns), three-Markovs (#2 hybrid = the MarkovBundler projection wave). + +--- + +## 2026-05-31 — E-ENGLISH-BIFURCATES — English deconstructs into BOTH fact-landings and story-arcs; the temporal marker is the router, the splat is the literal→basin resolver, ±5..500 is the missing wire + +**Status:** CONJECTURE (architecture synthesis; assembles shipped parts + names the missing wires — end-to-end unbuilt/unmeasured). User keystone 2026-05-31 ("English can become both landing as facts and/or as story arc … enough moving parts to create the holy Grail"). Capstone that ties the four world-spine threads into one engine. Doc: `english-fact-story-bifurcation-grail-v1.md`. + +**The keystone — English SPO bifurcates by temporality:** +- **atemporal SPO → FACT-LANDING** → aerial 10000² splat resonance proposes the OWL/DOLCE class → CAM confirms → **frozen identity** (DOLCE/OGIT, never moves). "a dog is a mammal." +- **temporal SPO → STORY-ARC** → ±5 coreference (`context_chain`) threads it → `EpisodicEdges64` basin (`family==0`) → `WitnessTable` accumulate-then-prune. "the dog ran to the park." +- **The router already exists in the sensor:** DeepNSM emits `SentenceStructure{triples, modifiers, negations, TEMPORALS}` (`parser.rs:57-66`). The `temporals` field IS the fact/story switch — WIRED today, read by nothing. Smallest net-new piece. + +**The splat IS the literal→basin resolver (the piece the basin/literal duality was missing).** literal-arc = many COCA pointers (surface, redundant); basin-arc = the one DOLCE class (declared, exact). The 10000² gaussian splat lands a literal cluster on its basin: similarity PROPOSES (float, offline, jc-certified ρ=0.9973 → frozen integer codebook), CAM CONFIRMS. = the **semantic-landing** resolver, distinct from ±5 coreference (local) and head2head (angle). Corrects OQ-RESOLUTION-TREE: the "resolution tree" is THREE resolvers at three scales, not one mechanism. + +**It IS E-EPISODIC-CLOSURE's three lifecycle structures, routed by temporality:** FACT → frozen identity (DOLCE/CAM, never moves); STORY-recent → within-session CLAM (±5, the only mover); STORY-old → cross-session append-index (±500 tail). The bifurcation is not a new structure — it is the rule that picks WHICH of the three an English SPO lands in. So "±5..500" = hot CLAM aging into the cold append-index, the two episodic structures already named. + +**The "missing wire" (user-named): ±5.** DeepNSM emits SPO but its own markov does NOT connect to the contract-side `context_chain` ±5 replay-resolver. Latent defect surfaced: DeepNSM has TWO disconnected, dimensionally-incompatible mechanisms — a 512-bit `ContextWindow` (LIVE, `pipeline.rs:199`) and a 16384-dim `MarkovBundler` (DEAD — no producer; `content_fp` test-only). Three wires open: (1) DeepNSM SPO → `context_chain` ±5; (2) the temporal router (read `temporals`, route, net-new); (3) ±5→±500 tier (hot CLAM → cold append-index, net-new). Already free: `WitnessTable` ships the accumulate→prune lifecycle verbatim; `context_chain` ships the ±5 replay. + +**Firewall HELD (GoBD-with-Rumi guard, end-to-end):** language/COCA stays UPSTREAM in DeepNSM (core has 0 deepnsm dep); both destinations AGNOSTIC (DOLCE class, episodic basin = opaque handles, never `rank:u16`); float lives only offline in jc, online is integer; similarity proposes, identity addresses, never swapped. The ~4096 story-basins ≠ COCA-4096 (independent 12-bit `local`; OQ-BASIN-COUNT confirmed distinct). + +**Honest state:** DeepNSM SPO+temporals WIRED (102 tests); aerial splat→DOLCE SHAPE wired (42 tests; producer in ndarray; end-to-end CONJECTURE); ±5 `context_chain` WIRED contract-side; `EpisodicEdges64`+`WitnessTable` WIRED (#446); routing + 3 wires = net-new. ~5 tested shapes, 3 missing wires, 1 net-new router. **First buildable slice (firewall-safe):** `Trajectory::split_arcs → (BasinArc, LiteralArc)` in deepnsm (names the duality at the `disambiguator_glue` seam; gives the dead `MarkovBundler` a producer; English-side only). **Promoting probe:** does temporal-routed, English-sourced SPO landing reproduce #444 locality (98.6% intra-basin) on the fact path? PASS ⇒ CONJECTURE→FINDING. Cross-ref: `E-EPISODIC-CLOSURE`, `E-ARM-JC-RESOLVES-BOTH-SEAMS`, three-Markovs taxonomy, `splat-codebook-aerial-wikidata-compression.md`, `owl-dolce-hhtl-compartments-aerial-fed.md`. + +--- + +## 2026-05-31 — E-EPISODIC-CLOSURE — the episodic spine closes on three lifecycle-separated structures; compression IS the bounded horizon (not a codec) + +**Status:** FINDING (architecture; converged 2026-05-31, grounded in cognitive-risc/faiss-homology/wikidata-hhtl docs + AriGraph 2407.04363 + #444 probe). + +1. **Three structures by lifecycle:** frozen identity = OGIT palette + CAM (never moves); cross-session index = Lance append-only version log = pseudo-radix (append + immutable pointer => stable addressing, no rebalance); within-session = CLAM over an ephemeral KV (the only thing that moves). +2. **EW64 = AriGraph episodic edges** (not a CE64 lens): basin + multiple edges; intra-basin (~98.6%) inherited ~0 bits; cross-family (~1.4%) = 4-bit nibble into the OGIT-class palette (identities inherited, never on the edge). Shipped: EpisodicEdges64 (D-EW64-1). +3. **Compression IS the bounded horizon:** a research = a free-energy descent resting at the homeostasis floor; awareness (MUL residual-F) = the stop; 256 inputs -> <32 clusters; 4096-64k/KV = shock-absorber headroom. Lever = horizon-shortening (arbiter quality), not a codec. Bitmask doubles as attention mask; ViewAngle (D-VIEW-1) selects the inherited view-schema. + +Firewall held: identity exact (CAM/OGIT), stories flexible (CLAM/discovery), never swapped. Plan: episodic-risc-spine-v1.md. + +--- + + ## 2026-05-31 — FINDING (PROBE RESULT, measured): ontology partition-locality SURVIVES on real ontologies — locality 98.6%, max fan-out 3 (<=16), Q=0.325 ⇒ 16-bit local refs + <=16 family frontier are REAL (on real data, NOT yet Wikidata) **Status:** FINDING (measured, not asserted). Probe `crates/jc/examples/ontology_locality_probe.rs` run on the on-disk ontologies (DOLCE-Ultralite, schema.org, Odoo, PROV-O, QUDT, OWL-Time) — the falsifier for the delta-card/inherited-nothingness addressing claim (probe #1 of `delta-card-addressing-integration-map.md`). PASS. diff --git a/.claude/board/INTEGRATION_PLANS.md b/.claude/board/INTEGRATION_PLANS.md index 82821e30..2566a4b3 100644 --- a/.claude/board/INTEGRATION_PLANS.md +++ b/.claude/board/INTEGRATION_PLANS.md @@ -1,3 +1,10 @@ +## 2026-05-31 — episodic-risc-spine-v1 (the converged episodic addressing spine: 3 lifecycle-separated structures — CAM/OGIT identity, Lance-version pseudo-radix index, CLAM ephemeral KV; EW64 episodic edges with a 4-bit inherited palette; bounded-horizon compression) + +**Status:** ACTIVE — contract slices shipped (D-EW64-1, D-VIEW-1; + VersionScheduler D-MBX-9-IN, head2head D-H2H-1); core/cold CI-gated. **Plan file:** `.claude/plans/episodic-risc-spine-v1.md`. **Finding:** EPIPHANIES E-EPISODIC-CLOSURE. + +--- + + ## 2026-05-31 — odoo-classes-bitmask-render-v1 (the bounded-weekend classes.md fix: `ClassId` discriminator + per-class `FieldPositionTable` + `FieldMask(u64)` + per-class askama templates + Aerial+ shape-family discovery over the 66 OdooEntities) **Status:** PROPOSAL / integration plan, pre-council. Design-spec only, no code. **Plan file:** `.claude/plans/odoo-classes-bitmask-render-v1.md`. **Anchored spec:** `cognitive-risc-classes.md` v0.2 §"Jinja = classes + presence bitmask" + §"NON-DEFERRABLE freeze-time moves" (N1, N3) — bounded-weekend doctrine (line 56-57): *"discriminator + parent-pointer + parent-walking resolution against the existing cache. Full machinery (shape-compiler-to-grid, behavior/traits, SIMD kernels) is explicitly DEFERRED."* diff --git a/.claude/board/LATEST_STATE.md b/.claude/board/LATEST_STATE.md index bf3586dc..30690c4e 100644 --- a/.claude/board/LATEST_STATE.md +++ b/.claude/board/LATEST_STATE.md @@ -42,6 +42,12 @@ ## Current Contract Inventory (lance-graph-contract) +> **2026-05-31 — ADDED (D-EW64-1 + D-VIEW-1, episodic-RISC-spine)**: `episodic_edges::{EpisodicEdges64(u64), EdgeRef{family:u8,local:u16}}` — AriGraph episodic edges, 4x[4-bit family | 12-bit local]: family 0 = intra-basin (inherited, ~98.6% per #444), 1..=15 = cross-family index into the OGIT-class-inherited palette (~1.4%; identities inherited, never on the edge — I-VSA-IDENTITIES). Plus `view_angle::ViewAngle` (4-bit view-schema selector; presence bitmask doubles as attention mask, inherited). Zero-dep; 527 contract lib tests; clippy pedantic+nursery clean. Plan: episodic-risc-spine-v1.md. + +> **2026-05-31 — ADDED (D-H2H-1, head2head superposition winner-select)**: `lance_graph_contract::head2head::{Head2Head (judge), WinnerCriterion (DissonanceMin≈infight / SupportSpread≈Raumgewinn / ConfidenceMax / Tempered=default), CompetitionOutcome}`. `Head2Head::select(&Blackboard) -> Option` picks the winning competing-expert bid over the existing `a2a_blackboard` (confidence/dissonance/support) — pure read + arg-extremum, **no new identity, copies nothing** (select-don't-duplicate, `I-VSA-IDENTITIES`); `margin` = the dark-horse signal. The *selection* half of head2head superposition; parallel-mailbox *execution* is the CI-gated consumer side. Zero-dep; 516 contract lib tests (+7); clippy pedantic+nursery clean. + +> **2026-05-31 — ADDED (D-MBX-9-IN, VersionScheduler contract slice, on `b6e3cc6`/lance7)**: `lance_graph_contract::scheduler::{DatasetVersion(u64), VersionScheduler (trait), NextPhaseScheduler (reference impl)}`. The IN-direction dual of `MailboxSoaOwner` (`E-SUBSTRATE-IS-THE-SCHEDULER`): `on_version(&V, DatasetVersion, ExecTarget) -> Option` lowers a Lance `versions()` tick to the next legal Rubicon `KanbanMove`; `NextPhaseScheduler` is the forward-arc reference (Libet `-550ms` anchor on Planning→CognitiveWork, `None` on absorbing). Read-only over the view (**propose-not-dispose**, R1); composes only existing contract types; zero-dep. 509 contract lib tests (+6); clippy pedantic-clean. CI-gated twin = `LanceVersionScheduler` over `VersionedGraph::versions()` via callcenter `LanceVersionWatcher`. Closes D-MBX-9 IN-direction at the type level (OUT twin + core impl remain CI-gated). + > **2026-05-31 — MERGED (#441, D-CLS arc, merge `a77e119`)**: `lance_graph_contract::class_view::{FieldMask (u64 presence bitmask), ClassView (resolver trait), ClassProjection, RenderRow}` + `ClassView::render_rows` (off-bits-skipped). `ClassId = u16` (reuses `soa_view::class_id`). The class meta-DTO **flies ABOVE the agnostic SoA** — labels/shape/DOLCE resolve LATE from the OGIT cache, nothing semantic in the row (C2 presence≠semantics; N3 stable positions; out-of-range mask bits IGNORED not folded — Codex P2). Ontology side: `class_resolver::RegistryClassView` (impls `ClassView` over the live `OntologyRegistry`, DOLCE via `classify_odoo`) + `odoo_blueprint::class_signature::{StructuralSignature, OdooEntity::signature()/object_view() carrier methods, audit, shape_families, curated_entities, corpus_summary}` (deterministic FNV-1a structural-hash group-by, NOT Aerial-cluster). Zero-dep preserved; extends `ontology::{ObjectView,FieldRef,DisplayTemplate}`, reuses `class_id` (no new newtype). 497 contract + 240 ontology lib tests. D-CLS-{FM,RES,SIG,AUDIT,RENDER} all Shipped. > > **2026-05-30 — PR-in-flight addition** (D-MBX-A6 Phase 2 — Rubicon lifecycle + ExecTarget): `lance_graph_contract::kanban::{ExecTarget (Native/Jit/SurrealQl/Elixir), RubiconTransitionError}` + `KanbanColumn::{next_phases, can_transition_to}` (the Rubicon lifecycle DAG) + `KanbanMove.exec: ExecTarget` field + `MailboxSoaOwner::try_advance_phase()` (checked lifecycle enforcement → `Result`). Zero-dep; `KanbanMove` still ≤16 B; 489 contract lib tests (+4); downstream cargo-check clean. Lifecycle enforcement + planner exec-target are now contract-level invariants. Resolves the #437 deferred exec-target NOTE. Cross-ref D-MBX-A6 / #437. diff --git a/.claude/board/STATUS_BOARD.md b/.claude/board/STATUS_BOARD.md index 2b8dda7b..b8bc12b5 100644 --- a/.claude/board/STATUS_BOARD.md +++ b/.claude/board/STATUS_BOARD.md @@ -561,6 +561,10 @@ Plan path: `.claude/plans/unified-soa-convergence-v1.md`. Handover `.claude/hand | D-MBX-7 | `lance-graph` containers ≡ `MailboxSoA` layout ≡ `ndarray::simd_soa.rs`-aligned (1.4–4.2× SIMD payoff; hard prereq for SurrealDB transparent view) | lance-graph + ndarray | 300 | HIGH | **Queued** | gates on D-MBX-A2 + D-MBX-10 + D-MBX-11 + PR-NDARRAY-MIRI-COMPLETE | | D-MBX-8 | Σ10 commit stamps **t = −550 ms** wall-clock (Libet anchor) in `SigmaTierRouter`; downstream ractor START fires | sigma-tier-router + shader-driver | 120 | MED | **Queued** | gates on D-MBX-A4 + D-MBX-A6 Phase 1 | | D-MBX-9 | Rubicon kanban view in `surrealkv`-on-lance (4 columns: Planning · Cognitive work · Evaluation · Commit·Plan·Prune); ractor lifecycle hooks = kanban moves | surreal_container + ractor | 250 | HIGH | **Queued** | gates on D-MBX-7 + D-MBX-8 + surreal_container BLOCKED(B/C/D) resolved (OQ-11.6) + D-PERSONA-5 | +| D-MBX-9-IN | contract slice of D-MBX-9 IN-direction (`E-SUBSTRATE-IS-THE-SCHEDULER`): `scheduler::{DatasetVersion, VersionScheduler, NextPhaseScheduler}` — Lance `versions()` tick → next legal `KanbanMove`, zero-dep, read-only-over-view (propose-not-dispose) | lance-graph-contract | 130 | LOW | **Shipped (contract)** | 509 lib tests (+6); clippy pedantic-clean; CI-gated twin `D-MBX-9-IN-impl` (LanceVersionScheduler over `VersionedGraph::versions()`) named not written | +| D-H2H-1 | head2head superposition winner-select (item 4, Go infight-vs-Raumgewinn): `head2head::{Head2Head, WinnerCriterion, CompetitionOutcome}` — `select(&Blackboard)` arg-extremum over existing bids, select-not-duplicate | lance-graph-contract | 130 | LOW | **Shipped (contract)** | 516 lib tests (+7); clippy pedantic+nursery clean; parallel-mailbox executor = CI-gated consumer side | +| D-EW64-1 | `episodic_edges::{EpisodicEdges64, EdgeRef}` — AriGraph episodic edges (4x[4b family|12b local]); intra=inherited (~98.6%), cross=4-bit nibble->OGIT-class palette (~1.4%) | lance-graph-contract | 120 | LOW | **Shipped (contract)** | 527 lib tests; clippy clean; SoA columns = D-EW64-2 (CI-gated) | +| D-VIEW-1 | `view_angle::ViewAngle` — 4-bit view-schema selector; presence-bitmask-as-attention (inherited) | lance-graph-contract | 40 | LOW | **Shipped (contract)** | 527 lib tests; clippy clean | | D-MBX-10 | SoA version byte at layout root (`MailboxSoAHeader`); refuse v(N>M) bytes on v(M) reader; field-isolation matrix tests on every column op (`I-LEGACY-API-FEATURE-GATED` discipline) | lance-graph-contract | 100 | HIGH | **Queued** | foundation — should land early in P2; gates on OQ-11.5 | | D-MBX-11 | Lance `=6.0.0 → =6.0.1` patch bump (5 Cargo.toml files identified) | workspace Cargo.toml | 10 | LOW | **Queued (mechanical)** | none — can land in parallel with par-tile prereq | | D-MBX-12 | 8-PR workspace-wide consumer alignment: 12.1 AriGraph · 12.2 Vsa16k audit · 12.4 lance-graph · 12.5 planner · 12.6 shader-driver · 12.7 callcenter · 12.8 ontology audit · 12.9 thinking-styles | per-crate | 800 | per-PR | **Queued (multi-PR)** | sequencing per OQ-11.8: 12.4 → 12.5 → 12.6 → 12.7 → 12.1 → 12.9 → 12.2 → 12.8 | diff --git a/.claude/board/TECH_DEBT.md b/.claude/board/TECH_DEBT.md index ca7952bc..f30a5c39 100644 --- a/.claude/board/TECH_DEBT.md +++ b/.claude/board/TECH_DEBT.md @@ -2619,3 +2619,7 @@ W6 entropy-ledger reframe of `DEEPNSM-NSM-1`. - **Status:** **Open** (2026-05-28). Until resolved, BLOCKED(D) in the workspace root `Cargo.toml` stays open and the transitive uses crates.io ndarray 0.16.1. - **Introduced-by-PR:** N/A (latent since #423's lance 4→6 bump; the 0.16-vs-0.17 gap was always there but invisible without an explicit patch attempt). - **Payoff-estimate:** small if path 1 is taken (a `0.16-fork` branch + patch line) once the AdaWorldAPI patches' relevance to 0.16 is audited; otherwise gated on upstream. + +## TD-DEEPNSM-CLIPPY-195 — 12 pre-existing default-clippy lints in deepnsm (clippy 1.95 bump) + +`cargo clippy --manifest-path crates/deepnsm/Cargo.toml --all-targets -- -D warnings` reports 12 errors across 7 files (codebook 2, encoder 4, similarity 2, disambiguator_glue/nsm_primes/parser/quantum_mode 1 each) — newer lints (`manual_repeat_n`, `uninlined_format_args`, …) that were clean when written and fire only under clippy 1.95.0. Pre-existing (not from the E-ENGLISH-BIFURCATES slice; `arcs.rs` is clean at pedantic+nursery). Tests unaffected (94+4+8+1 green). Fix = a separate mechanical sweep across the 7 files; deliberately NOT bundled into the feature slice (7-file scope creep). Surfaced 2026-05-31. diff --git a/.claude/knowledge/english-fact-story-bifurcation-grail-v1.md b/.claude/knowledge/english-fact-story-bifurcation-grail-v1.md new file mode 100644 index 00000000..03ce1b51 --- /dev/null +++ b/.claude/knowledge/english-fact-story-bifurcation-grail-v1.md @@ -0,0 +1,283 @@ +# The English-Fact-Story Bifurcation — the world-spine capstone ("the holy grail") + +**READ BY:** integration-lead, truth-architect, world-spine work; anyone wiring +DeepNSM → AriGraph → aerial/DOLCE → episodic. +**Status:** CONJECTURE (architecture synthesis, 2026-05-31). Assembles **shipped** +parts (each cited WIRED / CONJECTURE below) and **names the missing wires**. +End-to-end is unbuilt and unmeasured — this doc is the assembly map, not a claim +that the engine runs. +**References (does not duplicate):** `splat-codebook-aerial-wikidata-compression.md`, +`owl-dolce-hhtl-compartments-aerial-fed.md`, `agnostic-lazy-world-spine.md`, +`delta-card-addressing-integration-map.md`, `markov-triplet-query-quorum.md`; +EPIPHANIES `E-ENGLISH-BIFURCATES` (this doc's finding), `E-EPISODIC-CLOSURE`, +`E-ARM-JC-RESOLVES-BOTH-SEAMS`, the three-Markovs taxonomy. + +--- + +## 0. The grail in one sentence + +A deterministic, integer-addressed, **LLM-free** engine that reads English and +lands each clause where it belongs — **atemporal knowledge into the OWL/DOLCE +ontology (FACTS), temporal events into the episodic story-arc (STORIES)** — using +the SAME role-indexed deconstruction, on the SAME agnostic CAM-PQ substrate, with +bounded **±5..500** resolution. It is the concrete form of CLAUDE.md's AGI-as-glove +claim: *parsing a sentence and parsing a thought use the same algebraic slices.* + +--- + +## 1. The keystone — English bifurcates (user, 2026-05-31) + +The same SPO can resolve to **two destinations**, chosen by **temporality**: + +``` + DeepNSM (English sensor — MUST stay English) + COCA-4096 + PoS-FSM → SpoTriple{ s, p, o : 12-bit ranks } + + SentenceStructure{ modifiers, negations, TEMPORALS } ← parser.rs:57-66 + │ + ┌──────────┴──────────┐ router reads `temporals` + atemporal │ │ temporal (WIRED field, today UNREAD) + ▼ ▼ + FACT-LANDING STORY-ARC + aerial 10000² splat ±5 coreference (context_chain) + resonance → DOLCE class → → EpisodicEdges64 basin (family==0) + (similarity proposes, → WitnessTable accumulate → prune + CAM confirms) │ + │ │ + ▼ ▼ + frozen identity CLAM ±5 ──age──► append-index ±500 + (OGIT/CAM, (within-session, (cross-session, + never moves) the only mover) immutable pointer) +``` + +- **"a dog is a mammal"** → atemporal → FACT → DOLCE (frozen identity, never moves). +- **"the dog ran to the park"** → temporal → STORY → episodic arc (±5 → ±500). +- **The router signal already exists in the sensor.** DeepNSM emits + `SentenceStructure{triples, modifiers, negations, **temporals**}` + (`parser.rs:57-66`, WIRED). The `temporals` field *is* the fact/story switch — + built today, read by nothing yet. The router is the smallest net-new piece. + +--- + +## 2. The moving parts (honest inventory) + +| part | role in the grail | status | home (file:line where known) | +|---|---|---|---| +| **DeepNSM** | English sensor: text → SPO + temporals | **WIRED** (102 tests) | `crates/deepnsm`; emit `parser.rs:395-413` → `spo.rs:38` | +| **`temporals` field** | the fact↔story **router signal** | **WIRED but UNREAD** | `parser.rs:57-66` | +| **10000² gaussian splat** | builds the codebook (float, OFFLINE) | **PARTIAL** — producer in ndarray; jc certifies ρ=0.9973 | `ndarray hpc::splat3d` + `jc::ewa_sandwich`, `sigma_codebook_probe` | +| **aerial** | splat→DOLCE proposer = the **literal→basin resolver** | **WIRED shape** (42 tests); end-to-end **CONJECTURE** | `lance-graph-arm-discovery`: `aerial::codebook::{TopKDistance,CodebookDistance}` | +| **OWL/DOLCE cache** | fact-landing target (frozen identity) | **WIRED** projector; #444 locality 98.6% | `aerial::ontology::{OntologyProjector,dolce_id}` | +| **`context_chain` ±5** | coreference / ambiguity resolver (replay) | **WIRED** (contract) | `contract::grammar::context_chain` (`MARKOV_RADIUS`, margin 0.1) | +| **EpisodicEdges64** | story-arc basin (`family==0` intra-basin spine) | **WIRED** (#446) | `contract::episodic_edges` | +| **WitnessTable** | accumulate-then-prune lifecycle | **WIRED** | `contract::witness_table` (`spo_fact_ref None→Some→tombstone`) | +| **±500 tier** | story-old cold tail | **CONJECTURE** (net-new) | (Lance append-index, per `E-EPISODIC-CLOSURE`) | + +**Net:** ~5 tested shapes + 3 missing wires (§6) + 1 net-new router. Most of the +grail already exists and is tested in isolation; the grail is the **wiring**. + +--- + +## 3. The three resolvers, three scales (corrects OQ-RESOLUTION-TREE) + +The basin/literal grounding left "the resolution tree" open. It is **not one +mechanism** — it is three resolvers at three scales: + +| scale | resolver | resolves | status | +|---|---|---|---| +| **local (±5)** | `context_chain` | coreference / pronoun / local ambiguity | **WIRED** | +| **semantic landing** | aerial 10000² splat → DOLCE | *which ontology basin a fragment belongs to* | **SHAPE wired** | +| **angle / story** | `head2head::select` | competing-arc arbitration | **WIRED** | + +The **splat is the literal→basin resolver** — the piece the language↔meaning +duality was missing. literal-arc = many COCA pointers (surface, redundant); +basin-arc = the one DOLCE class (declared, exact). The splat is the resonance +field that lands a literal cluster on its basin: **similarity proposes (float, +offline, jc-certified), CAM confirms (integer, online).** + +--- + +## 4. The bifurcation IS routing over the three lifecycle structures + +It is not a new structure. `E-EPISODIC-CLOSURE` already established three +structures separated by **lifecycle**; the bifurcation is the rule that picks +**which one** an English SPO lands in, by temporality: + +| English clause | destination | lifecycle structure (E-EPISODIC-CLOSURE) | +|---|---|---| +| atemporal **FACT** | DOLCE class | **frozen identity** (OGIT palette + CAM — never moves) | +| recent **STORY** | episodic arc, ±5 | **within-session CLAM** (the only thing that moves) | +| old **STORY** | episodic arc, ±500 | **cross-session append-index** (immutable pointer, pseudo-radix) | + +So "±5..500" is not one window — it is the **hot CLAM (±5) aging into the cold +append-index (±500)**, exactly the two episodic structures already named. + +--- + +## 5. The firewall (why this is safe) — the GoBD-with-Rumi guard, end to end + +The whole engine holds the firewall the board already ratified +(`E-EPISODIC-CLOSURE`, the markov_soa SoC finding): + +1. **Language stays UPSTREAM.** COCA / grammar templates live in DeepNSM only; + core has **0 deepnsm dep** (the dep graph enforces it). DeepNSM scans English, + emits SPO, stops. +2. **Both destinations are AGNOSTIC.** A DOLCE class and an `EpisodicEdges64` + basin are **opaque handles** (`dolce_id:u8`, `EdgeRef{family,local}`, + `spo_fact_ref:u64`) — never `rank:u16`. Storing a COCA rank as a basin witness + would be the GoBD-with-Rumi error (a *language* lens over an *agnostic* graph). +3. **Float lives only offline.** The splat is float resonance = **discovery**; it + runs once in jc (ρ=0.9973), emits a **frozen integer codebook**; aerial's + online path is integer. Similarity proposes, identity addresses, **never + swapped** (`I-VSA-IDENTITIES`). +4. **Two 4096s, kept apart.** The ~4096 story-arc basins are the independent 12-bit + `local` space — **not** the COCA-4096 reused. Coupling basin-count to vocab + would re-introduce language into addressing (OQ-BASIN-COUNT — confirmed distinct). + +--- + +## 6. The missing wires (what is NOT built) + +1. **DeepNSM SPO → `context_chain` ±5** (the user's "missing wire"). DeepNSM's own + markov does **not** reach the contract-side ±5 resolver. Note the latent defect + surfaced by grounding: DeepNSM has **two disconnected** mechanisms — a 512-bit + `ContextWindow` (LIVE, used by `pipeline.rs:199`) and a 16384-dim `MarkovBundler` + (**DEAD** — no production caller, `content_fp` constructed only in tests). They + are dimensionally incompatible (OQ-ARC-PRODUCER). +2. **The temporal router** — read `temporals`, route fact-vs-story. Net-new; the + signal is WIRED, the consumer is not. +3. **The ±5 → ±500 tier** — hot CLAM aging into the cold append-index. Net-new + (likely the `EpisodicEdges64` cross-session column, not a bigger ring). + +What is **already done for free**: the accumulate-then-prune lifecycle the +conjecture wanted ships verbatim in `WitnessTable` +(`spo_fact_ref None→Some→tombstone`); the ±5 replay-resolution ships in +`context_chain`. The grail does not need them invented — only connected. + +--- + +## 7. First buildable slice + the promoting probe + +**Slice (firewall-safe, verifiable offline):** `Trajectory::split_arcs → +(BasinArc, LiteralArc)` in deepnsm. + +```rust +// crates/deepnsm/src/trajectory.rs (or arcs.rs) — zero new dep +pub struct BasinArc(pub Vec); // the semantic spine: ONE role-superposed bundle +pub struct LiteralArc(pub Vec); // the language surface: COCA ranks (prunable later) +impl Trajectory { pub fn split_arcs(&self, literal_ranks: &[u16]) -> (BasinArc, LiteralArc); } +``` + +Proves: (a) "basin = one bundle, literal = many pointers" is realizable from the +existing `Trajectory.fingerprint` with no new substrate; (b) gives the **dead +`MarkovBundler` its first producer shape** (closes the no-producer gap); +(c) names the duality at the existing `disambiguator_glue` seam (today a bare +`&[f32]` + untyped candidate iterator). Stays entirely English-side — the +prune/tombstone lifecycle remains in contract `WitnessTable`. + +**Probe that promotes this CONJECTURE → FINDING:** does **temporal-routed, +English-sourced** SPO landing reproduce the #444 locality result +(98.6% intra-basin, max fan-out 3) on the fact path? If yes, the bifurcation's +fact-leg addressing is real on language-derived data (not just curated ontologies +— the open #444 caveat). If no, the splat→DOLCE landing degrades to mostly-far +pointers and the fact-leg needs rework before wiring. + +--- + +## 8. Open questions + +- **OQ-ARC-PRODUCER** (blocks wire #1): dead 16384-dim `MarkovBundler` vs live + 512-bit `ContextWindow` — which is canonical? They cannot both feed the ±5 seam. +- **OQ-WINDOW-500**: tiered (±5 hot CLAM → ±500 cold append-index) vs a single + grown radius. §4 argues tiered (it reuses the two existing episodic structures). +- **OQ-ROUTER-SIGNAL**: is `temporals` alone the router, or also FSM tense/aspect? + A clause can be **both** (a fact asserted inside a narrative) — does it land + twice (fact AND story), or does one win? The bifurcation may be a *fork*, not a + *switch*. +- **OQ-BASIN-COUNT**: ~4096 story-basins = the independent 12-bit `local`, NOT the + COCA-4096 (firewall). Confirmed distinct; keep them so. +- **OQ-GRAMMAR-TEMPLATES**: the 200–500 discoverable templates have **zero surface** + today (one hardcoded 5-state FSM). Net-new, and orthogonal to the bifurcation — + do not block the grail on it. + +--- + +*This doc is the capstone assembly map. The four threads it ties — +splat/aerial/DOLCE (facts), DeepNSM (English), context_chain (±5), EpisodicEdges64 +(stories) — each have their own doc above. The new claim is only the bifurcation +and its routing onto the three lifecycle structures.* + +--- + +## Session update — 2026-05-31 (first wire shipped, commit 9af7f15) + +Both gating OQs auto-resolved from source; the first slice is built, tested, pushed. + +- **OQ-ARC-PRODUCER → RESOLVED: the 16384-dim role-indexed `Trajectory` is canonical** for the grail seam (not the 512-bit `ContextWindow`). It carries the `TEMPORAL` band `[9000..9200)` that IS the router, and already bridges to contract `context_chain` via `disambiguator_glue.rs:65`. The "dead" status is a *producer* gap (`MarkovBundler::push` uncalled by `pipeline.rs`), not wrong-substrate. The 512-bit ring stays DeepNSM's internal disambiguator. +- **OQ-ROUTER-SIGNAL → RESOLVED: FORK, not switch.** Every SPO relation is a fact-candidate; temporal content *adds* a story-arc ("the dog, which is a mammal, ran" → both). The temporal band is the discriminating signal; the fact leg is universal (commit-policy is downstream). +- **Shipped:** `crates/deepnsm/src/arcs.rs` — `Trajectory::{split_arcs, temporal_energy, threads_story, landing}` + `BasinArc`/`LiteralArc`/`Landing`. 5 tests; deepnsm 94+4+8+1 green; `arcs.rs` clippy-clean (pedantic+nursery). Firewall-safe (English-side, f32 upstream-only, no COCA rank reaches the agnostic graph). +- **Remaining wires (still net-new):** (1) `pipeline.rs` actually producing `Trajectory` (calling `MarkovBundler::push`); (2) the ±5→±500 tier; (3) committing routed landings into contract `EpisodicEdges64` (story) / DOLCE (fact). The promoting probe (English-SPO locality vs #444's 98.6%) is unrun. +- **New debt:** `TD-DEEPNSM-CLIPPY-195`. + +--- + +## Session update — 2026-05-31 (the three faculties: Broca / Wernicke / Hippocampus) + +User correction (anti-spaghetti): *"Markov bundler should be separate as the projection, while the sentence resolution is literal text comprehension with ambiguity resolution without tokens … we're sitting on a Broca and Wernicke and hippocampus."* This is the organizing frame that keeps the parts SEPARATE — each is its own faculty with a clean boundary, and the data flows between them. + +| faculty | brain region | does | home | status | +|---|---|---|---|---| +| **projection / syntax** | **Broca** | PoS-FSM → SPO, then the role-superposed MarkovBundler **wave**; basin/literal split | `parser.rs`, `markov_bundle.rs`, `arcs.rs` (`split_arcs`) | WIRED (split shipped) | +| **comprehension / resolution** | **Wernicke** | literal text comprehension (COCA ranks, **tokenless**); ambiguity resolution (±5); fact/story router, per-triple | `comprehension.rs` (`SentenceStructure::{is_temporal,triple_landing,landings}`); ±5 = contract `context_chain` (unwired) | router WIRED; ±5 ambiguity-resolution wire OPEN | +| **episodic memory + consolidation** | **Hippocampus** | story-arc (±5→±500); aged story **consolidates** into a semantic fact | contract `EpisodicEdges64`, `WitnessTable`; DOLCE = neocortex | WIRED shapes; consolidation arc CONJECTURE | + +**Separation enforced in code (anti-spaghetti):** the fact/story router was initially (commit `9af7f15`) a method on `Trajectory` (the projection carrier) — that fused Wernicke onto Broca. Corrected: routing now reads `SentenceStructure` (the *comprehended*, tokenless structure) in `comprehension.rs`; `Trajectory` keeps only `split_arcs` (projection). Projection ≠ resolution; never the same carrier. + +**The consolidation insight (refines the bifurcation — it's not only an input fork):** the `WitnessTable` lifecycle `spo_fact_ref None→Some→tombstone` IS hippocampal→neocortical **systems consolidation** — a story-arc witness accumulates in episodic memory (hippocampus), crystallises (`Some` = committed), then the episodic witness prunes (tombstone). An **aged story becomes a fact**. So the fact-leg has TWO sources: (1) the input fork (atemporal SPO → DOLCE directly), and (2) consolidation (a temporal story-arc, repeated/aged over ±500, crystallising into a DOLCE fact). `OQ-CONSOLIDATION` (net-new): is the ±500 tail the consolidation trigger, and is crystallisation the `spo_fact_ref None→Some` transition? + +**Tokenless, concretely:** DeepNSM is COCA-word distributional (4096 ranks + the 4096² distance matrix), not BPE/subword. Wernicke comprehension + ambiguity resolution operate over that literal whole-word semantic space — "without tokens" = without a learned subword tokenizer. The firewall is unchanged: Broca+Wernicke live in deepnsm (English); Hippocampus+neocortex are downstream/agnostic; only the `Landing{fact,story}` bit crosses (a boolean, not COCA). + +--- + +## Session update — 2026-05-31 (the full language network, not just three regions) + +User extended the frame from Broca/Wernicke/Hippocampus to the distributed language network. Mapped to the workspace — honest status; **N/A = a real modality boundary, not a gap to fill**: + +| region | function | workspace component | status | +|---|---|---|---| +| **Broca** | speech production, grammar, sentence construction | PoS-FSM→SPO (`parser.rs`) + MarkovBundler wave (`markov_bundle.rs`→`Trajectory`, `arcs.rs`) | WIRED; **producer gap** (`push` uncalled) | +| **Wernicke** | comprehension (spoken+written) | `comprehension.rs` (per-triple resolution) + COCA distributional similarity | router WIRED; **±5 ambiguity wire OPEN** | +| **Hippocampus** | short→long memory; learning facts/events | `EpisodicEdges64` + `WitnessTable` (episodic ±5→±500 + consolidation) | WIRED shapes; consolidation CONJECTURE | +| **Temporal lobe (semantic)** | word meanings; pattern recognition | COCA 4096² distance (`similarity.rs`, lexical) + DOLCE store (consolidated facts = neocortex) | WIRED | +| **Angular gyrus** | reading/writing; words↔concepts; metaphor | `vocabulary.rs` (rank↔concept) + `nsm_primes.rs` (universal primes); metaphor = aerial cross-cohort X→Y | WIRED (vocab/NSM); metaphor CONJECTURE | +| **Prefrontal cortex** | organize thoughts; hold context; select words; suppress irrelevant | MUL (`planner/mul/`: DK/trust/homeostasis/gate) + global_context + free-energy descent + planner orchestration | WIRED planner-side; **not yet connected to the language faculty** | +| **Arcuate fasciculus** | Broca↔Wernicke cable; damage = conduction aphasia | `disambiguator_glue` (`Trajectory`→`context_chain`) | cable SHIPPED; **no signal (producer gap)** | +| **Supramarginal gyrus** | phonology; sound↔language | — | **N/A (text-only; modality boundary)** | +| **Primary auditory cortex** | sound processing | — | **N/A** | +| **Motor cortex** | articulators (speech output) | — | **N/A** | + +``` + Prefrontal Cortex = MUL + free-energy gate + global_context (planner-side, unconnected) + │ +Broca ───────────┼──── Arcuate Fasciculus ────── Wernicke +(parser + │ (disambiguator_glue: (comprehension.rs + + MarkovBundler │ CONDUCTION APHASIA — COCA similarity) + → Trajectory) │ cable shipped, no signal) + │ + Angular Gyrus = vocabulary + nsm_primes (word↔concept) + │ + Temporal Semantic = COCA 4096² distance + DOLCE + │ + Hippocampus = EpisodicEdges64 + WitnessTable (episodic + consolidation) +``` + +**Diagnosis — the stack has CONDUCTION APHASIA.** Broca (projection) and Wernicke (comprehension) each work in isolation, but the arcuate cable carries no signal: `disambiguator_glue` IS the arcuate fasciculus (`Trajectory`→`context_chain`) and is shipped, yet `MarkovBundler::push` is never called by `pipeline.rs` → no `Trajectory` is produced → nothing threads the cable into comprehension. Clinical signature matches exactly: comprehension + production intact, **repetition (connecting them) fails.** The fix names the next wire: `pipeline → MarkovBundler::push → Trajectory → disambiguator_glue → context_chain (±5) → comprehension router`. + +**Honest modality boundary:** auditory cortex / motor cortex / supramarginal (phonology) have NO counterpart — DeepNSM is text + COCA, not audio/speech. Correctly absent; **do not build phonology** (it would be scope creep across a modality the sensor doesn't have). + +--- + +## Session update — 2026-05-31 (arcuate connector shipped — the cable carries signal) + +First increment of the conduction-aphasia fix (`E-ARCUATE-CONDUCTION`): `crates/deepnsm/src/arcuate.rs`. `Arcuate` owns the `MarkovBundler` producer + the ±5 `ContextChain` ring; `feed(sentence)` pushes to the bundler and, on each emitted `Trajectory`, sign-binarizes it and **slides** it into the ring's newest slot; `disambiguate(candidates)` delegates to the now-populated chain. The connector owns the ring-slide the contract leaves to the language side (`ContextChain` has fill + coherence + replay but **no streaming advance**), and gives `MarkovBundler::push` its first caller — so the projection now flows from Broca into Wernicke's window. deepnsm lib 99 green (+4); `arcuate.rs` default-clippy-clean; firewall-clean (only `Binary16K` crosses to the contract; no COCA; no new dep). + +**Still open (deliberately):** (1) wiring `Arcuate` into `pipeline.rs` — coexistence with the live 512-bit `ContextWindow` is a distinct decision, deferred to avoid spaghetti; (2) feeding **per-sentence** fingerprints rather than the bundler's windowed bundle (`OQ-ARC-WINDOW` — double-windowing: bundler ±radius + chain ±5). diff --git a/.claude/plans/episodic-risc-spine-v1.md b/.claude/plans/episodic-risc-spine-v1.md new file mode 100644 index 00000000..b79284b3 --- /dev/null +++ b/.claude/plans/episodic-risc-spine-v1.md @@ -0,0 +1,110 @@ +# Episodic RISC Spine — v1 (the converged episodic addressing architecture) + +> **Authority:** the 2026-05-31 design dialogue (episodic basins+edges, EW64, +> CLAM/pseudo-radix, 4-bit inherited palette, bitmask-as-attention, bounded-horizon +> compression). **Grounds in:** user-supplied `cognitive-risc-{core,classes}.md`, +> `faiss-homology-campq.md`, `wikidata-hhtl-load.md`; the AriGraph paper (arXiv +> 2407.04363); the #444 locality probe (PASS: 98.6% intra-basin, fan-out ≤ 3, Q=0.325). +> **Iron firewalls (non-negotiable):** identity = CAM/OGIT (frozen, exact); stories / +> similarity = discovery (flexible, NEVER addresses). DeepNSM stays English upstream; +> aerial stays a zero-dep proposer. Same triples, two indexes, never swapped. + +## The closure — three structures, by lifecycle (zero overlap) + +| concern | structure | property | home | +|---|---|---|---| +| **frozen identity** (which-one / what-it-is) | OGIT class palette + CAM hash | exact, **never moves** | `lance-graph-ontology` + `cam` | +| **cross-session index** (the session log) | Lance append-only version log | append + immutable pointer = **pseudo-radix** (no rebalance) | `Dataset::versions()` / `DatasetVersion` | +| **within-session experience** (accumulating working set) | CLAM cluster tree over an ephemeral KV | grows + prunes | `ndarray` CLAM (build target) + `surrealkv` | + +Append-ness, clustering, freezing — three jobs, three structures, no overlap. You +don't *build* a radix; append + never-renumber *gives* you one (the Lance version log). + +## EW64 = AriGraph episodic edges (NOT a lens over CausalEdge64) + +- A mailbox(=episode) is a **basin** with **multiple edges**. The **temporal arc is + itself a basin+edges one HHTL level up** (`film › drama › episode{e1 e2 e3 e4}`) — + NOT a scalar prev-pointer (retracts the earlier "prev column"). +- **Sparse common case (~98.6% intra-basin, #444):** an edge is addressed BY its + family — inherited from the row's HHTL/`class_id` path. ~0 extra bits. +- **Cross-family crossover (~1.4%):** a **4-bit nibble** (16 families) indexes the + **OGIT-class-inherited cross-family palette** (a CAM_PQ facet code; codebook = the + OWL closed range). The 16 identities are inherited, NEVER stored on the edge. +- **SoC (the user's iron correction):** temporal (basin one level up) ⊥ witness + (cross-SoA edge) ⊥ frozen identity. **Never stack** — Markov-chained witnesses need + ≥2 pointers to stack; rejected. + +**Encoding — `EpisodicEdges64(u64)` = 4 × `EdgeRef{ family:4b, local:12b }`:** +`family` 0 = intra-basin (the row's own family, inherited — the cheap default); +`1..=15` = cross-family palette index. `local` = 1-based within-family index (12b, +≤4095). 4 edges/word; the within-basin local handles the small basins the probe +measured; the cross-session **episode store is a separate 16-bit Lance-column +pointer** (64k/session), not this word. + +## The bitmask is the discriminator AND the attention mask + +- The class-inherited **presence bitmask** (`class_view::FieldMask`) doubles as the + **attention mask** — "attend to what's present" is structural, NOT the forbidden + per-instance semantics. +- A **`ViewAngle`** (≤16, 4-bit) selects WHICH inherited view-schema attends — the + Quartettkarte "edition" / FAISS-view. A leaf/family can bake in N required default + angles. **Line:** an angle is a *class-inherited* attention pattern, never + instance-private meaning (the moment "angle 3" means something per-row → CISC slide). +- `head2head` (D-H2H-1, shipped) **competes angles**: infight (`DissonanceMin`) vs + Raumgewinn (`SupportSpread`). Within-story meta suffices first. + +## Lifecycle = two-clock + +read a book → **16k–256k hot tombstone-witnesses** in the session ephemeral KV +(`surrealkv`) → CLAM clusters them → **epoch-reset prunes** the transient mass +(core #6; tombstone = forwarding record only) → survivors **distill cold into a +palette256 ranking over 4096 story-arc archetypes** (`bgz17`; the DISCOVERY side — +proposes/ranks, **never the CAM key**) → snapshot = a tagged Lance version +("stories from day xz"). + +## The compression IS the bounded horizon (not a codec) + +A research = a free-energy descent; it **rests at the homeostasis floor** ("call it a +day"). Awareness (MUL / `MetaWord` residual-F) = the stopping rule. 256 inputs → <32 +clusters (locality, #444 fan-out ≤ 3); 4096–64k epiphanies/KV = **shock-absorber +headroom** (core #7), not a target. **Lever = horizon-shortening (proposer + +Rubicon-arbiter quality), not compression.** Cheapest research = the one that knows +soonest it's done. + +## Deliverables + +### Verifiable-now (contract, zero-dep, builds offline) — THIS WAVE +- **D-EW64-1** `episodic_edges::{EpisodicEdges64, EdgeRef}` — 4×[4b family|12b local]; + intra/cross; palette-inherited. **(building now)** +- **D-VIEW-1** `view_angle::ViewAngle` — 4-bit view-schema selector + the + presence-bitmask-as-attention doctrine. **(building now)** +- *(shipped this session)* D-MBX-9-IN `VersionScheduler` = the **pseudo-radix reader** + (`DatasetVersion` = the fixed session pointer). D-H2H-1 `head2head` = the story-meta. + +### CI-gated core (planned; no offline build — `protoc` absent in sandbox) +- **D-EW64-2** wire `EpisodicEdges64` + basin + temporal-arc-basin as `MailboxSoA` + columns (`cognitive-shader-driver`). +- **D-STORY-1** CLAM-**as-clusterer** over the ephemeral session KV (wire `ndarray` + CLAM; the current `lance-graph` CLAM is a probe-only *measurer*). +- **D-STORY-2** pseudo-radix session index = Lance version log + tagged snapshots; + `VersionScheduler` drives it. +- **D-STORY-3** palette256-over-4096 archetype ranking (`bgz17`, standalone-verifiable) + — discovery, not identity. +- **D-HORIZON-1** MUL residual-F = the snapshot / "call-it-a-day" trigger. + +### Resolved-by-decision (self-resolved per the dialogue) +- **4096 archetypes:** frozen-OGIT (CAM) vs discovered-Aerial+ (proposer) → user chose + **flexible/ephemeral** (CLAM/KV/snapshot) ⇒ **discovery side, never the CAM key**. +- **cross-family palette source:** per-`class_id`, `owl:disjointWith`-derived + (collision-free / purely additive, per `wikidata-hhtl-load`). +- **temporal:** a basin one HHTL level up, not a scalar column. + +## Sequencing +D-EW64-1 + D-VIEW-1 (now, contract) → D-EW64-2 (SoA columns, CI) → D-STORY-1 (CLAM +clusterer) → D-STORY-2 (session index) → D-STORY-3 (archetype ranking) → D-HORIZON-1 +(stopping rule). Each CI-gated step verified in a full checkout (with `protoc`). + +--- +*v1. Contract slices verifiable offline; core/cold slices CI-gated. Firewalls +non-negotiable: identity exact (CAM/OGIT), stories flexible (CLAM/discovery), never +swapped.* diff --git a/crates/deepnsm/src/arcs.rs b/crates/deepnsm/src/arcs.rs new file mode 100644 index 00000000..35b1c449 --- /dev/null +++ b/crates/deepnsm/src/arcs.rs @@ -0,0 +1,85 @@ +//! Basin/literal arc split — the **projection** decomposition (Broca / +//! MarkovBundler side of `E-ENGLISH-BIFURCATES`). +//! +//! This is strictly the *projection* faculty: it decomposes the role-superposed +//! `Trajectory` (the MarkovBundler's wave) into its two arcs. It does **not** +//! route and does **not** resolve — sentence resolution (literal comprehension + +//! ambiguity resolution, tokenless) is a SEPARATE faculty in `comprehension.rs` +//! (Wernicke). Keeping the projection apart from the resolution is the +//! anti-spaghetti boundary (user, 2026-05-31: *"Markov bundler should be +//! separate as the projection, while the sentence resolution is literal text +//! comprehension with ambiguity resolution without tokens"*). +//! +//! - **basin arc** — the role-superposed spine bundle: the *declared/exact* +//! meaning keyframe (points at ONE basin — a DOLCE class / story-arc). +//! - **literal arc** — the COCA ranks that fed it: the *detected/redundant* +//! surface, prunable once the basin resolves (the prune/tombstone lifecycle +//! itself lives contract-side in `WitnessTable`, not here). +//! +//! Firewall: both arcs stay English-side; the basin's f32 is upstream-only +//! (sign-binarized via `disambiguator_glue`, or resolved to an opaque handle, +//! before it ever crosses into the agnostic graph); no COCA rank reaches the +//! hot graph as identity. + +use crate::trajectory::Trajectory; + +/// The semantic spine: the role-superposed bundle that points at ONE basin. +/// The declared/exact side of the language↔meaning duality. +#[derive(Debug, Clone, PartialEq)] +pub struct BasinArc(pub Vec); + +/// The language surface: the COCA literal ranks that fed the bundle. +/// Multiple, redundant, prunable once the basin resolves (the detected side). +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct LiteralArc(pub Vec); + +impl Trajectory { + /// Split this trajectory (the projection / wave) into its **basin arc** + /// (the full role-superposed spine bundle) and its **literal arc** (the + /// COCA ranks that fed it). + /// + /// Projection-side only: it names the duality at the seam where + /// `disambiguator_glue` already threads the bundle into the contract + /// `context_chain`. It performs no fact/story routing — that is a + /// comprehension decision and lives in `comprehension.rs`. + #[must_use] + pub fn split_arcs(&self, literal_ranks: &[u16]) -> (BasinArc, LiteralArc) { + ( + BasinArc(self.fingerprint.clone()), + LiteralArc(literal_ranks.to_vec()), + ) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + const DIMS: usize = 16_384; + + #[test] + fn split_arcs_preserves_basin_and_literals() { + let t = Trajectory { + fingerprint: vec![0.25_f32; DIMS], + radius: 5, + }; + let ranks = [12_u16, 670, 2942]; + let (basin, literal) = t.split_arcs(&ranks); + assert_eq!(basin.0, t.fingerprint, "basin arc IS the spine bundle"); + assert_eq!(literal.0, ranks, "literal arc carries the COCA ranks verbatim"); + } + + #[test] + fn literal_arc_is_independent_of_basin() { + // Same basin, two different literal sets → distinct literal arcs. + // The prune target (literals) is separable from the spine (basin). + let t = Trajectory { + fingerprint: vec![1.0_f32; DIMS], + radius: 5, + }; + let (b1, l1) = t.split_arcs(&[1, 2, 3]); + let (b2, l2) = t.split_arcs(&[9, 9]); + assert_eq!(b1, b2, "basin unchanged by literal choice"); + assert_ne!(l1, l2, "literal arcs differ"); + } +} diff --git a/crates/deepnsm/src/arcuate.rs b/crates/deepnsm/src/arcuate.rs new file mode 100644 index 00000000..6033a2b3 --- /dev/null +++ b/crates/deepnsm/src/arcuate.rs @@ -0,0 +1,171 @@ +//! The **arcuate fasciculus** — the Broca↔Wernicke cable made to carry signal +//! (closes the conduction-aphasia gap, `E-ARCUATE-CONDUCTION`). +//! +//! ## What it does +//! +//! Owns both ends of the cable and slides the projection into the evidence ring: +//! - **Broca / producer** — a `MarkovBundler`. Each fed sentence advances its +//! window; once saturated it emits a `Trajectory` (the role-superposed +//! projection wave). +//! - **Wernicke / evidence ring** — a contract `ContextChain` (the ±5 replay +//! surface). Each emitted projection is sign-binarized and **slid** into the +//! ring's newest slot. +//! +//! The diagnosis it fixes: `disambiguator_glue` is the cable, and the contract +//! `ContextChain` gives fill + coherence + replay primitives — but **no +//! streaming advance**, and `MarkovBundler::push` had no caller. So the cable +//! existed but carried no signal (production + comprehension intact, *repetition* +//! failing — textbook conduction aphasia). `Arcuate` owns the producer + the +//! ring-slide, so the projection now flows from Broca into Wernicke's window. +//! +//! ## Scope (deliberate, anti-spaghetti) +//! +//! This is a SEPARATE seam — it is **not** wired into `pipeline.rs`'s live +//! 512-bit `ContextWindow`. How the two coexist is a distinct decision; fusing +//! them here would be the spaghetti the design explicitly avoids. The connector +//! is offline-testable on its own. +//! +//! ## Firewall +//! +//! The only thing crossing into the contract is a `Binary16K` fingerprint (the +//! sign-binarized projection) — never a COCA rank. The contract takes no +//! `deepnsm` dependency; `deepnsm` injects through the existing fingerprint seam. +//! Double-windowing note: the bundler does ±radius bundling and the chain is ±5, +//! so the ring holds windowed-projection fingerprints — adequate to carry signal; +//! whether per-sentence (radius-0) fingerprints are preferable is `OQ-ARC-WINDOW`. + +use crate::disambiguator_glue::sign_binarize_to_binary16k; +use crate::markov_bundle::{Kernel, MarkovBundler, WindowedSentence}; +use crate::trajectory::Trajectory; + +use lance_graph_contract::crystal::fingerprint::CrystalFingerprint; +use lance_graph_contract::grammar::context_chain::{ + ContextChain, DisambiguateOpts, DisambiguationResult, +}; + +/// The arcuate connector: a `MarkovBundler` producer feeding a ±5 +/// `ContextChain` evidence ring. +pub struct Arcuate { + bundler: MarkovBundler, + chain: ContextChain, +} + +impl Arcuate { + /// New connector with bundler `radius` + `kernel`. The ring is the + /// contract's fixed ±5 (`CHAIN_LEN = 11`), independent of `radius`. + #[must_use] + pub fn new(radius: u32, kernel: Kernel) -> Self { + Self { + bundler: MarkovBundler::new(radius, kernel), + chain: ContextChain::new(), + } + } + + /// Feed one sentence's windowed tokens (Broca). When the bundler window + /// saturates it emits a `Trajectory` (the projection); that projection is + /// sign-binarized and slid into the ±5 ring (the cable carrying signal). + /// Returns the emitted projection, or `None` while the window still fills. + pub fn feed(&mut self, sentence: WindowedSentence) -> Option { + let traj = self.bundler.push(sentence)?; + let bits = sign_binarize_to_binary16k(&traj.fingerprint); + self.slide_in(CrystalFingerprint::Binary16K(bits)); + Some(traj) + } + + /// Slide the ±5 ring forward by one: drop the oldest slot, append the + /// newest (so the newest sits at the last index and the focal at index 5 + /// trails it by five). The contract offers no streaming advance, so the + /// connector owns the ring via the chain's public `fingerprints`. + fn slide_in(&mut self, fp: CrystalFingerprint) { + self.chain.fingerprints.remove(0); + self.chain.fingerprints.push(Some(fp)); + } + + /// The ±5 evidence ring (Wernicke's replay surface). + #[must_use] + pub fn chain(&self) -> &ContextChain { + &self.chain + } + + /// Disambiguate at the focal position against `candidates` (the ±5 replay). + /// Delegates to the contract chain; the populated ring is the evidence. + pub fn disambiguate(&self, candidates: I) -> DisambiguationResult + where + I: IntoIterator, + { + self.chain.disambiguate_with( + ContextChain::focal_index(), + candidates, + DisambiguateOpts::default(), + ) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::markov_bundle::{GrammaticalRole, TokenWithRole}; + + /// One SUBJECT-band sentence whose content distinguishes it by `value`. + fn sentence(value: f32) -> WindowedSentence { + let len = GrammaticalRole::Subject.slice().len(); + WindowedSentence { + tokens: vec![TokenWithRole { + content_fp: vec![value; len], + role: GrammaticalRole::Subject, + }], + } + } + + fn fp(value: f32) -> CrystalFingerprint { + CrystalFingerprint::Binary16K(sign_binarize_to_binary16k(&vec![value; 16_384])) + } + + #[test] + fn window_fills_before_first_projection() { + // radius 2 → bundler needs 2*2+1 = 5 pushes before the first emit. + let mut arc = Arcuate::new(2, Kernel::Uniform); + for _ in 0..4 { + assert!(arc.feed(sentence(1.0)).is_none()); + } + assert_eq!(arc.chain().filled(), 0, "ring untouched while window fills"); + } + + #[test] + fn projection_slides_into_ring_when_window_saturates() { + let mut arc = Arcuate::new(2, Kernel::Uniform); + let mut emitted = None; + for _ in 0..5 { + emitted = arc.feed(sentence(1.0)); + } + assert!(emitted.is_some(), "5th feed (radius 2) emits a projection"); + assert_eq!(arc.chain().filled(), 1, "one projection slid into the ring"); + // Newest occupies the last slot; ring length stays at the contract ±5. + assert_eq!(arc.chain().fingerprints.len(), 11); + assert!(arc.chain().fingerprints[10].is_some(), "newest at the tail"); + } + + #[test] + fn ring_saturates_and_keeps_length_after_many_feeds() { + let mut arc = Arcuate::new(2, Kernel::Uniform); + // 5 to warm up + 11 emits to fill all slots = 16 feeds; vary content so + // the ring is not degenerate. + for i in 0..16 { + arc.feed(sentence(1.0 + i as f32)); + } + assert_eq!(arc.chain().fingerprints.len(), 11); + assert!(arc.chain().is_saturated(), "ring full after ≥15 emits"); + assert!(arc.chain().focal().is_some(), "focal slot carries signal"); + } + + #[test] + fn disambiguate_over_populated_ring_ranks_candidates() { + let mut arc = Arcuate::new(2, Kernel::Uniform); + for i in 0..16 { + arc.feed(sentence(1.0 + i as f32)); + } + let result = arc.disambiguate([fp(1.0), fp(-1.0)]); + assert_eq!(result.candidate_count, 2, "both candidates evaluated"); + assert!(result.winner_index < 2, "a real winner over the ±5 evidence"); + } +} diff --git a/crates/deepnsm/src/comprehension.rs b/crates/deepnsm/src/comprehension.rs new file mode 100644 index 00000000..ec4844b4 --- /dev/null +++ b/crates/deepnsm/src/comprehension.rs @@ -0,0 +1,129 @@ +//! Sentence resolution — literal text comprehension + the fact/story router +//! (the **Wernicke** faculty of `E-ENGLISH-BIFURCATES`). +//! +//! Separate from the MarkovBundler **projection** (`arcs.rs` / `markov_bundle.rs`) +//! by design (user, 2026-05-31: *"the sentence resolution is literal text +//! comprehension with ambiguity resolution without tokens"*). This faculty reads +//! the **comprehended** structure — `SentenceStructure`, whose words are COCA +//! ranks, **not** BPE tokens ("without tokens") — and resolves where each triple +//! lands. It never touches the VSA projection band; routing is a comprehension +//! decision, not a projection measurement. +//! +//! ## The three-faculty boundary (Broca / Wernicke / Hippocampus) +//! +//! - **Broca** (projection / syntax) — PoS-FSM → SPO + the MarkovBundler wave +//! (`parser.rs`, `markov_bundle.rs`, `arcs.rs`). *Assembles structure.* +//! - **Wernicke** (comprehension / resolution) — **this module**: maps the +//! comprehended SPO to meaning, resolves ambiguity, routes fact vs story. +//! *Resolves meaning.* (±5 coreference / `context_chain` is the deeper +//! ambiguity-resolution wire that plugs in here — not yet connected.) +//! - **Hippocampus** (episodic memory) — downstream/agnostic: the story-arc +//! (`EpisodicEdges64`, ±5→±500), and consolidation-to-semantic via the +//! `WitnessTable` lifecycle (`spo_fact_ref None→Some→tombstone` = an aged +//! story crystallising into a DOLCE fact). *Remembers + consolidates.* +//! +//! ## The router is a FORK, not a switch (`OQ-ROUTER-SIGNAL`) +//! +//! Every SPO relation is a fact-candidate; a triple the parser marked temporal +//! ALSO threads a story-arc. Resolved **per triple**, because one sentence can +//! carry both a timeless relation and a dated event ("the dog, which is a +//! mammal, ran"). Whether the fact leg is *committed* to the ontology (vs left +//! as an event-only relation) is a downstream policy, deliberately not here. + +use crate::parser::SentenceStructure; + +/// Where a single comprehended triple lands (`E-ENGLISH-BIFURCATES`). A FORK: +/// `fact` is the always-present (atemporal) SPO relation; `story` is the +/// additive temporal placement. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct Landing { + /// The SPO relation is assertable as an (atemporal) ontology fact. + /// Always true at this layer; commit-policy is downstream. + pub fact: bool, + /// The triple carries a temporal marker, so it ALSO threads a story-arc. + pub story: bool, +} + +impl SentenceStructure { + /// Did the parser comprehend triple `triple_index` as carrying a temporal + /// marker? Reads the per-triple `temporals` comprehension signal — no + /// tokens, no projection band. + #[must_use] + pub fn is_temporal(&self, triple_index: usize) -> bool { + self.temporals.iter().any(|&(ti, _)| ti == triple_index) + } + + /// Resolve the landing of triple `triple_index`: always a fact-candidate; + /// also a story-arc iff it carries a temporal marker (the fork). + #[must_use] + pub fn triple_landing(&self, triple_index: usize) -> Landing { + Landing { + fact: true, + story: self.is_temporal(triple_index), + } + } + + /// Landing for every triple in the sentence, in triple order. + #[must_use] + pub fn landings(&self) -> Vec { + (0..self.triples.len()) + .map(|i| self.triple_landing(i)) + .collect() + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::spo::SpoTriple; + + /// A sentence with two triples; triple 0 is marked temporal (e.g. a past- + /// tense event), triple 1 is atemporal (a timeless relation). + fn two_triples_first_temporal() -> SentenceStructure { + SentenceStructure { + triples: vec![SpoTriple::new(1, 2, 3), SpoTriple::new(4, 5, 6)], + modifiers: Vec::new(), + negations: Vec::new(), + temporals: vec![(0, 99)], // triple 0 has temporal word rank 99 + } + } + + #[test] + fn temporal_triple_forks_into_fact_and_story() { + let s = two_triples_first_temporal(); + assert!(s.is_temporal(0)); + let l = s.triple_landing(0); + assert!(l.fact && l.story, "temporal triple → BOTH fact and story (fork)"); + } + + #[test] + fn atemporal_triple_is_fact_only() { + let s = two_triples_first_temporal(); + assert!(!s.is_temporal(1)); + let l = s.triple_landing(1); + assert!(l.fact, "every SPO is a fact-candidate"); + assert!(!l.story, "no temporal marker → no story-arc"); + } + + #[test] + fn landings_are_per_triple_in_order() { + let s = two_triples_first_temporal(); + let ls = s.landings(); + assert_eq!(ls.len(), 2); + assert_eq!(ls[0], Landing { fact: true, story: true }); + assert_eq!(ls[1], Landing { fact: true, story: false }); + } + + #[test] + fn empty_sentence_has_no_landings() { + let s = SentenceStructure { + triples: Vec::new(), + modifiers: Vec::new(), + negations: Vec::new(), + temporals: Vec::new(), + }; + assert!(s.landings().is_empty()); + // Out-of-range index is fact-only (no temporal marker can match). + assert_eq!(s.triple_landing(7), Landing { fact: true, story: false }); + } +} diff --git a/crates/deepnsm/src/lib.rs b/crates/deepnsm/src/lib.rs index 326afe78..098ab068 100644 --- a/crates/deepnsm/src/lib.rs +++ b/crates/deepnsm/src/lib.rs @@ -65,6 +65,21 @@ pub mod trajectory; pub mod markov_bundle; pub mod nsm_primes; +// E-ENGLISH-BIFURCATES — two SEPARATE faculties (don't fuse them): +// arcs (Broca/projection): basin/literal decomposition of the MarkovBundler wave. +// comprehension (Wernicke): literal sentence resolution + fact/story router, +// tokenless, reading SentenceStructure — NOT the projection band. +// Hippocampus (episodic story-arc + consolidation) is downstream/agnostic. +// See .claude/knowledge/english-fact-story-bifurcation-grail-v1.md. +pub mod arcs; +pub mod comprehension; + +// E-ARCUATE-CONDUCTION: the arcuate fasciculus — owns the MarkovBundler +// producer + the ±5 ContextChain ring and slides the projection into it, so +// the Broca↔Wernicke cable carries signal. Separate seam; NOT wired into +// pipeline.rs's live ContextWindow (that coexistence is a distinct decision). +pub mod arcuate; + // Loose-end-#2 closer (PR-G3): glue from MarkovBundler::role_bundle() // → ContextChain::disambiguate_with(.., DisambiguateOpts { // sentinel_fp }). Closes the "real fp" honesty gap by giving the diff --git a/crates/lance-graph-contract/src/episodic_edges.rs b/crates/lance-graph-contract/src/episodic_edges.rs new file mode 100644 index 00000000..2e3f94f5 --- /dev/null +++ b/crates/lance-graph-contract/src/episodic_edges.rs @@ -0,0 +1,306 @@ +//! # `episodic_edges` — AriGraph episodic edges, RISC-encoded (zero-dep). +//! +//! EW64 is **AriGraph's episodic edges** — a mailbox(=episode) is a *basin* with +//! *multiple* edges (NOT a lens over one `CausalEdge64`). This is the witness/ +//! relational concern, SoC'd from both the temporal arc (a basin one HHTL level up) +//! and frozen identity (CAM/OGIT). +//! +//! ## Cost model (grounded by the #444 locality probe) +//! - **~98.6% intra-basin** (probe): an edge stays in the row's own family, which is +//! **inherited** from the HHTL/`class_id` path → ~0 extra bits. `EdgeRef::family == 0`. +//! - **~1.4% cross-family** (the crossover): a **4-bit nibble** (16 families, +//! `family ∈ 1..=15`) indexes the **OGIT-class-inherited cross-family palette** — a +//! CAM_PQ facet code whose codebook is the class's declared closed range +//! (`owl:disjointWith` ⇒ collision-free). The 16 *identities* live in the class, +//! **never on the edge** (`I-VSA-IDENTITIES`: point, don't copy). Probe fan-out +//! ≤ 3 ⇒ 4 bits (16) has headroom. +//! +//! ## Layout — `EpisodicEdges64(u64)` = 4 × `u16` slots +//! Each slot: `0x0000` = empty; else `[bits 12-15: family nibble][bits 0-11: local]`. +//! `local` is a **1-based within-family index** (`1..=4095`); the resolved family is +//! the row's own basin (`family == 0`, inherited) or `class.cross_family_palette[family]` +//! (`1..=15`). Cross-session reach is a *separate* 16-bit episode-store column, not this +//! word. Identity resolution flies ABOVE the row (the OGIT class), as `class_view` does. + +// The slot pack/unpack does intentional nibble extraction (slot>>12 ∈ 0..=15) and +// low-16-bit reads (u64 -> u16); both are provably-bounded narrowings. +// cast_possible_truncation: intentional bounded narrowings (nibble slot>>12 ∈ 0..=15; +// low-16-bit slot reads). doc_markdown: domain acronyms (AriGraph/OGIT/CAM_PQ/SoC) read +// better unbackticked in this module's prose. +#![allow(clippy::cast_possible_truncation, clippy::doc_markdown)] + +/// One episodic edge: a `(family, local)` reference in the episodic basin space. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct EdgeRef { + /// Cross-family selector. `0` = **intra-basin** (the row's own family, inherited + /// from the HHTL/`class_id` path — the ~98.6% common case). `1..=15` = a + /// cross-family index into the OGIT-class-inherited palette (the ~1.4% crossover). + pub family: u8, + /// 1-based within-family local index (`1..=4095`); `0` is the empty-slot sentinel. + pub local: u16, +} + +impl EdgeRef { + /// Family count addressable by the 4-bit nibble (probe fan-out ≤ 3 ⇒ headroom). + pub const FAMILIES: u8 = 16; + /// Max 1-based within-family local index (12 bits). + pub const MAX_LOCAL: u16 = 0x0FFF; + + /// A validated edge, or `None` if `family ≥ 16` or `local ∉ 1..=4095`. + #[must_use] + pub const fn new(family: u8, local: u16) -> Option { + if family < Self::FAMILIES && local >= 1 && local <= Self::MAX_LOCAL { + Some(Self { family, local }) + } else { + None + } + } + + /// An **intra-basin** edge (`family == 0`, the inherited common case). + #[must_use] + pub const fn intra(local: u16) -> Option { + Self::new(0, local) + } + + /// A **cross-family** edge into palette index `family ∈ 1..=15`. + #[must_use] + pub const fn cross(family: u8, local: u16) -> Option { + if family == 0 { + None + } else { + Self::new(family, local) + } + } + + /// Does this edge cross to another family (vs. staying intra-basin)? + #[must_use] + pub const fn is_cross(self) -> bool { + self.family != 0 + } + + fn to_slot(self) -> u16 { + (u16::from(self.family) << 12) | (self.local & Self::MAX_LOCAL) + } + + const fn from_slot(slot: u16) -> Option { + if slot == 0 { + None + } else { + Some(Self { family: (slot >> 12) as u8, local: slot & Self::MAX_LOCAL }) + } + } +} + +/// Up to 4 AriGraph episodic edges packed into one `u64` (4 × 16-bit slots). +/// +/// The witness/relational column of the per-row SoA: which other basin members this +/// episode touched. Agnostic — the nibble's *meaning* resolves in the OGIT class, not +/// here. `Default` / [`EpisodicEdges64::empty`] is the no-edge word. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)] +pub struct EpisodicEdges64(pub u64); + +impl EpisodicEdges64 { + /// Edge slots per word (4 × 16 bits = 64). + pub const CAPACITY: usize = 4; + + /// The empty (no-edge) word. + #[must_use] + pub const fn empty() -> Self { + Self(0) + } + + /// The edge in slot `i` (`0..4`), or `None` if the slot is empty / out of range. + #[must_use] + pub const fn edge(self, i: usize) -> Option { + if i >= Self::CAPACITY { + return None; + } + EdgeRef::from_slot((self.0 >> (i * 16)) as u16) + } + + /// How many slots carry an edge. + #[must_use] + pub fn count(self) -> usize { + (0..Self::CAPACITY).filter(|&i| self.edge(i).is_some()).count() + } + + /// All 4 slots full? + #[must_use] + pub fn is_full(self) -> bool { + self.count() == Self::CAPACITY + } + + /// Place `e` into the first empty slot; `None` if the word is already full. + #[must_use] + pub fn push(self, e: EdgeRef) -> Option { + let mut i = 0; + while i < Self::CAPACITY { + if self.edge(i).is_none() { + let shift = i * 16; + let cleared = self.0 & !(0xFFFF_u64 << shift); + return Some(Self(cleared | (u64::from(e.to_slot()) << shift))); + } + i += 1; + } + None + } + + /// Iterate the present edges in slot order. + pub fn iter(self) -> impl Iterator { + (0..Self::CAPACITY).filter_map(move |i| self.edge(i)) + } + + /// Count of cross-family edges (the crossover load — the ~1.4% the probe measured). + #[must_use] + pub fn cross_count(self) -> usize { + self.iter().filter(|e| e.is_cross()).count() + } + + // ── Little-endian byte contract (mirrors `causal-edge::CausalEdge64`) ── + // The frozen LE byte grammar every AriGraph consumer reads: surrealkv WAL, the + // baton wire (`CollapseGateEmission`), Lance columns. Canonical little-endian, + // platform-independent — changing the layout is a WAL/wire migration, never silent. + + /// The raw packed `u64` (host value; serialize via [`to_le_bytes`](Self::to_le_bytes)). + #[must_use] + pub const fn to_u64(self) -> u64 { + self.0 + } + + /// Reconstruct from a raw packed `u64`. + #[must_use] + pub const fn from_u64(raw: u64) -> Self { + Self(raw) + } + + /// Canonical **little-endian** wire bytes — the AriGraph-reference contract. + #[must_use] + pub const fn to_le_bytes(self) -> [u8; 8] { + self.0.to_le_bytes() + } + + /// Reconstruct from canonical little-endian wire bytes. + #[must_use] + pub const fn from_le_bytes(bytes: [u8; 8]) -> Self { + Self(u64::from_le_bytes(bytes)) + } + + /// Append the canonical LE bytes to a wire buffer (baton / WAL line). + pub fn write_le(self, buf: &mut Vec) { + buf.extend_from_slice(&self.to_le_bytes()); + } + + /// Read one word from `buf` at `offset` (LE), or `None` if fewer than 8 bytes remain. + #[must_use] + pub fn read_le(buf: &[u8], offset: usize) -> Option { + let end = offset.checked_add(8)?; + let mut b = [0u8; 8]; + b.copy_from_slice(buf.get(offset..end)?); + Some(Self::from_le_bytes(b)) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn edgeref_new_validates_family_and_local() { + assert!(EdgeRef::new(0, 1).is_some()); + assert!(EdgeRef::new(15, 4095).is_some()); + assert!(EdgeRef::new(16, 1).is_none()); + assert!(EdgeRef::new(0, 0).is_none()); + assert!(EdgeRef::new(0, 4096).is_none()); + assert!(EdgeRef::cross(0, 5).is_none()); + assert_eq!(EdgeRef::intra(5).unwrap().family, 0); + } + + #[test] + fn slot_roundtrip_and_empty_sentinel() { + for family in 0..16u8 { + for &local in &[1u16, 2, 100, 4095] { + let e = EdgeRef::new(family, local).unwrap(); + assert_eq!(EdgeRef::from_slot(e.to_slot()).unwrap(), e); + } + } + assert_eq!(EdgeRef::from_slot(0), None); + } + + #[test] + fn push_count_and_full() { + let mut w = EpisodicEdges64::empty(); + assert_eq!(w.count(), 0); + for k in 1..=4u16 { + w = w.push(EdgeRef::intra(k).unwrap()).expect("fits"); + } + assert_eq!(w.count(), 4); + assert!(w.is_full()); + assert!(w.push(EdgeRef::intra(5).unwrap()).is_none()); + } + + #[test] + fn edge_index_out_of_range_is_none() { + let w = EpisodicEdges64::empty().push(EdgeRef::intra(7).unwrap()).unwrap(); + assert_eq!(w.edge(0), EdgeRef::intra(7)); + assert_eq!(w.edge(1), None); + assert_eq!(w.edge(EpisodicEdges64::CAPACITY), None); + } + + #[test] + fn intra_is_cheap_default_cross_is_the_nibble() { + let w = EpisodicEdges64::empty() + .push(EdgeRef::intra(10).unwrap()) + .unwrap() + .push(EdgeRef::intra(11).unwrap()) + .unwrap() + .push(EdgeRef::intra(12).unwrap()) + .unwrap() + .push(EdgeRef::cross(3, 7).unwrap()) + .unwrap(); + assert_eq!(w.count(), 4); + assert_eq!(w.cross_count(), 1); + assert!(!w.edge(0).unwrap().is_cross()); + assert!(w.edge(3).unwrap().is_cross()); + assert_eq!(w.edge(3).unwrap().family, 3); + } + + #[test] + fn iter_yields_present_edges_in_order() { + let w = EpisodicEdges64::empty() + .push(EdgeRef::intra(1).unwrap()) + .unwrap() + .push(EdgeRef::cross(2, 9).unwrap()) + .unwrap(); + let got: Vec<_> = w.iter().collect(); + assert_eq!(got, vec![EdgeRef::intra(1).unwrap(), EdgeRef::cross(2, 9).unwrap()]); + } + + #[test] + fn word_is_exactly_64_bits() { + assert_eq!(core::mem::size_of::(), 8); + } + + #[test] + fn le_byte_contract_roundtrips() { + let w = EpisodicEdges64::empty() + .push(EdgeRef::intra(7).unwrap()) + .unwrap() + .push(EdgeRef::cross(5, 42).unwrap()) + .unwrap(); + assert_eq!(EpisodicEdges64::from_u64(w.to_u64()), w); + assert_eq!(EpisodicEdges64::from_le_bytes(w.to_le_bytes()), w); + let mut buf = Vec::new(); + w.write_le(&mut buf); + assert_eq!(buf.len(), 8); + assert_eq!(EpisodicEdges64::read_le(&buf, 0), Some(w)); + assert_eq!(EpisodicEdges64::read_le(&buf, 1), None); // only 7 bytes remain + } + + #[test] + fn le_bytes_are_canonical_little_endian() { + // slot 0 = intra local 1 => 0x0001 in the low 16 bits; LE => byte[0] = 0x01. + let w = EpisodicEdges64::empty().push(EdgeRef::intra(1).unwrap()).unwrap(); + assert_eq!(w.to_le_bytes()[0], 0x01); + assert_eq!(w.to_le_bytes()[1], 0x00); + } +} diff --git a/crates/lance-graph-contract/src/head2head.rs b/crates/lance-graph-contract/src/head2head.rs new file mode 100644 index 00000000..8fe91bec --- /dev/null +++ b/crates/lance-graph-contract/src/head2head.rs @@ -0,0 +1,238 @@ +//! # `head2head` — competing-expert superposition + winner selection. +//! +//! Two (or N) mailbox-experts reason over the same input *in parallel*, each +//! posting a [`BlackboardEntry`]; head2head selects **one** winner whose +//! emissions become the authoritative spiral. This is the *selection* half of +//! the superposition (item: "head2head mailbox thinking as superposition") — the +//! parallel mailbox *execution* is the CI-gated consumer side; the +//! winner-pick over the blackboard is what lives, zero-dep and verifiable, here. +//! +//! ## The Go metaphor (the user's framing) +//! Two strategies compete and the board scores them: +//! - **infight** (close tactical combat) ≈ [`WinnerCriterion::DissonanceMin`]: +//! the expert with the least internal contradiction — tightest local resolution. +//! - **Raumgewinn** (territory / influence) ≈ [`WinnerCriterion::SupportSpread`]: +//! the expert whose top-K support atoms cover the most distinct ground. +//! +//! ## Separation of concerns — select, never duplicate +//! [`Head2Head::select`] is a pure read + arg-extremum over the *existing* +//! [`Blackboard`] entries (`confidence` / `dissonance` / `support` are already +//! there). It stores no new identity and copies nothing — an identity is +//! *pointed at* ([`ExpertId`]), never re-materialized (`I-VSA-IDENTITIES`). The +//! winner is a *decision over state*, not a new copy of it. +//! +//! Zero dependencies. Pure data + one method on the selector carrier. + +// Pedantic carve-outs (the repo does not deny pedantic; these are in-context false +// positives): cast_precision_loss — the support count n ∈ 0..=4 is exact as f32; +// float_cmp — tests compare exact integer-valued scores; missing_const_for_fn — +// `score` transitively calls the non-const `slice::contains`. +#![allow(clippy::cast_precision_loss, clippy::float_cmp, clippy::missing_const_for_fn)] + +use crate::a2a_blackboard::{Blackboard, BlackboardEntry, ExpertId}; + +/// How to pick the winner among competing experts. Each maps a +/// [`BlackboardEntry`] to a scalar score; the highest score wins. +#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)] +pub enum WinnerCriterion { + /// Lowest dissonance wins — the **infight** reading: least internal + /// contradiction (score = `1 - dissonance`). + DissonanceMin, + /// Highest self-reported confidence wins (score = `confidence`). + ConfidenceMax, + /// Widest distinct support wins — the **Raumgewinn**/territory reading + /// (score = count of distinct non-zero atoms in `support[4]`). + SupportSpread, + /// Confidence tempered by dissonance: `confidence * (1 - dissonance)`. + /// The default — rewards certainty that isn't bought with contradiction. + #[default] + Tempered, +} + +/// The outcome of a head2head: who won, by how much, under which criterion. +/// +/// `margin` is the winner's lead over the best *other* expert +/// (`winner_score - runner_up_score`); with no distinct runner-up it is the +/// winner's uncontested score (lead over the `0.0` floor). A small `margin` is +/// the dark-horse signal — the wave was nearly a coin-flip, so the deterministic +/// particle chain should confirm before the winner's spiral is trusted. +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct CompetitionOutcome { + /// The expert whose top-scoring bid won. + pub winner: ExpertId, + /// The winner's score under `criterion`. + pub winner_score: f32, + /// The best *other* expert, if any competed. + pub runner_up: Option, + /// `winner_score - runner_up_score` (lead over the `0.0` floor if uncontested). + pub margin: f32, + /// The criterion the board judged by. + pub criterion: WinnerCriterion, +} + +/// The board judge: holds the [`WinnerCriterion`] and scores competitors. +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub struct Head2Head { + /// How this judge scores each bid. + pub criterion: WinnerCriterion, +} + +impl Head2Head { + /// A judge with an explicit criterion. + #[must_use] + pub fn new(criterion: WinnerCriterion) -> Self { + Self { criterion } + } + + /// Score one bid under this judge's criterion (higher = better). + #[must_use] + pub fn score(&self, e: &BlackboardEntry) -> f32 { + match self.criterion { + WinnerCriterion::DissonanceMin => 1.0 - e.dissonance, + WinnerCriterion::ConfidenceMax => e.confidence, + WinnerCriterion::SupportSpread => distinct_support(e.support) as f32, + WinnerCriterion::Tempered => e.confidence * (1.0 - e.dissonance), + } + } + + /// Select the winning expert over the blackboard's bids, or `None` if the + /// board is empty. Each entry is a *bid*; the expert of the top-scoring bid + /// wins, and the runner-up is the top bid from any *other* expert. + #[must_use] + pub fn select(&self, bb: &Blackboard) -> Option { + let best = bb + .entries + .iter() + .max_by(|a, b| self.score(a).total_cmp(&self.score(b)))?; + let winner = best.expert_id; + let winner_score = self.score(best); + + let runner = bb + .entries + .iter() + .filter(|e| e.expert_id != winner) + .max_by(|a, b| self.score(a).total_cmp(&self.score(b))); + let (runner_up, runner_score) = + runner.map_or((None, 0.0), |r| (Some(r.expert_id), self.score(r))); + + Some(CompetitionOutcome { + winner, + winner_score, + runner_up, + margin: winner_score - runner_score, + criterion: self.criterion, + }) + } +} + +/// Count of distinct non-zero atoms in a 4-slot support vector (the territory +/// measure for [`WinnerCriterion::SupportSpread`]). `0` is the no-atom sentinel. +fn distinct_support(support: [u16; 4]) -> usize { + let mut seen = [0u16; 4]; + let mut n = 0; + for a in support { + if a != 0 && !seen[..n].contains(&a) { + seen[n] = a; + n += 1; + } + } + n +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::a2a_blackboard::ExpertCapability; + + fn entry(id: ExpertId, confidence: f32, dissonance: f32, support: [u16; 4]) -> BlackboardEntry { + BlackboardEntry { + expert_id: id, + capability: ExpertCapability::ReasoningTopology, + result: 0, + confidence, + support, + dissonance, + cost_us: 0, + } + } + + fn board(es: Vec) -> Blackboard { + Blackboard { entries: es, round: 0 } + } + + #[test] + fn tempered_default_rewards_certainty_without_contradiction() { + // A: high confidence but high dissonance (0.9 * 0.4 = 0.36). + // B: lower confidence but clean (0.7 * 0.9 = 0.63 wins). + let bb = board(vec![ + entry(1, 0.9, 0.6, [0; 4]), + entry(2, 0.7, 0.1, [0; 4]), + ]); + let out = Head2Head::default().select(&bb).unwrap(); + assert_eq!(out.criterion, WinnerCriterion::Tempered); + assert_eq!(out.winner, 2); + assert_eq!(out.runner_up, Some(1)); + assert!(out.margin > 0.0); + } + + #[test] + fn dissonance_min_is_the_infight_pick() { + // Same confidence; the tighter (lower-dissonance) expert wins the infight. + let bb = board(vec![ + entry(1, 0.8, 0.5, [0; 4]), + entry(2, 0.8, 0.2, [0; 4]), + ]); + let out = Head2Head::new(WinnerCriterion::DissonanceMin).select(&bb).unwrap(); + assert_eq!(out.winner, 2); + } + + #[test] + fn support_spread_is_the_raumgewinn_pick() { + // Same confidence/dissonance; the expert covering more distinct ground wins. + let bb = board(vec![ + entry(1, 0.8, 0.1, [42, 42, 0, 0]), // 1 distinct atom + entry(2, 0.8, 0.1, [7, 9, 13, 21]), // 4 distinct atoms (territory) + ]); + let out = Head2Head::new(WinnerCriterion::SupportSpread).select(&bb).unwrap(); + assert_eq!(out.winner, 2); + assert_eq!(out.winner_score, 4.0); + assert_eq!(out.margin, 3.0); // 4 distinct vs 1 distinct + } + + #[test] + fn confidence_max_ignores_dissonance() { + let bb = board(vec![ + entry(1, 0.95, 0.9, [0; 4]), // noisy but loud → wins on raw confidence + entry(2, 0.6, 0.0, [0; 4]), + ]); + let out = Head2Head::new(WinnerCriterion::ConfidenceMax).select(&bb).unwrap(); + assert_eq!(out.winner, 1); + } + + #[test] + fn uncontested_single_expert_has_no_runner_up() { + let bb = board(vec![entry(7, 0.8, 0.2, [0; 4])]); + let out = Head2Head::default().select(&bb).unwrap(); + assert_eq!(out.winner, 7); + assert_eq!(out.runner_up, None); + assert_eq!(out.margin, out.winner_score); // lead over the 0.0 floor + } + + #[test] + fn multiple_bids_per_expert_use_the_experts_best() { + // Expert 1 posts twice; its best bid represents it; runner-up is expert 2. + let bb = board(vec![ + entry(1, 0.3, 0.0, [0; 4]), + entry(1, 0.9, 0.0, [0; 4]), // expert 1's best + entry(2, 0.5, 0.0, [0; 4]), + ]); + let out = Head2Head::new(WinnerCriterion::ConfidenceMax).select(&bb).unwrap(); + assert_eq!(out.winner, 1); + assert_eq!(out.runner_up, Some(2)); + } + + #[test] + fn empty_board_has_no_winner() { + assert!(Head2Head::default().select(&board(vec![])).is_none()); + } +} diff --git a/crates/lance-graph-contract/src/lib.rs b/crates/lance-graph-contract/src/lib.rs index bf6c6523..8799a785 100644 --- a/crates/lance-graph-contract/src/lib.rs +++ b/crates/lance-graph-contract/src/lib.rs @@ -50,12 +50,14 @@ pub mod container; pub mod crystal; pub mod cycle_accumulator; pub mod distance; +pub mod episodic_edges; pub mod escalation; pub mod exploration; pub mod external_membrane; pub mod faculty; pub mod grammar; pub mod graph_render; +pub mod head2head; pub mod hash; pub mod hhtl; pub mod high_heel; @@ -85,6 +87,7 @@ pub mod recipes; pub mod repository; pub mod savants; pub mod scenario; +pub mod scheduler; pub mod sensorium; pub mod sigma_propagation; pub mod sla; @@ -92,6 +95,7 @@ pub mod soa_view; pub mod splat; pub mod tax; pub mod thinking; +pub mod view_angle; pub mod vsa; pub mod witness_table; pub mod world_map; @@ -100,5 +104,9 @@ pub mod world_model; // Re-exports for the most commonly used collapse_gate types. pub use class_view::{ClassId, ClassProjection, ClassView, FieldMask, RenderRow}; pub use collapse_gate::{CollapseGateEmission, GateDecision, MailboxId, MergeMode}; +pub use episodic_edges::{EdgeRef, EpisodicEdges64}; +pub use head2head::{CompetitionOutcome, Head2Head, WinnerCriterion}; pub use kanban::{ExecTarget, KanbanColumn, KanbanMove, RubiconTransitionError}; +pub use scheduler::{DatasetVersion, NextPhaseScheduler, VersionScheduler}; pub use soa_view::{MailboxSoaOwner, MailboxSoaView}; +pub use view_angle::ViewAngle; diff --git a/crates/lance-graph-contract/src/scheduler.rs b/crates/lance-graph-contract/src/scheduler.rs new file mode 100644 index 00000000..46c7a912 --- /dev/null +++ b/crates/lance-graph-contract/src/scheduler.rs @@ -0,0 +1,232 @@ +//! # `scheduler` — the IN-direction reactive seam (`E-SUBSTRATE-IS-THE-SCHEDULER`). +//! +//! The dual of [`crate::soa_view::MailboxSoaOwner`]. The two directions of the +//! Rubicon kanban over the ONE per-mailbox SoA: +//! +//! - **OUT** ([`MailboxSoaOwner::try_advance_phase`](crate::soa_view::MailboxSoaOwner::try_advance_phase)): +//! the ractor owner advances a phase → that commit becomes a Lance dataset +//! **version** → a [`KanbanMove`] (`E-VERSION-ARC-IS-THE-KANBAN`). +//! - **IN** (this module): the reverse subscription — a substrate `LIVE`/scheduled +//! event over `Dataset::versions()` is **lowered to the next legal** +//! [`KanbanMove`], which the owner then applies via `try_advance_phase`. +//! +//! This collapses "build a transparent view" into "LIVE-subscribe + schedule" — +//! the same shape as a CI/PR webhook firing the next job (D-MBX-9, +//! `E-SUBSTRATE-IS-THE-SCHEDULER`). +//! +//! ## Why it lives in the zero-dep contract +//! It composes **only** [`MailboxSoaView`] + [`KanbanColumn`] + [`KanbanMove`] + +//! [`ExecTarget`] — no `lance`, no `surreal`, no async runtime. The CI-gated core +//! impl (`D-MBX-9-IN`: a `LanceVersionScheduler` subscribing to +//! `VersionedGraph::versions()` via the callcenter `LanceVersionWatcher`) lands in +//! a buildable downstream crate; this trait is its airgap. +//! +//! ## Invariant — propose, don't dispose +//! [`VersionScheduler::on_version`] takes `&V` (never `&mut`): the scheduler only +//! **proposes** the next move; the [`MailboxSoaOwner`](crate::soa_view::MailboxSoaOwner) +//! is the sole mutator (R1 "one SoA never transformed"; mirrors the +//! `MailboxSoaView` / `MailboxSoaOwner` read/write split). + +use crate::kanban::{ExecTarget, KanbanColumn, KanbanMove}; +use crate::soa_view::MailboxSoaView; + +/// A monotonic Lance dataset version — the surreal Timeline tick, i.e. one entry +/// of `Dataset::versions()`. The IN-direction event carrier. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)] +pub struct DatasetVersion(pub u64); + +/// Lower a substrate version event into the **next legal** kanban move for a +/// mailbox view, or `None` when no advance is due (the mailbox is in an absorbing +/// column, or the scheduler's policy filters this tick out). +/// +/// The dual of [`crate::soa_view::MailboxSoaOwner`]: a `VersionScheduler` is what a +/// `surreal_container` `LIVE` query (or the callcenter `LanceVersionWatcher`) calls +/// per `versions()` tick to decide whether — and how — the ractor owner should +/// advance the mailbox lifecycle. +pub trait VersionScheduler { + /// Decide the next move for `view` on observing dataset version `at`. `exec` + /// selects the backend the precipitated move runs on + /// ([`ExecTarget::Native`]/[`Jit`](ExecTarget::Jit)/[`SurrealQl`](ExecTarget::SurrealQl)/[`Elixir`](ExecTarget::Elixir)). + /// Returns `None` to schedule no advance (e.g. `view.phase().is_absorbing()`). + fn on_version( + &self, + view: &V, + at: DatasetVersion, + exec: ExecTarget, + ) -> Option; +} + +/// The canonical reference scheduler: on every version, advance the mailbox along +/// the Rubicon **forward arc** — the first legal successor of its current column — +/// or yield `None` when the column is absorbing (`Commit`/`Prune`). +/// +/// The "forward arc" is [`KanbanColumn::next_phases`]`().first()`: +/// `Planning → CognitiveWork`, `CognitiveWork → Evaluation`, `Evaluation → Commit`, +/// `Plan → Planning` (re-deliberate), `Commit`/`Prune` → none. It stamps the Libet +/// anchor (`-550_000 µs`) on the `Planning → CognitiveWork` Σ-commit crossing and +/// `0` elsewhere — matching the `MailboxSoaOwner::advance_phase` convention. +/// +/// This is the substrate-free reference: real schedulers may gate on the +/// `DatasetVersion` delta, choose `Plan`/`Prune` over the forward arc, or batch +/// ticks — they implement [`VersionScheduler`] with their own policy. +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] +pub struct NextPhaseScheduler; + +impl VersionScheduler for NextPhaseScheduler { + fn on_version( + &self, + view: &V, + _at: DatasetVersion, + exec: ExecTarget, + ) -> Option { + let from = view.phase(); + // `next_phases()` is empty exactly for the absorbing columns (Commit/Prune): + // `?` short-circuits to `None`, i.e. "the cycle ended — schedule nothing". + let to = *from.next_phases().first()?; + let libet_offset_us = + if from == KanbanColumn::Planning && to == KanbanColumn::CognitiveWork { + -550_000 + } else { + 0 + }; + Some(KanbanMove { + mailbox: view.mailbox_id(), + from, + to, + // Structural witness position (R4): the monotonic cycle stamp stands in + // for the chain index until the A3 `witness_arc` column lands. + witness_chain_position: view.current_cycle(), + libet_offset_us, + exec, + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::collapse_gate::MailboxId; + + /// Minimal `MailboxSoaView` with a settable phase — proves the scheduler + /// lowers a version event to the right move without any consumer crate + /// (same pattern as `soa_view::tests::FakeSoa`). + struct FakeView { + id: MailboxId, + phase: KanbanColumn, + cycle: u32, + } + impl MailboxSoaView for FakeView { + fn mailbox_id(&self) -> MailboxId { + self.id + } + fn n_rows(&self) -> usize { + 0 + } + fn w_slot(&self) -> u8 { + (self.id & 0x3F) as u8 + } + fn current_cycle(&self) -> u32 { + self.cycle + } + fn phase(&self) -> KanbanColumn { + self.phase + } + fn energy(&self) -> &[f32] { + &[] + } + fn edges_raw(&self) -> &[u64] { + &[] + } + fn meta_raw(&self) -> &[u32] { + &[] + } + fn entity_type(&self) -> &[u16] { + &[] + } + } + + fn view(phase: KanbanColumn) -> FakeView { + FakeView { id: 42, phase, cycle: 9 } + } + + #[test] + fn planning_schedules_cognitive_work_with_libet_anchor() { + let m = NextPhaseScheduler + .on_version(&view(KanbanColumn::Planning), DatasetVersion(1), ExecTarget::Native) + .expect("Planning is not absorbing"); + assert_eq!(m.from, KanbanColumn::Planning); + assert_eq!(m.to, KanbanColumn::CognitiveWork); // forward arc, not the Prune veto + assert_eq!(m.libet_offset_us, -550_000); // the Σ-commit Rubicon crossing + assert_eq!(m.mailbox, 42); + assert_eq!(m.witness_chain_position, 9); // current_cycle stamp + } + + #[test] + fn mid_cycle_advances_carry_no_libet_anchor() { + let cw = NextPhaseScheduler + .on_version(&view(KanbanColumn::CognitiveWork), DatasetVersion(2), ExecTarget::Native) + .unwrap(); + assert_eq!(cw.to, KanbanColumn::Evaluation); + assert_eq!(cw.libet_offset_us, 0); + + let ev = NextPhaseScheduler + .on_version(&view(KanbanColumn::Evaluation), DatasetVersion(3), ExecTarget::Native) + .unwrap(); + assert_eq!(ev.to, KanbanColumn::Commit); // forward arc = calcify + assert_eq!(ev.libet_offset_us, 0); + } + + #[test] + fn plan_re_deliberates_back_to_planning() { + let m = NextPhaseScheduler + .on_version(&view(KanbanColumn::Plan), DatasetVersion(4), ExecTarget::Native) + .unwrap(); + assert_eq!(m.from, KanbanColumn::Plan); + assert_eq!(m.to, KanbanColumn::Planning); // re-enter carrying the witness + } + + #[test] + fn absorbing_columns_schedule_nothing() { + // Commit + Prune are absorbing: the cycle has ended, no move is due. + assert!(NextPhaseScheduler + .on_version(&view(KanbanColumn::Commit), DatasetVersion(5), ExecTarget::Native) + .is_none()); + assert!(NextPhaseScheduler + .on_version(&view(KanbanColumn::Prune), DatasetVersion(6), ExecTarget::Native) + .is_none()); + } + + #[test] + fn exec_target_threads_through_to_the_move() { + // The scheduler carries the backend selection onto the precipitated move + // (the Native/Jit/SurrealQl/Elixir routing tag for the IN-direction). + for exec in [ExecTarget::Native, ExecTarget::Jit, ExecTarget::SurrealQl, ExecTarget::Elixir] { + let m = NextPhaseScheduler + .on_version(&view(KanbanColumn::Planning), DatasetVersion(7), exec) + .unwrap(); + assert_eq!(m.exec, exec); + } + } + + #[test] + fn scheduled_move_is_a_legal_rubicon_edge() { + // Whatever the scheduler proposes MUST be a legal transition the owner's + // `try_advance_phase` will accept (no illegal-edge proposals). + for phase in [ + KanbanColumn::Planning, + KanbanColumn::CognitiveWork, + KanbanColumn::Evaluation, + KanbanColumn::Plan, + ] { + let m = NextPhaseScheduler + .on_version(&view(phase), DatasetVersion(8), ExecTarget::Native) + .unwrap(); + assert!( + m.from.can_transition_to(m.to), + "{:?} -> {:?} must be a legal Rubicon edge", + m.from, + m.to + ); + } + } +} diff --git a/crates/lance-graph-contract/src/view_angle.rs b/crates/lance-graph-contract/src/view_angle.rs new file mode 100644 index 00000000..3b9c971f --- /dev/null +++ b/crates/lance-graph-contract/src/view_angle.rs @@ -0,0 +1,79 @@ +//! # `view_angle` — the attention-angle selector (zero-dep). +//! +//! The class-inherited **presence bitmask** ([`crate::class_view::FieldMask`]) does +//! double duty: it says *which fields are populated* (presence) and therefore *which +//! to attend* (attention) — "attend to what's present" is **structural**, never the +//! forbidden per-instance semantics (`class_view` C2: presence ≠ semantics). +//! +//! A [`ViewAngle`] (≤ 16, a 4-bit nibble) selects **which inherited view-schema +//! attends** — the Quartettkarte "edition" / FAISS-view over the same card. The +//! view-schema for each angle is declared *in the OGIT class* (a leaf/family can bake +//! in N required default angles); resolution flies ABOVE the row: +//! +//! ```text +//! attention(row, angle) = class.view_schema(angle) & row.presence_bitmask +//! └── inherited (OGIT) ──┘ └── per-row presence ──┘ +//! ``` +//! +//! **The line that keeps it RISC:** an angle selects an *inherited* attention +//! pattern; it must NEVER mean something different per row (`class_view` C2). +//! +//! `head2head` ([`crate::head2head`]) competes angles — `DissonanceMin` ≈ infight, +//! `SupportSpread` ≈ Raumgewinn — picking which lens wins for a story. + +/// A 4-bit view-schema selector (`0..16`). The *meaning* of each angle is declared in +/// the OGIT class, not here; this is only the agnostic index. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)] +pub struct ViewAngle(u8); + +impl ViewAngle { + /// Number of distinct angles a 4-bit selector addresses. + pub const MAX: u8 = 16; + + /// A validated angle, or `None` if `angle ≥ 16`. + #[must_use] + pub const fn new(angle: u8) -> Option { + if angle < Self::MAX { + Some(Self(angle)) + } else { + None + } + } + + /// The canonical / default view (angle `0`) — the view every shape answers. + #[must_use] + pub const fn canonical() -> Self { + Self(0) + } + + /// The raw 0-based angle index (to key the OGIT class's per-angle view-schema). + #[must_use] + pub const fn index(self) -> u8 { + self.0 + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn new_bounds_to_four_bits() { + assert!(ViewAngle::new(0).is_some()); + assert!(ViewAngle::new(15).is_some()); + assert!(ViewAngle::new(16).is_none()); + } + + #[test] + fn canonical_is_angle_zero_and_default() { + assert_eq!(ViewAngle::canonical().index(), 0); + assert_eq!(ViewAngle::default(), ViewAngle::canonical()); + } + + #[test] + fn index_round_trips() { + for a in 0..ViewAngle::MAX { + assert_eq!(ViewAngle::new(a).unwrap().index(), a); + } + } +}