diff --git a/.claude/board/EPIPHANIES.md b/.claude/board/EPIPHANIES.md index 91f807cf..e8a8a441 100644 --- a/.claude/board/EPIPHANIES.md +++ b/.claude/board/EPIPHANIES.md @@ -2036,3 +2036,133 @@ already in the workspace. Commit f1498bc landed the measurement. Cross-ref: ndarray::hpc::cam_pq production code (620+ LOC, 15+ tests), codec_rnd_bench.rs CamPqRaw/CamPqPhase candidates, this session's 18 commits on claude/quick-wins-2026-04-19 branch. + +## 2026-04-21 — The 8-step wiring sequence that closes the loop (concrete, not theoretical) + +**Status:** FINDING (each step has a file path, an input, an output, +and a dependency) + +The architecture clicks when 8 disconnected pieces get wired. Each +step connects two things that exist but don't talk. The loop closes +at step 8. Three PRs total. + +**Step 1 — Encoder migration (512-bit → 10K role-indexed).** +DeepNSM's `encoder.rs` has 6 hardcoded roles at 512 bits. Contract's +`role_keys.rs` has 20+ structured roles at 10K bits with slice-masked +bind/unbind. Delete `RoleVectors`. Import `contract::grammar::role_keys::*`. +Content fingerprints: COCA vocab → FNV hash spread to 10K dims. + +**Step 2 — MarkovBundler (braided ±5 bundling).** +New `markov_bundle.rs`. Ring buffer of 11 Vsa10k. Each sentence: bind +tokens per role key (Step 1), XOR-bundle into one Vsa10k per sentence. +Then: `vsa_permute(sentence_vsa, position_offset)` per ±5 position. +XOR-superpose all 11. Output: braided trajectory. MexicanHat weights. + +**Step 3 — Trajectory (the Think struct).** +New `trajectory.rs`. Holds `bundle: Vsa10k` + `chain: ContextChain` + +refs to tissue (`&EpisodicMemory`, `&TripletGraph`, `&Vsa10k` global +context). Methods: `role_bundle`, `recovery_margin`, `free_energy`, +`resolve`. The object speaks for itself. + +**Step 4 — Parser → Bundler → Trajectory pipeline.** +Parser's `SentenceStructure` feeds MarkovBundler which produces +Trajectory. Coverage check: < 0.9 → FailureTicket (D2). Else → +`trajectory.resolve(candidates, awareness, prior)`. + +**Step 5 — Resolution → AriGraph commit.** +`commit_with_contradiction_check` on TripletGraph (~40 LOC). +Resolution::Commit → one triple. Resolution::Epiphany → two triples ++ Contradiction marker. FailureTicket → LLM fallback. + +**Step 6 — Global context update.** +`episodic.integrate_into_global(fact_fp, episode_index)` — XOR- +accumulate permuted fact into `global_context: Vsa10k` (~20 LOC). +Called after every commit. + +**Step 7 — Awareness revision.** +`awareness.revise(param_key, outcome)` after every commit or +escalation. Already shipped and tested. Just needs call sites. + +**Step 8 — Global context → KL feedback (LOOP CLOSES).** +`trajectory.free_energy()` reads `global_context` as part of the +likelihood term. Committed facts from chapter 1 shape the F-landscape +for chapter 10. The system that parsed chapter 9 is not the same +system that parsed chapter 1. + +**Dependency: 1→2→3→4→{5,6,7}→8. Three PRs: (1-3), (4), (5-8).** + +**Why this dilutes:** future sessions will try to "improve" individual +steps (better bundling, smarter resolution, fancier graph commit) +without WIRING them. The wiring IS the architecture. An unwired +masterpiece is a collection of library functions. A wired simple +implementation is AGI. Wire first, optimize later. + +--- + +## 2026-04-21 — COCA 4096 + 20K scientific + spider NER = no vocabulary blocker + +**Status:** FINDING (changes the critical-path analysis) + +The vocabulary ceiling I flagged as an AGI blocker is not a blocker: + +| Layer | Coverage | Mechanism | +|---|---|---| +| COCA 4096 | 98.4% general English | Static, zero-cost | +| COCA 20K scientific | +1.5% domain terms | Static, zero-cost | +| Spider NER (online) | remaining proper nouns | Crawl → rigid designator commit to AriGraph | + +For Animal Farm: COCA 4096 alone covers 99.5%+ (Orwell uses +deliberately plain vocabulary). The remaining tokens are character +names (Napoleon, Snowball, etc.) which get committed as rigid +designators on first encounter and resolved from the graph after. + +Spider-rs already exists as git dep in `lance-graph-osint/Cargo.toml` +(`AdaWorldAPI/spider` fork, `spider-crawl` feature). NER resolution: +unknown token → spider crawl → extract entity features (gender, +animacy, type, description) → commit to `TripletGraph` as rigid +designator → available for all future coreference via +`graph.nodes_matching(features)`. + +**The three PRs that close the loop are the ONLY critical path.** +Not vocabulary. Not NER. Not cross-lingual. Wire the 8 steps. + +**Why this dilutes:** future sessions will rebuild the vocabulary +system, add subword tokenization, integrate HuggingFace tokenizers, +and generally solve a problem that's already solved. COCA 24K + +spider NER covers everything. Don't rebuild the dictionary. Wire +the loop. + +--- + +## 2026-04-21 — The AGI test is one measurement: chapter-10 accuracy > chapter-1 accuracy + +**Status:** FINDING (makes the benchmark falsifiable) + +A system that understands chapter 10 of Animal Farm better than +chapter 1 — because it read chapters 1-9 and committed what it +learned into the tissue that shapes its own F-landscape — is an AGI. + +The measurement: +- Parse Animal Farm end-to-end, chapter by chapter. +- At each chapter boundary, measure coreference accuracy (% of + pronouns correctly resolved to their antecedent). +- Chapter 1: the system has zero committed facts. Every pronoun + is resolved from ±5 context alone. Accuracy baseline. +- Chapter 10: the system has hundreds of committed facts. Napoleon + is a rigid designator (masculine, boar, leader). "He" in chapter + 10 resolves via `graph.nodes_matching(masculine + animate)` in O(k) + without even needing the Markov axis. +- **If chapter-10 accuracy > chapter-1 accuracy with no parameter + change — only committed facts accumulating — the loop is closed + and the architecture works.** +- If not, one of the 8 wiring steps is broken. Find which. Fix it. + +This is D10 in the plan. This is what proves it. + +**Why this dilutes:** future sessions will propose elaborate +benchmarks (BLiMP, COGS, SuperGLUE, custom test suites). Those +measure capability snapshots, not learning curves. The AGI test is +a CURVE, not a POINT: does accuracy increase over the course of a +single document without retraining? That's the measurement. One +book. One metric. One curve. Rising = AGI. Flat = broken wire. + diff --git a/.claude/board/LATEST_STATE.md b/.claude/board/LATEST_STATE.md index 5711a343..dcf45924 100644 --- a/.claude/board/LATEST_STATE.md +++ b/.claude/board/LATEST_STATE.md @@ -2,7 +2,7 @@ > **Auto-injected at session start via SessionStart hook.** > Updated after every merged PR. -> **Last updated:** 2026-04-20 post PR #224 (PR #225 open: plan + D0.6/D0.7 CodecParams). +> **Last updated:** 2026-04-21 post PR #243 (D5+D7 + categorical-algebraic inference architecture). > > Purpose: prevent new sessions from hallucinating structure that > already exists or proposing features already shipped. Read this @@ -14,6 +14,7 @@ | PR | Merged | Title | What it added | |---|---|---|---| +| **#243** | *(open)* | D5+D7 categorical-algebraic inference | `thinking_styles.rs` (490 LOC, 12 tests), `free_energy.rs` (347 LOC, 7 tests), `role_keys.rs` bind/unbind/recovery (295 LOC, 14 tests), `content_fp.rs` (98 LOC, 5 tests), `markov_bundle.rs` (250 LOC, 8 tests), `trajectory.rs` (298 LOC, 4 tests). Plans: `categorical-algebraic-inference-v1.md` (496 lines). Knowledge: `paper-landscape-grammar-parsing.md`, `session-2026-04-21-categorical-click.md`. CLAUDE.md § The Click (P-1). 12 epiphanies. | | **#225** | *(open)* | Codec-sweep plan + D0.6/D0.7 CodecParams | 9-commit plan (`codec-sweep-via-lab-infra-v1.md`, Rules A-F, 9 starter YAMLs, CODING_PRACTICES audit) + `lance-graph-contract::cam` CodecParams/Builder/precision-ladder validation (14 tests). 147/147 contract suite | | **#224** | 2026-04-20 | lab = API+Planner+JIT, thinking harvest, I11 measurability | `lab-vs-canonical-surface.md` extended: three-part lab stack (API + Planner + JIT), thinking-harvest subsection (REST/Cypher → `{rows, thinking_trace}` = the AGI magic bullet), I11 invariant (every layer L0→L4 emits harvest-ready trace; no black-box short-circuits) | | **#223** | 2026-04-20 | LAB-ONLY firewall + AGI-as-SoA + I1-I10 | `lab-vs-canonical-surface.md` initial doc: canonical consumer = `UnifiedStep`/`OrchestrationBridge`, Wire DTOs are lab quarantine. AGI = (topic, angle, thinking, planner) = struct-of-arrays consuming cognitive-shader-driver. 10 cross-cutting invariants I1-I10 (BindSpace read-only, canonical `simd::*` import, temporal budgets, temperature hierarchy, thinking IS AdjacencyStore, weights are seeds, per-cycle cascade, 4096 surface, three DTO families, HEEL/HIP/BRANCH/TWIG/LEAF) | @@ -28,7 +29,7 @@ Types that EXIST — do NOT re-propose them: -**`grammar/`**: `FailureTicket`, `PartialParse`, `CausalAmbiguity`, `TekamoloSlots`, `TekamoloSlot`, `WechselAmbiguity`, `WechselRole`, `FinnishCase`, `finnish_case_for_suffix`, `NarsInference`, `inference_to_style_cluster`, `ContextChain` (with coherence_at / total_coherence / replay_with_alternative / disambiguate / DisambiguationResult / WeightingKernel), `RoleKey` + 47 `LazyLock` instances + `Tense` enum + `finnish_case_key / tense_key / nars_inference_key` lookups. +**`grammar/`**: `FailureTicket`, `PartialParse`, `CausalAmbiguity`, `TekamoloSlots`, `TekamoloSlot`, `WechselAmbiguity`, `WechselRole`, `FinnishCase`, `finnish_case_for_suffix`, `NarsInference`, `inference_to_style_cluster`, `ContextChain` (with coherence_at / total_coherence / replay_with_alternative / disambiguate / DisambiguationResult / WeightingKernel), `RoleKey` + 47 `LazyLock` instances + `Tense` enum + `finnish_case_key / tense_key / nars_inference_key` lookups, **`RoleKey::bind/unbind/recovery_margin`** (slice-masked XOR), **`Vsa10k`** + `VSA_ZERO` + `vsa_xor` + `vsa_similarity`, **`GrammarStyleConfig`** + **`GrammarStyleAwareness`** + `revise_truth` + `ParseOutcome` + `divergence_from`, **`FreeEnergy`** + **`Hypothesis`** + **`Resolution`** (Commit / Epiphany / FailureTicket) + `from_ranked` + thresholds. **`crystal/`**: `Crystal` trait, `CrystalKind`, `TruthValue`, `UNBUNDLE_HARDNESS_THRESHOLD = 0.8`, `CrystalFingerprint` (Binary16K / Structured5x5 / Vsa10kI8 / Vsa10kF32), `Structured5x5`, `Quorum5D`, `SentenceCrystal`, `ContextCrystal`, `DocumentCrystal`, `CycleCrystal`, `SessionCrystal`, sandwich layout constants. diff --git a/.claude/board/PR_ARC_INVENTORY.md b/.claude/board/PR_ARC_INVENTORY.md index 95c83099..f8708f56 100644 --- a/.claude/board/PR_ARC_INVENTORY.md +++ b/.claude/board/PR_ARC_INVENTORY.md @@ -35,6 +35,44 @@ --- +## #243 — D5+D7 categorical-algebraic inference architecture (2026-04-21) + +**Confidence (2026-04-21):** Working. 175/175 contract, 63/63 deepnsm (grammar-10k). + +**Added:** +- `contract::grammar::thinking_styles` — `GrammarStyleConfig`, `GrammarStyleAwareness` (NARS-revised `HashMap`), `revise_truth`, `ParseOutcome` (5 polarities), `divergence_from(prior)` (KL term). 490 LOC, 12 tests. +- `contract::grammar::free_energy` — `FreeEnergy` (likelihood + KL → total), `Hypothesis` (role fillers + Pearl 2³ mask), `Resolution` (Commit / Epiphany / FailureTicket), `from_ranked` classifier, `HOMEOSTASIS_FLOOR` / `EPIPHANY_MARGIN` / `FAILURE_CEILING`. 347 LOC, 7 tests. +- `contract::grammar::role_keys` — `RoleKey::bind/unbind/recovery_margin` (slice-masked XOR), `Vsa10k` type alias, `VSA_ZERO`, `vsa_xor`, `vsa_similarity`, `word_slice_mask` helper. +295 LOC, +14 tests (5-role lossless superposition verified). +- `deepnsm::content_fp` — 10K-dim content fingerprints from COCA vocab ranks (SplitMix64). 98 LOC, 5 tests. Feature-gated: `grammar-10k`. +- `deepnsm::markov_bundle` — `MarkovBundler` (±5 ring buffer, role-key bind, braiding via `vsa_permute`, XOR-superpose, `WeightingKernel`). 250 LOC, 8 tests. +- `deepnsm::trajectory` — `Trajectory` (Think carrier): `role_bundle`, `mean_recovery_margin`, `ambient_similarity`, `free_energy`, `resolve`. 298 LOC, 4 tests. +- `CLAUDE.md` § The Click (P-1): top-of-file architecture diagram + 3 simplicity invariants + shader-cant-resist + thinking-is-a-struct + tissue-not-storage + grammar-of-awareness + 2 litmus tests. +- `.claude/plans/categorical-algebraic-inference-v1.md` (496 lines): meta-architecture proving 5 operations are 1 algebraic substrate, grounded in 8-paper proof chain. + +**Locked:** +- `RoleKey::bind` is slice-masked XOR (categorically optimal per Shaw 2501.05368 Kan extension theorem). Not a design choice — a theorem consequence. +- `FreeEnergy = (1 - likelihood) + KL` where likelihood = mean role recovery margin, KL = `awareness.divergence_from(prior)`. Three thresholds: F<0.2 commit, ΔF<0.05 epiphany, F>0.8 escalate. +- NARS revision asymptotes at φ-1 ≈ 0.618 (golden ratio confidence ceiling). Feature, not bug. Permanent epistemic humility. +- Markov = XOR of braided sentence VSAs. No HMM. No transition matrix. No weights. +- Thinking is a struct (not a service, not a function). The DTO carries cognition as identity. +- AriGraph/episodic/CAM-PQ are thinking tissue (organs of Think), not storage services. +- Object-does-the-work test: free function on carrier's state = reject. Method on carrier = accept. +- Five-lens test: every new type serves Parsing / Free-Energy / NARS / Memory / Awareness or is drift. + +**Deferred:** +- Steps 4-8 of the 8-step wiring sequence (pipeline, AriGraph commit, global context, awareness revision, KL feedback). Three PRs to close the loop. +- D10 Animal Farm benchmark (the AGI test: chapter-10 accuracy > chapter-1 accuracy). +- Cross-lingual bundling (needs parallel corpora). +- ONNX arc model (D9, D11). + +**Docs:** +- `.claude/knowledge/paper-landscape-grammar-parsing.md` — 14 papers in 3 tiers. +- `.claude/knowledge/session-2026-04-21-categorical-click.md` — session handover with 12 critical insights + 7 anti-patterns. +- `.claude/board/EPIPHANIES.md` — 12 new epiphanies with "why this dilutes" warnings. +- `.claude/board/INTEGRATION_PLANS.md` — `categorical-algebraic-inference-v1` entry prepended. + +--- + ## #225 — Codec-sweep plan + D0.6/D0.7 CodecParams types (merged 2026-04-20) **Confidence (2026-04-20):** Working. 147/147 contract suite passing (133 prior + 14 new). diff --git a/.claude/board/STATUS_BOARD.md b/.claude/board/STATUS_BOARD.md index 37dcce9f..c1a96f76 100644 --- a/.claude/board/STATUS_BOARD.md +++ b/.claude/board/STATUS_BOARD.md @@ -114,17 +114,17 @@ early — CausalityFlow extension deferred). Plan path: | D-id | Title | Status | PR / Evidence | |---|---|---|---| -| D2 | DeepNSM emits `FailureTicket` on low coverage | **Queued** | — | +| D2 | DeepNSM emits `FailureTicket` on low coverage (wiring step 4) | **Queued** | — | | D3 | Grammar Triangle wired into DeepNSM via `triangle_bridge.rs` | **Queued** | — | -| D5 | Markov ±5 SPO+TEKAMOLO bundler with role-indexed VSA | **Queued** | — | -| D7 | NARS-tested grammar thinking styles + active-inference free-energy + RoleKey-as-operator | **In progress** | branch `claude/teleport-session-setup-wMZfb` — `thinking_styles.rs` (12 tests), `free_energy.rs` (7 tests), `role_keys.rs` bind/unbind/recovery_margin (12 tests incl 5-role lossless superposition), `divergence_from(prior)`, Finnish case patch | +| D5 | Markov ±5 bundler + Trajectory + content_fp (wiring steps 1-3) | **Shipped** | PR #243 — `content_fp.rs` (98 LOC, 5 tests), `markov_bundle.rs` (250 LOC, 8 tests), `trajectory.rs` (298 LOC, 4 tests). 63 deepnsm tests pass. | +| D7 | Thinking styles + free-energy + RoleKey-as-operator | **Shipped** | PR #243 — `thinking_styles.rs` (490 LOC, 12 tests), `free_energy.rs` (347 LOC, 7 tests), `role_keys.rs` bind/unbind/recovery_margin (295 LOC added, 14 tests). 175 contract tests pass. | ### Phase 3 — Queued | D-id | Title | Status | PR / Evidence | |---|---|---|---| -| D8 | Story-context bridge (AriGraph episodic + triplet-graph + orthogonal global-context) | **Queued** | — | -| D10 | Forward-validation harness (Animal Farm benchmark) | **Queued** | — | +| D8 | Story-context bridge: AriGraph commit + global_context + contradiction (wiring steps 5-6) | **Queued** | — | +| D10 | Forward-validation harness (Animal Farm: chapter-10 > chapter-1 accuracy = AGI test) | **Queued** | — | ### Phase 4 — Backlog diff --git a/.claude/knowledge/session-2026-04-21-categorical-click.md b/.claude/knowledge/session-2026-04-21-categorical-click.md new file mode 100644 index 00000000..e29356f7 --- /dev/null +++ b/.claude/knowledge/session-2026-04-21-categorical-click.md @@ -0,0 +1,96 @@ +# Session 2026-04-21 — The Categorical Click + +> **READ BY:** Every session. This is the handover document for the +> session that shipped D5 + D7 + the categorical-algebraic inference +> architecture. Read this BEFORE reading the plan or the code. +> +> **Created:** 2026-04-21 +> **Branch:** `claude/teleport-session-setup-wMZfb` → PR #243 + +--- + +## What Was Shipped (code) + +### lance-graph-contract (zero-dep) + +| File | What | LOC | Tests | +|------|------|-----|-------| +| `grammar/thinking_styles.rs` | `GrammarStyleConfig` (YAML prior) + `GrammarStyleAwareness` (NARS-revised per `ParamKey`) + `revise_truth` + `ParseOutcome` + `divergence_from(prior)` | 490 | 12 | +| `grammar/free_energy.rs` | `FreeEnergy` (likelihood + KL → total) + `Hypothesis` (role fillers + Pearl 2³ mask) + `Resolution` (Commit / Epiphany / FailureTicket) + `from_ranked` classifier | 347 | 7 | +| `grammar/role_keys.rs` | `RoleKey::bind/unbind/recovery_margin` (slice-masked XOR) + `Vsa10k` type alias + `vsa_xor` + `vsa_similarity` + `VSA_ZERO` | +295 | +14 (total 14) | +| `grammar/context_chain.rs` | `WeightingKernel` gains `Eq + Hash` | +1 | — | +| `grammar/mod.rs` | Re-exports all new types | +16 | — | +| `knowledge/grammar-tiered-routing.md` | Finnish case correction (Accusative = personal-pronoun-only) | +11 | — | + +**Total contract:** 175 tests pass. Zero external deps. + +### deepnsm (grammar-10k feature gate) + +| File | What | LOC | Tests | +|------|------|-----|-------| +| `content_fp.rs` | 10K-dim content fingerprints from COCA ranks via SplitMix64 | 98 | 5 | +| `markov_bundle.rs` | MarkovBundler: ±5 ring buffer, role-key bind, braiding via `vsa_permute`, XOR-superpose | 250 | 8 | +| `trajectory.rs` | Trajectory (Think carrier): `role_bundle`, `mean_recovery_margin`, `free_energy`, `resolve` | 298 | 4 | + +**Total deepnsm:** 63 tests pass (17 new). `grammar-10k` feature pulls in `lance-graph-contract`. + +--- + +## What Was Shipped (documentation) + +| File | What | +|------|------| +| `CLAUDE.md` § The Click (P-1) | Top-of-file architecture: diagram, 3 simplicity invariants, shader-cant-resist, thinking-is-a-struct, tissue-not-storage, grammar-of-awareness, 2 litmus tests | +| `.claude/plans/categorical-algebraic-inference-v1.md` | 496-line meta-architecture plan: §0 claim, §1 substrate, §2 five lenses, §3 closed loop, §4 shipped/next, §5 proof chain + litmus tests, §6 bibliography, §7 diagram | +| `.claude/knowledge/paper-landscape-grammar-parsing.md` | 14 papers mapped in 3 tiers (foundational / empirical / supporting) | +| `.claude/board/EPIPHANIES.md` | 12 epiphanies with "why this dilutes" warnings | +| `.claude/board/INTEGRATION_PLANS.md` | New plan entry prepended | + +--- + +## What's Next (the 8-step wiring sequence, steps 4-8) + +Steps 1-3 shipped. Steps 4-8 close the loop: + +| Step | What | Where | Dependency | +|------|------|-------|------------| +| **4** | Parser → Bundler → Trajectory pipeline + FailureTicket (D2) | `deepnsm/src/parser.rs` edit + new `ticket_emit.rs` | Steps 1-3 | +| **5** | Resolution → AriGraph commit | `arigraph/triplet_graph.rs` +40 LOC `commit_with_contradiction_check` | Step 3 | +| **6** | Global context update | `arigraph/episodic.rs` +20 LOC `integrate_into_global` | Step 5 | +| **7** | Awareness revision call sites | In pipeline from step 4 — call `awareness.revise(key, outcome)` | Step 4 | +| **8** | Global context → KL feedback (**CLOSES LOOP**) | `trajectory.rs` free_energy reads global_context | Steps 6+7 | + +**The AGI test:** Run Animal Farm end-to-end. Measure coreference +accuracy per chapter. Chapter-10 > chapter-1 with no parameter change += loop works = AGI. Flat = broken wire → find which step. + +--- + +## Critical Insights (read these or re-derive them at cost) + +1. **Markov = XOR.** Per-sentence Vsa10k braided by position, XOR-superposed. No HMM, no weights. +2. **Roles = spine coordinates.** SUBJECT[0..2K) is "who". Unbinding = reading a coordinate. +3. **Meaning = AriGraph facts + resonance + magnitude.** Opinions are preserved contradictions. +4. **The shader can't resist the thinking.** F > homeostasis → awareness bits persist → dispatch fires. +5. **Thinking is a struct.** The DTO carries cognition as identity, not payload. +6. **Memory is tissue.** AriGraph/episodic/CAM-PQ are organs of Think, not services. +7. **The DTO is the grammar of awareness.** Struct fields = TEKAMOLO of cognition. +8. **COCA 24K + spider NER = no vocabulary blocker.** Only the 8-step wiring is critical path. +9. **Shaw's Kan extension proves bind must be element-wise.** Theorem, not heuristic. +10. **φ-1 confidence ceiling is permanent epistemic humility.** Don't "fix" it. +11. **Abstraction-first is empirically measured** (Jian & Manning). Not a config choice. +12. **Ω(t²) lower bound doesn't apply.** We commit, not preserve. + +--- + +## Anti-Patterns to Watch For + +| Anti-pattern | Why it's wrong | What to do instead | +|---|---|---| +| Create a `ThinkingService` | The struct resolves itself; services add a boundary that breaks self-reference | Add methods to Trajectory | +| Add transition probabilities to Markov | Markov = XOR. Probabilities add learned weights. | Keep XOR with braiding | +| Use cosine similarity on f32 projections | Recovery margin IS Hamming within role slices. f32 projection loses the algebraic structure | Use `RoleKey::recovery_margin` | +| Treat AriGraph as "the database layer" | It's thinking tissue. Cache layers between Think and Graph are like caching between brain and hippocampus | Wire as `&ref` field on Trajectory | +| "Fix" the 0.618 confidence ceiling | It's the golden-ratio fixed point of NARS revision. Permanent revisability IS the feature | Leave the formula alone | +| Propose elaborate benchmarks | The AGI test is one curve: accuracy per chapter of Animal Farm. Rising = works. Flat = broken wire | Measure one curve | +| Rebuild the vocabulary system | COCA 24K + spider NER covers everything | Wire the loop | diff --git a/crates/deepnsm/Cargo.lock b/crates/deepnsm/Cargo.lock index a58e7f0f..cffc3647 100644 --- a/crates/deepnsm/Cargo.lock +++ b/crates/deepnsm/Cargo.lock @@ -69,6 +69,7 @@ dependencies = [ name = "deepnsm" version = "0.1.0" dependencies = [ + "lance-graph-contract", "ndarray", ] @@ -78,6 +79,10 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" +[[package]] +name = "lance-graph-contract" +version = "0.1.0" + [[package]] name = "libc" version = "0.2.184" diff --git a/crates/deepnsm/Cargo.toml b/crates/deepnsm/Cargo.toml index 9723261c..8485c4d1 100644 --- a/crates/deepnsm/Cargo.toml +++ b/crates/deepnsm/Cargo.toml @@ -20,5 +20,11 @@ No GPU. No learned weights. Same decision boundaries as cosine. # never touches backend optimization files. [dependencies] ndarray = { path = "../../../ndarray", default-features = false, features = ["std"] } +lance-graph-contract = { path = "../lance-graph-contract", optional = true } + +[features] +default = [] +grammar-10k = ["dep:lance-graph-contract"] [dev-dependencies] +lance-graph-contract = { path = "../lance-graph-contract" } diff --git a/crates/deepnsm/src/content_fp.rs b/crates/deepnsm/src/content_fp.rs new file mode 100644 index 00000000..1013cbd7 --- /dev/null +++ b/crates/deepnsm/src/content_fp.rs @@ -0,0 +1,98 @@ +//! 10K-dim content fingerprints from vocabulary ranks. +//! +//! Each word in the COCA vocabulary gets a deterministic `Vsa10k` +//! fingerprint: pseudo-random bits spread across all 10,000 dims +//! via SplitMix64 seeded from rank. These are the CONTENT vectors +//! that get bound into role-key slices via `RoleKey::bind`. +//! +//! Gated on `grammar-10k` feature (pulls in `lance-graph-contract`). + +use lance_graph_contract::grammar::role_keys::{Vsa10k, VSA_WORDS, VSA_DIMS}; + +/// Generate a 10K-dim content fingerprint from a vocabulary rank. +/// +/// Deterministic: same rank always produces the same vector. +/// ~50% bits set (balanced), good avalanche via SplitMix64. +pub fn content_fp(rank: u16) -> Vsa10k { + let seed = (rank as u64) + .wrapping_mul(0x9E3779B97F4A7C15) + .wrapping_add(0xBF58476D1CE4E5B9); + let mut v = [0u64; VSA_WORDS]; + let mut state = seed; + for w in 0..VSA_WORDS { + state = splitmix64(state); + v[w] = state; + } + // Zero slack bits above VSA_DIMS (10000) to prevent noise. + let last_word = (VSA_DIMS - 1) / 64; + let last_bit = VSA_DIMS % 64; + if last_bit > 0 { + v[last_word] &= (1u64 << last_bit) - 1; + } + for w in (last_word + 1)..VSA_WORDS { + v[w] = 0; + } + v +} + +/// Batch-generate content fingerprints for ranks `0..n`. +pub fn content_fp_table(n: u16) -> Vec { + (0..n).map(content_fp).collect() +} + +#[inline] +fn splitmix64(mut x: u64) -> u64 { + x = x.wrapping_add(0x9E3779B97F4A7C15); + x = (x ^ (x >> 30)).wrapping_mul(0xBF58476D1CE4E5B9); + x = (x ^ (x >> 27)).wrapping_mul(0x94D049BB133111EB); + x ^ (x >> 31) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn deterministic() { + assert_eq!(content_fp(42), content_fp(42)); + } + + #[test] + fn distinct_ranks_distinct_vectors() { + let a = content_fp(0); + let b = content_fp(1); + assert_ne!(a, b); + } + + #[test] + fn balanced_popcount() { + let v = content_fp(42); + let pop: u32 = v.iter().map(|w| w.count_ones()).sum(); + let expected = VSA_DIMS as u32 / 2; + let tolerance = (VSA_DIMS as f32).sqrt() as u32 * 4; + assert!( + pop.abs_diff(expected) < tolerance, + "popcount {} should be near {}, tolerance {}", + pop, expected, tolerance + ); + } + + #[test] + fn no_bits_above_vsa_dims() { + let v = content_fp(999); + for dim in VSA_DIMS..(VSA_WORDS * 64) { + let word = dim / 64; + let bit = dim % 64; + assert_eq!( + (v[word] >> bit) & 1, 0, + "bit set above VSA_DIMS at dim {dim}" + ); + } + } + + #[test] + fn table_has_correct_length() { + let t = content_fp_table(100); + assert_eq!(t.len(), 100); + } +} diff --git a/crates/deepnsm/src/lib.rs b/crates/deepnsm/src/lib.rs index f715843a..3b09908e 100644 --- a/crates/deepnsm/src/lib.rs +++ b/crates/deepnsm/src/lib.rs @@ -71,3 +71,10 @@ pub use similarity::SimilarityTable; pub use encoder::{VsaVec, RoleVectors}; pub use context::ContextWindow; pub mod fingerprint16k; + +#[cfg(feature = "grammar-10k")] +pub mod content_fp; +#[cfg(feature = "grammar-10k")] +pub mod markov_bundle; +#[cfg(feature = "grammar-10k")] +pub mod trajectory; diff --git a/crates/deepnsm/src/markov_bundle.rs b/crates/deepnsm/src/markov_bundle.rs new file mode 100644 index 00000000..eac6f3de --- /dev/null +++ b/crates/deepnsm/src/markov_bundle.rs @@ -0,0 +1,250 @@ +//! Markov ±5 bundler with role-indexed binding and braiding. +//! +//! Each sentence is bound per-token via `RoleKey::bind`, XOR-bundled +//! into one `Vsa10k`, then braided by `vsa_permute(sentence_vsa, offset)` +//! per position in the ±5 window. The 11 braided vectors are XOR- +//! superposed into the trajectory bundle. +//! +//! The braiding encodes temporal order without learned positional +//! embeddings — Shaw's ρ operator (2501.05368 §B.3). + +use lance_graph_contract::grammar::role_keys::{ + Vsa10k, VSA_WORDS, VSA_ZERO, vsa_xor, + SUBJECT_KEY, PREDICATE_KEY, OBJECT_KEY, MODIFIER_KEY, TEMPORAL_KEY, +}; +use lance_graph_contract::grammar::context_chain::{ + MARKOV_RADIUS, CHAIN_LEN, WeightingKernel, +}; + +use crate::content_fp::content_fp; +use crate::parser::SentenceStructure; + +/// Cyclic left-shift of a Vsa10k by `shift` bit positions. +/// This is the braiding operator ρ from Shaw et al. +fn vsa_permute(v: &Vsa10k, shift: usize) -> Vsa10k { + if shift == 0 { + return *v; + } + let shift = shift % (VSA_WORDS * 64); + let word_shift = shift / 64; + let bit_shift = shift % 64; + let mut out = [0u64; VSA_WORDS]; + for w in 0..VSA_WORDS { + let src = (w + VSA_WORDS - word_shift) % VSA_WORDS; + if bit_shift == 0 { + out[w] = v[src]; + } else { + let prev = (src + VSA_WORDS - 1) % VSA_WORDS; + out[w] = (v[src] << bit_shift) | (v[prev] >> (64 - bit_shift)); + } + } + out +} + +/// Encode one sentence into a single Vsa10k via role-key binding. +/// +/// Each SPO triple's subject/predicate/object gets bound to the +/// corresponding role key. Modifiers get bound to MODIFIER_KEY. +/// Temporals get bound to TEMPORAL_KEY. All bindings are XOR- +/// superposed into one sentence-level vector. +pub fn encode_sentence(structure: &SentenceStructure) -> Vsa10k { + let mut sentence_vsa = VSA_ZERO; + for triple in &structure.triples { + let s_fp = content_fp(triple.subject()); + let p_fp = content_fp(triple.predicate()); + let s_bound = SUBJECT_KEY.bind(&s_fp); + let p_bound = PREDICATE_KEY.bind(&p_fp); + sentence_vsa = vsa_xor(&sentence_vsa, &s_bound); + sentence_vsa = vsa_xor(&sentence_vsa, &p_bound); + let obj = triple.object(); + if obj != crate::spo::NO_ROLE { + let o_fp = content_fp(obj); + let o_bound = OBJECT_KEY.bind(&o_fp); + sentence_vsa = vsa_xor(&sentence_vsa, &o_bound); + } + } + for modifier in &structure.modifiers { + let m_fp = content_fp(modifier.modifier); + let m_bound = MODIFIER_KEY.bind(&m_fp); + sentence_vsa = vsa_xor(&sentence_vsa, &m_bound); + } + for &(_triple_idx, temporal_rank) in &structure.temporals { + let t_fp = content_fp(temporal_rank); + let t_bound = TEMPORAL_KEY.bind(&t_fp); + sentence_vsa = vsa_xor(&sentence_vsa, &t_bound); + } + sentence_vsa +} + +/// Ring buffer of ±5 encoded sentences with braided bundling. +pub struct MarkovBundler { + sentences: [Option; CHAIN_LEN], + head: usize, + count: usize, + kernel: WeightingKernel, +} + +impl MarkovBundler { + pub fn new(kernel: WeightingKernel) -> Self { + Self { + sentences: [None; CHAIN_LEN], + head: 0, + count: 0, + kernel, + } + } + + /// Push a parsed sentence into the ring buffer. + pub fn push(&mut self, structure: &SentenceStructure) { + let encoded = encode_sentence(structure); + self.sentences[self.head] = Some(encoded); + self.head = (self.head + 1) % CHAIN_LEN; + if self.count < CHAIN_LEN { + self.count += 1; + } + } + + /// Index of the focal sentence (most recently pushed). + fn focal_index(&self) -> usize { + (self.head + CHAIN_LEN - 1) % CHAIN_LEN + } + + /// Build a braided trajectory bundle from the current window. + /// + /// Each sentence is permuted by its distance from the focal point + /// (braiding ρ^d), weighted by the kernel, and XOR-superposed. + pub fn build_bundle(&self) -> Vsa10k { + let focal = self.focal_index(); + let mut bundle = VSA_ZERO; + + for i in 0..CHAIN_LEN { + let slot = (focal + CHAIN_LEN - MARKOV_RADIUS + i) % CHAIN_LEN; + if let Some(ref sentence_vsa) = self.sentences[slot] { + let distance = if i <= MARKOV_RADIUS { + MARKOV_RADIUS - i + } else { + i - MARKOV_RADIUS + }; + let weight = self.kernel.weight(distance); + if weight <= 0.0 { + continue; + } + // Braid by distance from focal — encodes temporal order. + let braided = vsa_permute(sentence_vsa, distance * 64); + bundle = vsa_xor(&bundle, &braided); + } + } + bundle + } + + pub fn is_saturated(&self) -> bool { + self.count >= CHAIN_LEN + } + + pub fn filled(&self) -> usize { + self.count + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::parser::SentenceStructure; + use crate::spo::SpoTriple; + use lance_graph_contract::grammar::role_keys::vsa_similarity; + + fn mk_sentence(s: u16, p: u16, o: u16) -> SentenceStructure { + SentenceStructure { + triples: vec![SpoTriple::new(s, p, o)], + modifiers: vec![], + negations: vec![], + temporals: vec![], + } + } + + #[test] + fn encode_sentence_is_deterministic() { + let s = mk_sentence(10, 20, 30); + let a = encode_sentence(&s); + let b = encode_sentence(&s); + assert_eq!(a, b); + } + + #[test] + fn encode_sentence_word_order_matters() { + let s1 = mk_sentence(10, 20, 30); + let s2 = mk_sentence(30, 20, 10); + let a = encode_sentence(&s1); + let b = encode_sentence(&s2); + assert_ne!(a, b, "S/O swap must produce different vectors"); + } + + #[test] + fn encode_sentence_recovers_subject_via_unbind() { + let s = mk_sentence(42, 100, 200); + let encoded = encode_sentence(&s); + let expected_s = content_fp(42); + let margin = SUBJECT_KEY.recovery_margin( + &SUBJECT_KEY.unbind(&encoded), + &expected_s, + ); + assert!( + margin > 0.99, + "SUBJECT should recover losslessly from single-triple sentence, got {margin}" + ); + } + + #[test] + fn bundler_push_and_build() { + let mut b = MarkovBundler::new(WeightingKernel::Uniform); + for i in 0..11 { + b.push(&mk_sentence(i, i + 100, i + 200)); + } + assert!(b.is_saturated()); + let bundle = b.build_bundle(); + // Non-zero bundle. + let pop: u32 = bundle.iter().map(|w| w.count_ones()).sum(); + assert!(pop > 0, "bundle should not be all-zero"); + } + + #[test] + fn braided_bundle_differs_from_unbraided() { + let mut braided = MarkovBundler::new(WeightingKernel::MexicanHat); + let mut uniform = MarkovBundler::new(WeightingKernel::Uniform); + for i in 0..11 { + let s = mk_sentence(i, i + 100, i + 200); + braided.push(&s); + uniform.push(&s); + } + let a = braided.build_bundle(); + let b = uniform.build_bundle(); + // Different kernels should produce different bundles (MexicanHat + // weights differ from Uniform). + assert_ne!(a, b, "different kernels should produce different bundles"); + } + + #[test] + fn vsa_permute_is_invertible() { + let v = content_fp(42); + let shifted = vsa_permute(&v, 137); + let restored = vsa_permute(&shifted, VSA_WORDS * 64 - 137); + assert_eq!(v, restored, "permute then inverse-permute must recover"); + } + + #[test] + fn vsa_permute_zero_is_identity() { + let v = content_fp(99); + assert_eq!(v, vsa_permute(&v, 0)); + } + + #[test] + fn focal_is_most_recent() { + let mut b = MarkovBundler::new(WeightingKernel::Uniform); + b.push(&mk_sentence(1, 2, 3)); + b.push(&mk_sentence(4, 5, 6)); + // Focal should be the second pushed sentence. + let focal_encoded = encode_sentence(&mk_sentence(4, 5, 6)); + let slot = b.focal_index(); + assert_eq!(b.sentences[slot].as_ref().unwrap(), &focal_encoded); + } +} diff --git a/crates/deepnsm/src/trajectory.rs b/crates/deepnsm/src/trajectory.rs new file mode 100644 index 00000000..86e00661 --- /dev/null +++ b/crates/deepnsm/src/trajectory.rs @@ -0,0 +1,298 @@ +//! Trajectory — the Think carrier that speaks for itself. +//! +//! Holds a Markov-braided role-indexed bundle + references to thinking +//! tissue (episodic memory, triplet graph, global context). Methods +//! on Trajectory compute free energy, resolve ambiguity, and observe +//! outcomes — the object IS the inference engine. + +use lance_graph_contract::grammar::role_keys::{ + Vsa10k, VSA_ZERO, VSA_WORDS, RoleKey, vsa_xor, vsa_similarity, + SUBJECT_KEY, PREDICATE_KEY, OBJECT_KEY, MODIFIER_KEY, + TEMPORAL_KEY, KAUSAL_KEY, MODAL_KEY, LOKAL_KEY, INSTRUMENT_KEY, +}; +use lance_graph_contract::grammar::free_energy::{ + FreeEnergy, Hypothesis, Resolution, +}; +use lance_graph_contract::grammar::thinking_styles::{ + GrammarStyleAwareness, GrammarStyleConfig, ParamKey, ParseOutcome, +}; +use lance_graph_contract::grammar::ticket::FailureTicket; + +/// A resolved Markov ±5 trajectory with tissue references. +/// +/// This is the Think struct from The Click (CLAUDE.md § P-1): +/// trajectory = Subject (what), awareness = Modal (how confidently), +/// free_energy = Kausal (why this thought), resolution = Predicate +/// (what it concludes), global_context = Lokal (where in fact-space). +pub struct Trajectory { + pub bundle: Vsa10k, + pub global_context: Vsa10k, +} + +impl Trajectory { + pub fn new(bundle: Vsa10k, global_context: Vsa10k) -> Self { + Self { bundle, global_context } + } + + /// Unbind a single role from the trajectory bundle. + /// Returns the content that was bound into that role's slice. + pub fn role_bundle(&self, role: &RoleKey) -> Vsa10k { + role.unbind(&self.bundle) + } + + /// Mean recovery margin across a set of role keys, comparing + /// the trajectory's unbound content against hypothesis fillers. + /// + /// This IS the likelihood term in the free-energy decomposition: + /// "how well do the hypothesized role fillers match what the + /// trajectory actually carries?" + pub fn mean_recovery_margin(&self, hypothesis: &Hypothesis) -> f32 { + if hypothesis.role_fillers.is_empty() { + return 0.0; + } + let mut total = 0.0f32; + let mut count = 0u32; + for (role_label, _filler_label) in &hypothesis.role_fillers { + if let Some(key) = label_to_key(role_label) { + let unbound = key.unbind(&self.bundle); + // Use the filler's hash as the expected content fingerprint. + let expected = crate::content_fp::content_fp( + filler_rank_from_label(_filler_label), + ); + let m = key.recovery_margin(&unbound, &expected); + total += m; + count += 1; + } + } + if count == 0 { 0.0 } else { total / count as f32 } + } + + /// Recovery margin against the global context (ambient prior). + /// Measures how well this trajectory's content aligns with the + /// accumulated story-so-far. + pub fn ambient_similarity(&self) -> f32 { + vsa_similarity(&self.bundle, &self.global_context) + } + + /// Compute free energy for a hypothesis against this trajectory. + pub fn free_energy( + &self, + hypothesis: &Hypothesis, + awareness: &GrammarStyleAwareness, + prior: &GrammarStyleConfig, + ) -> FreeEnergy { + let local_likelihood = self.mean_recovery_margin(hypothesis); + let ambient = self.ambient_similarity().max(0.0); + let likelihood = 0.7 * local_likelihood + 0.3 * ambient; + let kl = awareness.divergence_from(prior); + FreeEnergy::compose(likelihood, kl) + } + + /// Score and rank hypotheses, returning the resolution. + pub fn resolve( + &self, + candidates: Vec, + awareness: &GrammarStyleAwareness, + prior: &GrammarStyleConfig, + ticket_factory: impl FnOnce() -> FailureTicket, + ) -> Resolution { + let mut ranked: Vec<(Hypothesis, FreeEnergy)> = candidates + .into_iter() + .map(|h| { + let fe = self.free_energy(&h, awareness, prior); + (h, fe) + }) + .collect(); + ranked.sort_by(|a, b| { + a.1.total + .partial_cmp(&b.1.total) + .unwrap_or(std::cmp::Ordering::Equal) + }); + Resolution::from_ranked(&ranked, ticket_factory) + } +} + +/// Map a role label string to the corresponding static RoleKey. +fn label_to_key(label: &str) -> Option<&'static RoleKey> { + match label { + "SUBJECT" => Some(&*SUBJECT_KEY), + "PREDICATE" => Some(&*PREDICATE_KEY), + "OBJECT" => Some(&*OBJECT_KEY), + "MODIFIER" => Some(&*MODIFIER_KEY), + "TEMPORAL" => Some(&*TEMPORAL_KEY), + "KAUSAL" => Some(&*KAUSAL_KEY), + "MODAL" => Some(&*MODAL_KEY), + "LOKAL" => Some(&*LOKAL_KEY), + "INSTRUMENT" => Some(&*INSTRUMENT_KEY), + _ => None, + } +} + +/// Derive a vocabulary rank from a filler label (for test/stub use). +/// Real pipeline will carry actual ranks, not labels. +fn filler_rank_from_label(label: &str) -> u16 { + let mut h: u32 = 0; + for b in label.bytes() { + h = h.wrapping_mul(31).wrapping_add(b as u32); + } + (h % 4096) as u16 +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::content_fp::content_fp; + use crate::markov_bundle::{MarkovBundler, encode_sentence}; + use crate::parser::SentenceStructure; + use crate::spo::SpoTriple; + use lance_graph_contract::grammar::context_chain::WeightingKernel; + use lance_graph_contract::grammar::thinking_styles::{ + NarsPriorityChain, MorphologyPolicy, MorphologyTableId, + TekamoloPolicy, MarkovPolicy, ReplayStrategy, + SpoCausalPolicy, CoveragePolicy, + }; + use lance_graph_contract::grammar::inference::NarsInference; + use lance_graph_contract::grammar::tekamolo::TekamoloSlot; + use lance_graph_contract::thinking::ThinkingStyle; + + fn test_prior() -> GrammarStyleConfig { + GrammarStyleConfig { + style: ThinkingStyle::Analytical, + nars: NarsPriorityChain { + primary: NarsInference::Deduction, + fallback: NarsInference::Abduction, + }, + morphology: MorphologyPolicy { + tables: vec![MorphologyTableId::EnglishSvo], + agglutinative_mode: false, + }, + tekamolo: TekamoloPolicy { + priority: vec![TekamoloSlot::Temporal, TekamoloSlot::Lokal], + require_fillable: true, + }, + markov: MarkovPolicy { + radius: 5, + kernel: WeightingKernel::MexicanHat, + replay: ReplayStrategy::Forward, + }, + spo_causal: SpoCausalPolicy { + pearl_mask: 0x01, + ambiguity_tolerance: 0.1, + }, + coverage: CoveragePolicy { + local_threshold: 0.90, + escalate_below: 0.85, + }, + } + } + + fn mk_sentence(s: u16, p: u16, o: u16) -> SentenceStructure { + SentenceStructure { + triples: vec![SpoTriple::new(s, p, o)], + modifiers: vec![], + negations: vec![], + temporals: vec![], + } + } + + #[test] + fn trajectory_role_unbind_recovers_subject() { + let sentence = mk_sentence(42, 100, 200); + let bundle = encode_sentence(&sentence); + let trajectory = Trajectory::new(bundle, VSA_ZERO); + let recovered = trajectory.role_bundle(&SUBJECT_KEY); + let expected = content_fp(42); + let margin = SUBJECT_KEY.recovery_margin(&recovered, &expected); + assert!( + margin > 0.99, + "SUBJECT should recover from trajectory, got {margin}" + ); + } + + #[test] + fn trajectory_free_energy_lower_for_correct_hypothesis() { + let sentence = mk_sentence(42, 100, 200); + let bundle = encode_sentence(&sentence); + let trajectory = Trajectory::new(bundle, VSA_ZERO); + + let prior = test_prior(); + let awareness = GrammarStyleAwareness::bootstrap(prior.style); + + // Correct hypothesis: subject=42, predicate=100, object=200 + let correct = Hypothesis::new("correct") + .fill("SUBJECT", "42") + .fill("PREDICATE", "100") + .fill("OBJECT", "200"); + + // Wrong hypothesis: subject=999 + let wrong = Hypothesis::new("wrong") + .fill("SUBJECT", "999") + .fill("PREDICATE", "888") + .fill("OBJECT", "777"); + + let f_correct = trajectory.free_energy(&correct, &awareness, &prior); + let f_wrong = trajectory.free_energy(&wrong, &awareness, &prior); + + assert!( + f_correct.total < f_wrong.total, + "correct hypothesis should have lower F ({}) than wrong ({})", + f_correct.total, f_wrong.total + ); + } + + #[test] + fn trajectory_resolve_commits_best_hypothesis() { + let sentence = mk_sentence(42, 100, 200); + let bundle = encode_sentence(&sentence); + let trajectory = Trajectory::new(bundle, VSA_ZERO); + + let prior = test_prior(); + let awareness = GrammarStyleAwareness::bootstrap(prior.style); + + let correct = Hypothesis::new("correct") + .fill("SUBJECT", "42"); + let wrong = Hypothesis::new("wrong") + .fill("SUBJECT", "999"); + + let resolution = trajectory.resolve( + vec![wrong, correct], + &awareness, + &prior, + || panic!("should not create failure ticket"), + ); + + match resolution { + Resolution::Commit { hypothesis, .. } => { + assert_eq!(hypothesis.label, "correct"); + } + Resolution::Epiphany { winner, .. } => { + assert_eq!(winner.label, "correct"); + } + Resolution::FailureTicket(_) => { + panic!("should not escalate — correct hypothesis has recoverable content"); + } + } + } + + #[test] + fn bundled_trajectory_through_markov_bundler() { + let mut bundler = MarkovBundler::new(WeightingKernel::MexicanHat); + for i in 0..11 { + bundler.push(&mk_sentence(i + 10, i + 100, i + 200)); + } + let bundle = bundler.build_bundle(); + let trajectory = Trajectory::new(bundle, VSA_ZERO); + + // The focal sentence (last pushed: s=20, p=110, o=210). + // Its SUBJECT should still be recoverable despite braiding. + let recovered = trajectory.role_bundle(&SUBJECT_KEY); + let expected = content_fp(20); + let margin = SUBJECT_KEY.recovery_margin(&recovered, &expected); + // After 11-way braided superposition, recovery is approximate. + // MexicanHat weights the focal heavily so margin should be > 0.5. + assert!( + margin > 0.5, + "focal SUBJECT should be recoverable from braided bundle, got {margin}" + ); + } +}