Skip to content

Commit a6fbe10

Browse files
committed
feat(deepnsm): arcuate connector — the Broca<->Wernicke cable carries signal (E-ARCUATE-CONDUCTION)
First increment of the conduction-aphasia fix. Arcuate owns the MarkovBundler producer + the ±5 ContextChain evidence ring; feed(sentence) pushes to the bundler and, on each emitted Trajectory, sign-binarizes the projection and slides it into the ring's newest slot; disambiguate(candidates) delegates to the chain. This gives MarkovBundler::push its first caller and owns the ring-slide the contract leaves to the language side (ContextChain has fill + coherence + replay but no streaming advance) -- so the projection now flows from Broca into Wernicke's window. Separate seam, NOT wired into pipeline.rs's live 512-bit ContextWindow (that coexistence is a distinct decision, deferred to avoid spaghetti). Firewall: only Binary16K crosses into the contract; no COCA; no new dep. deepnsm lib 99 green (+4 arcuate); arcuate.rs default-clippy-clean. OQ-ARC-WINDOW: double-windowing (bundler ±radius + chain ±5) — ring holds windowed-projection fingerprints; per-sentence (radius-0) may be preferable. https://claude.ai/code/session_012SorR8UbtEvYmbX8cXftj7
1 parent bbb0ca9 commit a6fbe10

4 files changed

Lines changed: 199 additions & 0 deletions

File tree

.claude/board/AGENT_LOG.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,17 @@
1+
## [Main thread / Opus] arcuate connector — the Broca↔Wernicke cable carries signal (E-ARCUATE-CONDUCTION, first fix)
2+
3+
**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.
4+
5+
**Shipped:** NEW `crates/deepnsm/src/arcuate.rs` + `lib.rs` mod decl. `Arcuate{MarkovBundler + ContextChain}`: `feed(WindowedSentence)→Option<Trajectory>` 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.
6+
7+
**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`).
8+
9+
**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`).
10+
11+
**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.
12+
13+
---
14+
115
## [Main thread / Opus] full language-network map + conduction-aphasia diagnosis (E-ARCUATE-CONDUCTION)
216

317
**Branch:** claude/jolly-cori-clnf9. Design-only (map + diagnosis; no code). User extended Broca/Wernicke/Hippocampus to the full distributed language network (10 landmarks).

.claude/knowledge/english-fact-story-bifurcation-grail-v1.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -273,3 +273,11 @@ Broca ───────────┼──── Arcuate Fasciculus ──
273273
**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`.
274274

275275
**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).
276+
277+
---
278+
279+
## Session update — 2026-05-31 (arcuate connector shipped — the cable carries signal)
280+
281+
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).
282+
283+
**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).

crates/deepnsm/src/arcuate.rs

Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
//! The **arcuate fasciculus** — the Broca↔Wernicke cable made to carry signal
2+
//! (closes the conduction-aphasia gap, `E-ARCUATE-CONDUCTION`).
3+
//!
4+
//! ## What it does
5+
//!
6+
//! Owns both ends of the cable and slides the projection into the evidence ring:
7+
//! - **Broca / producer** — a `MarkovBundler`. Each fed sentence advances its
8+
//! window; once saturated it emits a `Trajectory` (the role-superposed
9+
//! projection wave).
10+
//! - **Wernicke / evidence ring** — a contract `ContextChain` (the ±5 replay
11+
//! surface). Each emitted projection is sign-binarized and **slid** into the
12+
//! ring's newest slot.
13+
//!
14+
//! The diagnosis it fixes: `disambiguator_glue` is the cable, and the contract
15+
//! `ContextChain` gives fill + coherence + replay primitives — but **no
16+
//! streaming advance**, and `MarkovBundler::push` had no caller. So the cable
17+
//! existed but carried no signal (production + comprehension intact, *repetition*
18+
//! failing — textbook conduction aphasia). `Arcuate` owns the producer + the
19+
//! ring-slide, so the projection now flows from Broca into Wernicke's window.
20+
//!
21+
//! ## Scope (deliberate, anti-spaghetti)
22+
//!
23+
//! This is a SEPARATE seam — it is **not** wired into `pipeline.rs`'s live
24+
//! 512-bit `ContextWindow`. How the two coexist is a distinct decision; fusing
25+
//! them here would be the spaghetti the design explicitly avoids. The connector
26+
//! is offline-testable on its own.
27+
//!
28+
//! ## Firewall
29+
//!
30+
//! The only thing crossing into the contract is a `Binary16K` fingerprint (the
31+
//! sign-binarized projection) — never a COCA rank. The contract takes no
32+
//! `deepnsm` dependency; `deepnsm` injects through the existing fingerprint seam.
33+
//! Double-windowing note: the bundler does ±radius bundling and the chain is ±5,
34+
//! so the ring holds windowed-projection fingerprints — adequate to carry signal;
35+
//! whether per-sentence (radius-0) fingerprints are preferable is `OQ-ARC-WINDOW`.
36+
37+
use crate::disambiguator_glue::sign_binarize_to_binary16k;
38+
use crate::markov_bundle::{Kernel, MarkovBundler, WindowedSentence};
39+
use crate::trajectory::Trajectory;
40+
41+
use lance_graph_contract::crystal::fingerprint::CrystalFingerprint;
42+
use lance_graph_contract::grammar::context_chain::{
43+
ContextChain, DisambiguateOpts, DisambiguationResult,
44+
};
45+
46+
/// The arcuate connector: a `MarkovBundler` producer feeding a ±5
47+
/// `ContextChain` evidence ring.
48+
pub struct Arcuate {
49+
bundler: MarkovBundler,
50+
chain: ContextChain,
51+
}
52+
53+
impl Arcuate {
54+
/// New connector with bundler `radius` + `kernel`. The ring is the
55+
/// contract's fixed ±5 (`CHAIN_LEN = 11`), independent of `radius`.
56+
#[must_use]
57+
pub fn new(radius: u32, kernel: Kernel) -> Self {
58+
Self {
59+
bundler: MarkovBundler::new(radius, kernel),
60+
chain: ContextChain::new(),
61+
}
62+
}
63+
64+
/// Feed one sentence's windowed tokens (Broca). When the bundler window
65+
/// saturates it emits a `Trajectory` (the projection); that projection is
66+
/// sign-binarized and slid into the ±5 ring (the cable carrying signal).
67+
/// Returns the emitted projection, or `None` while the window still fills.
68+
pub fn feed(&mut self, sentence: WindowedSentence) -> Option<Trajectory> {
69+
let traj = self.bundler.push(sentence)?;
70+
let bits = sign_binarize_to_binary16k(&traj.fingerprint);
71+
self.slide_in(CrystalFingerprint::Binary16K(bits));
72+
Some(traj)
73+
}
74+
75+
/// Slide the ±5 ring forward by one: drop the oldest slot, append the
76+
/// newest (so the newest sits at the last index and the focal at index 5
77+
/// trails it by five). The contract offers no streaming advance, so the
78+
/// connector owns the ring via the chain's public `fingerprints`.
79+
fn slide_in(&mut self, fp: CrystalFingerprint) {
80+
self.chain.fingerprints.remove(0);
81+
self.chain.fingerprints.push(Some(fp));
82+
}
83+
84+
/// The ±5 evidence ring (Wernicke's replay surface).
85+
#[must_use]
86+
pub fn chain(&self) -> &ContextChain {
87+
&self.chain
88+
}
89+
90+
/// Disambiguate at the focal position against `candidates` (the ±5 replay).
91+
/// Delegates to the contract chain; the populated ring is the evidence.
92+
pub fn disambiguate<I>(&self, candidates: I) -> DisambiguationResult
93+
where
94+
I: IntoIterator<Item = CrystalFingerprint>,
95+
{
96+
self.chain.disambiguate_with(
97+
ContextChain::focal_index(),
98+
candidates,
99+
DisambiguateOpts::default(),
100+
)
101+
}
102+
}
103+
104+
#[cfg(test)]
105+
mod tests {
106+
use super::*;
107+
use crate::markov_bundle::{GrammaticalRole, TokenWithRole};
108+
109+
/// One SUBJECT-band sentence whose content distinguishes it by `value`.
110+
fn sentence(value: f32) -> WindowedSentence {
111+
let len = GrammaticalRole::Subject.slice().len();
112+
WindowedSentence {
113+
tokens: vec![TokenWithRole {
114+
content_fp: vec![value; len],
115+
role: GrammaticalRole::Subject,
116+
}],
117+
}
118+
}
119+
120+
fn fp(value: f32) -> CrystalFingerprint {
121+
CrystalFingerprint::Binary16K(sign_binarize_to_binary16k(&vec![value; 16_384]))
122+
}
123+
124+
#[test]
125+
fn window_fills_before_first_projection() {
126+
// radius 2 → bundler needs 2*2+1 = 5 pushes before the first emit.
127+
let mut arc = Arcuate::new(2, Kernel::Uniform);
128+
for _ in 0..4 {
129+
assert!(arc.feed(sentence(1.0)).is_none());
130+
}
131+
assert_eq!(arc.chain().filled(), 0, "ring untouched while window fills");
132+
}
133+
134+
#[test]
135+
fn projection_slides_into_ring_when_window_saturates() {
136+
let mut arc = Arcuate::new(2, Kernel::Uniform);
137+
let mut emitted = None;
138+
for _ in 0..5 {
139+
emitted = arc.feed(sentence(1.0));
140+
}
141+
assert!(emitted.is_some(), "5th feed (radius 2) emits a projection");
142+
assert_eq!(arc.chain().filled(), 1, "one projection slid into the ring");
143+
// Newest occupies the last slot; ring length stays at the contract ±5.
144+
assert_eq!(arc.chain().fingerprints.len(), 11);
145+
assert!(arc.chain().fingerprints[10].is_some(), "newest at the tail");
146+
}
147+
148+
#[test]
149+
fn ring_saturates_and_keeps_length_after_many_feeds() {
150+
let mut arc = Arcuate::new(2, Kernel::Uniform);
151+
// 5 to warm up + 11 emits to fill all slots = 16 feeds; vary content so
152+
// the ring is not degenerate.
153+
for i in 0..16 {
154+
arc.feed(sentence(1.0 + i as f32));
155+
}
156+
assert_eq!(arc.chain().fingerprints.len(), 11);
157+
assert!(arc.chain().is_saturated(), "ring full after ≥15 emits");
158+
assert!(arc.chain().focal().is_some(), "focal slot carries signal");
159+
}
160+
161+
#[test]
162+
fn disambiguate_over_populated_ring_ranks_candidates() {
163+
let mut arc = Arcuate::new(2, Kernel::Uniform);
164+
for i in 0..16 {
165+
arc.feed(sentence(1.0 + i as f32));
166+
}
167+
let result = arc.disambiguate([fp(1.0), fp(-1.0)]);
168+
assert_eq!(result.candidate_count, 2, "both candidates evaluated");
169+
assert!(result.winner_index < 2, "a real winner over the ±5 evidence");
170+
}
171+
}

crates/deepnsm/src/lib.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,12 @@ pub mod nsm_primes;
7474
pub mod arcs;
7575
pub mod comprehension;
7676

77+
// E-ARCUATE-CONDUCTION: the arcuate fasciculus — owns the MarkovBundler
78+
// producer + the ±5 ContextChain ring and slides the projection into it, so
79+
// the Broca↔Wernicke cable carries signal. Separate seam; NOT wired into
80+
// pipeline.rs's live ContextWindow (that coexistence is a distinct decision).
81+
pub mod arcuate;
82+
7783
// Loose-end-#2 closer (PR-G3): glue from MarkovBundler::role_bundle()
7884
// → ContextChain::disambiguate_with(.., DisambiguateOpts {
7985
// sentinel_fp }). Closes the "real fp" honesty gap by giving the

0 commit comments

Comments
 (0)