From 55b4add251fa711b18d66ee73e5185afb4b2fd76 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 23 Jul 2026 17:40:32 +0000 Subject: [PATCH] =?UTF-8?q?D-DIA-S9:=20epiphany=20attractors=20=E2=80=94?= =?UTF-8?q?=20rank=20basins=20by=20rate,=20never=20count?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `nars/epiphany.rs::rank_epiphany_attractors(&arena, min_attempts) -> Vec< EpiphanyAttractor>` ranks subject-basins by epiphany DENSITY (derived ÷ total closure rate per subject — a rung≥1 belief proxying a landed epiphany, the same closure-density notion `nars::insight` uses globally), never by raw count. The E-DOOMSCROLL "rank by rate, never count" discipline (plan §1 S9, third confirmation) applied to epiphany sites, so the field never collapses into its largest basin. Falsifier `rate_ranking_beats_count_ranking`: a LARGE sparse basin (20 obs + 5 derived → rate 0.20, count 5) vs a SMALL dense basin (1 obs + 3 derived → rate 0.75, count 3). Rate-ranking surfaces the dense basin (#1); count-ranking surfaces the large one (#1) — the orderings DIVERGE, and that divergence is the point: count accrues epiphanies merely by size and buries the genuine attractor. Completes D-DIA-V3 (V3-A detector + V3-B response + V3-C loop + S9 attractors). Built by one Sonnet grindwork agent from an Opus spec, Opus-gated. Central gating caught + fixed a regression: the agent's alphabetical re-export insert dropped the `reach_out` re-export line; restored before gating (why the fleet edits and the orchestrator compiles centrally). 3 epiphany + 47 nars tests green; clippy -D warnings + fmt clean. Board: EPIPHANIES E-DIA-S9-EPIPHANY-ATTRACTOR-RATE-1, STATUS_BOARD D-DIA-V3, plan §4, AGENT_LOG. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01K3RyLEbuNSHxxB3NTTrGki --- .claude/board/AGENT_LOG.md | 8 + .claude/board/EPIPHANIES.md | 8 + .claude/board/STATUS_BOARD.md | 2 +- .claude/board/exec-runs/dia-s9-epiphany.txt | 35 ++++ .claude/plans/dialectic-engine-v1.md | 11 +- .../lance-graph-planner/src/nars/epiphany.rs | 176 ++++++++++++++++++ crates/lance-graph-planner/src/nars/mod.rs | 3 + 7 files changed, 241 insertions(+), 2 deletions(-) create mode 100644 .claude/board/exec-runs/dia-s9-epiphany.txt create mode 100644 crates/lance-graph-planner/src/nars/epiphany.rs diff --git a/.claude/board/AGENT_LOG.md b/.claude/board/AGENT_LOG.md index e339b57a8..a9a74fee7 100644 --- a/.claude/board/AGENT_LOG.md +++ b/.claude/board/AGENT_LOG.md @@ -1,3 +1,11 @@ +## 2026-07-23 — D-DIA-S9 epiphany attractors (rank by rate) — 1 Sonnet grindwork agent from an Opus spec, Opus-gated (sole board writer) + +- **Operator steer:** answered the D-SCI-1-vs-remainders AskUserQuestion with "small dialectic remainders" (+ Gutenberg science text for D-SCI-1's future corpus, + the arXiv-ephemeral-via-tesseract/spider option). This is the S9 remainder. Build was grindwork (design = the density-ranking primitive + the rate-vs-count divergence falsifier) → one `general-purpose·sonnet` (edit-only; tag `exec-runs/dia-s9-epiphany.txt`). +- **Deliverable:** `nars/epiphany.rs` (`rank_epiphany_attractors` + `EpiphanyAttractor`) — rank subject-basins by density (`derived/total`) not count, the E-DOOMSCROLL discipline (S9). `E-DIA-S9-EPIPHANY-ATTRACTOR-RATE-1`. Completes D-DIA-V3. +- **⚠ Central gate caught a Sonnet regression:** the agent, inserting the `epiphany` re-export alphabetically into `nars/mod.rs`, DROPPED the `reach_out` re-export line (module still declared, public items no longer re-exported). Orchestrator restored `pub use reach_out::{reach_out_integrate, FeltOutcome, ReachOutConfig};` before gating — a concrete instance of why the fleet edits and the orchestrator compiles/gates centrally (the edit-only agent can't see the compile regression its own alphabetical edit caused). +- **Falsifier finding (banked):** rate-ranking surfaces a small dense basin (rate 0.75, count 3) that count-ranking buries under a large sparse one (rate 0.20, count 5) — the orderings diverge, which is the E-DOOMSCROLL point. +- **Orchestrator-verified:** 3 epiphany tests + 47 nars-module tests green (the 47 confirms the reach_out re-export restore compiles); `cargo fmt` + `cargo clippy -p lance-graph-planner --lib -- -D warnings` clean; `Cargo.lock` restored. + ## 2026-07-23 — D-DIA-V5-A reach-out felt integration (§3.6) — 1 Sonnet grindwork agent from an Opus spec, Opus-gated (sole board writer) - **Operator directive:** "use sonnet agents for grindwork." The §3.6 criterion reduces to a structural test (middle-term click = a derivation lands), so the design (quarantine + new-derivation gate + the size-matched falsifier) was worked on the main thread and the build was grindwork → one `general-purpose·sonnet` (edit-only, no cargo/worktree; tag `exec-runs/dia-v5a-reach-out.txt`). diff --git a/.claude/board/EPIPHANIES.md b/.claude/board/EPIPHANIES.md index 3675f87ec..523e91738 100644 --- a/.claude/board/EPIPHANIES.md +++ b/.claude/board/EPIPHANIES.md @@ -1,3 +1,11 @@ +## 2026-07-23 — E-DIA-S9-EPIPHANY-ATTRACTOR-RATE-1 — epiphany attractors rank basins by DENSITY (derived/total closure rate per subject), never raw count — the E-DOOMSCROLL "rank by rate, never count" discipline (S9, third confirmation) applied to epiphany sites, so the field never collapses into its largest basin. + +**Status:** SHIPPED (`nars/epiphany.rs`, Sonnet-built from an Opus spec, Opus-gated; 3 tests green, 47 nars-module tests, clippy `-D warnings` + fmt clean). **Confidence:** High — a size-normalized ranking primitive over the shipped closure-density notion; the discipline itself is thrice-confirmed (E-DOOMSCROLL). + +**What it is.** D-DIA-S9. `rank_epiphany_attractors(&arena, min_attempts) -> Vec` groups beliefs by subject, computes each basin's epiphany density (`derived ÷ total`, a `rung>=1` belief proxying a landed epiphany at that subject — the same closure-density notion `nars::insight` uses globally), and ranks by RATE desc (ties → subject id asc, deterministic). The `min_attempts` floor drops thin basins. + +**The finding (why rate, not count).** The falsifier `rate_ranking_beats_count_ranking` builds a LARGE sparse basin (subject 1: 20 obs + 5 derived → rate 0.20, raw count 5) and a SMALL dense basin (subject 2: 1 obs + 3 derived → rate 0.75, raw count 3). Rate-ranking surfaces subject 2 (#1); count-ranking surfaces subject 1 (#1) — the two orderings **diverge**, and the divergence IS the point: a count-ranked field collapses into whichever basin is biggest (it accrues raw epiphany-count merely by size), burying the genuinely dense attractor. Size-normalization is the fix. This is the third confirmation of E-DOOMSCROLL — the same rate-not-count discipline that governs thought promotion/prune and WisdomMarker neighborhood bias, now on epiphany sites. Cross-ref: `nars::insight` (closure density, global), plan §1 S9 + §4 V3 (epiphany attractors), E-DOOMSCROLL. + ## 2026-07-23 — E-DIA-V5-A-FELT-INTEGRATION-1 — the §3.6 "dull shadow vs new insight" felt criterion is ONE structural test read two ways: fetched external material integrates IFF it serves as the middle term composing pre-existing disjoint-stamp beliefs (a derivation lands, coherence rises = insight); otherwise nothing moves (dull shadow). The felt form and the audit form are the same event — no separate qualia service. **Status:** SHIPPED (`nars/reach_out.rs`, Sonnet-built from an Opus spec, Opus-gated; 4 tests green, 255 planner-lib tests, clippy `-D warnings` + fmt clean). **Confidence:** High on the structural criterion + the quarantine; the S12 qualia-ablation *field-scale* falsifier (doomscroll cycles, Staunen→Wisdom conversion) is a later V5 slice. diff --git a/.claude/board/STATUS_BOARD.md b/.claude/board/STATUS_BOARD.md index 40834dc4c..bb8364091 100644 --- a/.claude/board/STATUS_BOARD.md +++ b/.claude/board/STATUS_BOARD.md @@ -8,7 +8,7 @@ Plan: `.claude/plans/dialectic-engine-v1.md` (six operator pillars + S1-S12 synt | D-DIA-V1 | The five tactics (RCR/TR/ASC/CAS/CR) over the Belief arena + throttles + ReasoningGap | lance-graph | Shipped (PR #816; lance-graph-planner `nars/{belief,tactics}` over `TruthValue`; 17 nars + 233 planner tests) | plan §3, §4 | | D-DIA-V2-A | insight/mush S10 detector + size-preserving null falsifier | lance-graph | Shipped (PR #819 merged; `nars/insight.rs`; null falsifier caught + fixed 2 formula confounds — `E-S10-COHERENCE-CLOSURE-DENSITY-1`) | plan §1 S10, §4 | | D-DIA-V2-B | The loop: bias→recipe tactic-LUT, byte-lane council, texture window | lance-graph | **bias→tactic LUT SHIPPED** (`E-DIA-V2-B-BIAS-TACTIC-LUT-1`; `nars/tactic_select.rs` `tactic_for_bias`/`TacticChoice` reusing `contract::sensorium::GraphBias`; confusion-matrix falsifier `examples/tactic_select_confusion.rs` — G1 5/5 diagonal, G2 3/3 structural discrimination, G3 beats constant policy; honest split: RCR/TR/CAS structural, ASC/CR normative; 4 unit + 28 nars tests, clippy clean). Byte-lane i8 council + texture window still queued. | plan §1 S8, §4 | -| D-DIA-V3 | Dissolution → field rung-elevation (the cathedral floors) + Staunen↔Wisdom flow accounting | lance-graph | **V3-A + V3-B + V3-C SHIPPED (the S11 loop is CLOSED).** V3-A detector (`E-DIA-V3-A-DISSOLUTION-DETECTOR-1`; `nars/dissolution.rs`, S10 mirror on `insight::Snapshot`; null PASS). V3-B response (`E-DIA-V3-B-FIELD-ELEVATION-1`; `nars/elevation.rs` `elevate_field` — mass-induction mints abstract parents over shared-predicate clusters, HHTL grows upward; falsifier proves the PAYOFF = one parent-fact propagates to all k children via closure, and the honest guard = nothing minted on structureless noise; 3 tests). V3-C `regulate_cycle` (`nars/regulate.rs`) composes detect_dissolution→should_elevate→elevate_field into one active-inference cycle (`E-DIA-V3-C-REGULATION-LOOP-1`; elevation TRIGGERED by measurement not chosen; 3 tests — dissolving→elevates, crystallizing→no-op, loop bounded across cycles). Remaining V3: epiphany attractors (S9). | plan §0 pillars 3-5, §1 S11, §4 | +| D-DIA-V3 | Dissolution → field rung-elevation (the cathedral floors) + Staunen↔Wisdom flow accounting | lance-graph | **V3-A + V3-B + V3-C SHIPPED (the S11 loop is CLOSED).** V3-A detector (`E-DIA-V3-A-DISSOLUTION-DETECTOR-1`; `nars/dissolution.rs`, S10 mirror on `insight::Snapshot`; null PASS). V3-B response (`E-DIA-V3-B-FIELD-ELEVATION-1`; `nars/elevation.rs` `elevate_field` — mass-induction mints abstract parents over shared-predicate clusters, HHTL grows upward; falsifier proves the PAYOFF = one parent-fact propagates to all k children via closure, and the honest guard = nothing minted on structureless noise; 3 tests). V3-C `regulate_cycle` (`nars/regulate.rs`) composes detect_dissolution→should_elevate→elevate_field into one active-inference cycle (`E-DIA-V3-C-REGULATION-LOOP-1`; elevation TRIGGERED by measurement not chosen; 3 tests — dissolving→elevates, crystallizing→no-op, loop bounded across cycles). **S9 epiphany attractors SHIPPED** (`E-DIA-S9-EPIPHANY-ATTRACTOR-RATE-1`; `nars/epiphany.rs` `rank_epiphany_attractors` — rank basins by density not count, E-DOOMSCROLL 3rd confirmation; 3 tests, rate-vs-count divergence falsifier). V3 fully shipped. | plan §0 pillars 3-5, §1 S11, §4 | | D-DIA-V4 | Foveated HHTL-trie field-search (addressing-first ladder; field search = total-function floor). Rung 1 = `PROBE-CODEBOOK-44` (16-way hierarchical codebook), rung 2 = foveated morton-comma descent, rung 3 = `PremultipliedOver` blasgraph `mxv` floor. Kuzu factorized-processing alignment. | lance-graph + bgz17 | Rung 1 MECHANISM-GREEN + REAL-DATA ρ RUN (`E-PROBE-CODEBOOK-44-MECHANISM-1`: `bgz17::build_hierarchical`, prefix==ancestry purity 1.0 vs flat 0.16; real-data ρ on jina-v3 = hierarchy fidelity-neutral (structure-is-free confirmed), but anchor-close blocked by the Base17 17-dim fold ceiling ρ=0.26 — `TD-BASE17-FOLD-CEILING-SINGLE-WORD`, NOT the codebook; M1 not fully closed). **Rungs 2-3 + M26 SHIPPED** (`E-DIA-V4-FIELD-SEARCH-LOOP-1`, 4-agent Sonnet fleet, Opus-gated): rung 2 `foveated_descend` (8× prune + full recall at fovea_k=2), rung 3 `premultiplied_over` commutative composite (bgz17 palette, not blasgraph), M26 `Belief⟷SpoFacet` lossless byte round-trip. #4 CV-sweep refined `TD-BASE17-FOLD-CEILING`. Architecture `E-FOVEATED-HHTL-TRIE-FIELD-SEARCH-1` | plan §4, S1 | | D-DIA-V5 | Reach-out felt integration (dull shadow vs insight) + qualia ablation falsifier | lance-graph | **V5-A SHIPPED** (`E-DIA-V5-A-FELT-INTEGRATION-1`; `nars/reach_out.rs` `reach_out_integrate`/`FeltOutcome` — the §3.6 middle-term-click criterion as a structural test: fetched bridge quarantined at prior 0.1, NewInsight iff ≥1 derivation composed vs DullShadow; 4 tests incl. size-matched + quarantine-cap). Remaining V5: the S12 qualia-ablation FIELD-scale falsifier. | plan §3.6, S12 | diff --git a/.claude/board/exec-runs/dia-s9-epiphany.txt b/.claude/board/exec-runs/dia-s9-epiphany.txt new file mode 100644 index 000000000..813d214c0 --- /dev/null +++ b/.claude/board/exec-runs/dia-s9-epiphany.txt @@ -0,0 +1,35 @@ +Task: D-DIA-S9 — epiphany attractors ranked by rate, never count + +Files touched: +- crates/lance-graph-planner/src/nars/epiphany.rs (NEW) +- crates/lance-graph-planner/src/nars/mod.rs (EDIT: added `pub mod epiphany;` + in alphabetical position after `elevation` before `facet_fold`, and + `pub use epiphany::{rank_epiphany_attractors, EpiphanyAttractor};` re-export) + +Deliverable: +- `EpiphanyAttractor { subject: u16, epiphanies: usize, attempts: usize, rate: f32 }` +- `rank_epiphany_attractors(arena: &BeliefArena, min_attempts: usize) -> Vec` + groups arena entries by `stmt.s`, counts total (attempts) vs rung>=1 (epiphanies), + filters by min_attempts, sorts by rate DESC then subject ASC (f32::total_cmp). + +Tests (3, as specified): +1. rate_ranking_beats_count_ranking — S9/E-DOOMSCROLL falsifier. Large sparse + basin (subject 1: 20 observed + 5 derived, rate 0.20) vs small dense basin + (subject 2: 1 observed + 3 derived, rate 0.75). Asserts rate-ranking top = + subject 2, count-ranking top = subject 1, and the two orderings diverge. +2. min_attempts_filters_thin_basins — subject 1 (3 beliefs) survives + min_attempts=2, subject 2 (1 belief) is filtered out. +3. ordering_is_deterministic_on_ties — subject 3 and subject 5 both at + rate 0.5; asserts tie broken by subject id ASC (3 before 5). + +Source verification performed (not guessed): +- Read belief.rs: confirmed `BeliefArena::entries()`, `observe(CStmt, TruthValue, + Stamp) -> ReviseOutcome`, `admit_derived(CStmt, TruthValue, &[u32], u32) -> bool`, + `Belief { stmt, rung, .. }`, `CStmt { s, cop, p }`, `Copula::Inh`, + `Stamp::source(u32)` all match the spec exactly. Confirmed `BeliefArena` + derives `Default` (used `BeliefArena::default()` in tests; equivalent to + `BeliefArena::new()` used elsewhere in the file). +- Confirmed mod.rs re-export style/alphabetical pub mod ordering before editing. + +Did NOT run cargo (build/check/test/clippy/fmt) per hard rules — edit-only, +orchestrator gates centrally. No git operations performed. diff --git a/.claude/plans/dialectic-engine-v1.md b/.claude/plans/dialectic-engine-v1.md index fcd9c282e..9d13ba0aa 100644 --- a/.claude/plans/dialectic-engine-v1.md +++ b/.claude/plans/dialectic-engine-v1.md @@ -231,7 +231,16 @@ stamps) and felt form (Datapath, texture) are one event read from two buckets. > 3 tests: dissolving-step→elevates, crystallizing-step→no-op, loop bounded > across cycles (the V3-B idempotence is what makes a CLOSED loop safe). The > S11 dissolution→elevation floor is now a complete, closed, bounded, null- - > tested loop. Remaining V3: epiphany attractors (S9). + > tested loop. + > **✅ S9 epiphany attractors SHIPPED (2026-07-23):** `nars/epiphany.rs` — + > `rank_epiphany_attractors(&arena, min_attempts)` ranks subject-basins by + > epiphany DENSITY (`derived/total` closure rate), never raw count — the + > E-DOOMSCROLL "rank by rate, never count" discipline (S9, 3rd confirmation) + > so the field never collapses into its largest basin + > (`E-DIA-S9-EPIPHANY-ATTRACTOR-RATE-1`; falsifier: a small dense basin + > out-ranks a large sparse one on rate while count buries it — the orderings + > diverge). **V3 is now fully shipped** (V3-A detector + V3-B response + V3-C + > loop + S9 attractors). > **✅ V3-B (field-elevation RESPONSE) SHIPPED (2026-07-23):** `nars/elevation.rs` > — `elevate_field(&mut arena, min_cluster) -> Elevation`. The mass-induction > sweep: groups OBSERVED `is_a` subjects by shared predicate `M`, and for each diff --git a/crates/lance-graph-planner/src/nars/epiphany.rs b/crates/lance-graph-planner/src/nars/epiphany.rs new file mode 100644 index 000000000..0f8e5d9f8 --- /dev/null +++ b/crates/lance-graph-planner/src/nars/epiphany.rs @@ -0,0 +1,176 @@ +//! `epiphany` — epiphany attractors ranked by RATE, never count (D-DIA-S9, +//! plan §1 S9, `E-DOOMSCROLL`). +//! +//! Plan §1 S9 (E-DOOMSCROLL, third confirmation): thought promotion/prune and +//! `WisdomMarker` neighborhood bias use normalized RATES, size-normalized — +//! a count-ranked field collapses into its largest basin. This module applies +//! that discipline to EPIPHANY ATTRACTORS: rank subject-basins by epiphany +//! DENSITY (the local closure rate — derived beliefs ÷ total beliefs for that +//! subject), NOT by raw epiphany count. A basin where reasoning closes +//! DENSELY (high rate) is a genuine attractor even if small; the largest +//! basin wins on raw count merely by being large, so count-ranking collapses +//! the field into it. Rate-ranking surfaces the dense basin the +//! count-ranking buries. +//! +//! Grounding note: "epiphany" here is proxied by a landed derivation — a +//! `rung >= 1` belief — at that subject; local closure density = epiphany +//! density, consistent with the shipped global `coherence = derived/total` +//! in [`super::insight`]. This is the density proxy, not a per-step S10 +//! insight attribution. + +use super::belief::BeliefArena; +use std::collections::BTreeMap; + +/// One epiphany attractor: a subject-basin ranked by its epiphany DENSITY. +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct EpiphanyAttractor { + /// The subject term identifying the basin. + pub subject: u16, + /// Epiphanies at this subject = derived (`rung >= 1`) beliefs with this subject. + pub epiphanies: usize, + /// Attempts = total beliefs with this subject (the size normalizer). + pub attempts: usize, + /// The size-normalized rate `epiphanies / attempts` — the S9 ranking key. + pub rate: f32, +} + +/// Rank subject-basins as epiphany attractors by RATE, never count (S9, +/// E-DOOMSCROLL). For every subject that has at least `min_attempts` beliefs, +/// compute its epiphany density (`derived / total`) and return the attractors +/// sorted by rate DESC, ties broken by subject id ASC (deterministic). A +/// count-ranked field would surface the largest basin; this surfaces the +/// densest, so the field never collapses into its largest basin. +#[must_use] +pub fn rank_epiphany_attractors( + arena: &BeliefArena, + min_attempts: usize, +) -> Vec { + // (subject) -> (epiphanies, attempts) + let mut basins: BTreeMap = BTreeMap::new(); + for b in arena.entries() { + let e = basins.entry(b.stmt.s).or_insert((0, 0)); + e.1 += 1; + if b.rung >= 1 { + e.0 += 1; + } + } + let mut out: Vec = basins + .into_iter() + .filter(|&(_, (_, attempts))| attempts >= min_attempts) + .map(|(subject, (epiphanies, attempts))| EpiphanyAttractor { + subject, + epiphanies, + attempts, + rate: epiphanies as f32 / attempts as f32, + }) + .collect(); + // Rate DESC, then subject ASC for determinism. Use total_cmp on the f32. + out.sort_by(|a, b| { + b.rate + .total_cmp(&a.rate) + .then_with(|| a.subject.cmp(&b.subject)) + }); + out +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::nars::{BeliefArena, CStmt, Copula, Stamp, TruthValue}; + + fn inh(s: u16, p: u16) -> CStmt { + CStmt { + s, + cop: Copula::Inh, + p, + } + } + + /// The S9 / E-DOOMSCROLL falsifier: rate-ranking surfaces the dense small + /// basin that count-ranking buries under the large sparse basin. + #[test] + fn rate_ranking_beats_count_ranking() { + let mut a = BeliefArena::default(); + + // LARGE sparse basin, subject 1: 20 observed + 5 derived. + // epiphanies 5, attempts 25, rate 0.20. + for p in 100..120u16 { + a.observe( + inh(1, p), + TruthValue::new(0.9, 0.9), + Stamp::source(p as u32), + ); + } + for p in 200..205u16 { + assert!(a.admit_derived(inh(1, p), TruthValue::new(0.9, 0.9), &[], 1)); + } + + // SMALL dense basin, subject 2: 1 observed + 3 derived. + // epiphanies 3, attempts 4, rate 0.75. + a.observe(inh(2, 300), TruthValue::new(0.9, 0.9), Stamp::source(300)); + for p in 301..304u16 { + assert!(a.admit_derived(inh(2, p), TruthValue::new(0.9, 0.9), &[], 1)); + } + + let ranked = rank_epiphany_attractors(&a, 2); + + assert_eq!( + ranked[0].subject, 2, + "rate-ranking must surface the dense small basin, not the large sparse one" + ); + assert!(ranked[0].rate > ranked[1].rate); + + // Build the COUNT ranking for contrast. + let mut by_count = ranked.clone(); + by_count.sort_by(|x, y| { + y.epiphanies + .cmp(&x.epiphanies) + .then(x.subject.cmp(&y.subject)) + }); + assert_eq!( + by_count[0].subject, 1, + "count-ranking buries the dense basin under the large one" + ); + + assert_ne!( + ranked[0].subject, by_count[0].subject, + "S9 / E-DOOMSCROLL: rate-ranking and count-ranking must diverge — \ + the field must not collapse into its largest basin" + ); + } + + #[test] + fn min_attempts_filters_thin_basins() { + let mut a = BeliefArena::default(); + + // Subject 1: 3 beliefs (clears min_attempts = 2). + a.observe(inh(1, 10), TruthValue::new(0.9, 0.9), Stamp::source(10)); + a.observe(inh(1, 11), TruthValue::new(0.9, 0.9), Stamp::source(11)); + assert!(a.admit_derived(inh(1, 12), TruthValue::new(0.9, 0.9), &[], 1)); + + // Subject 2: 1 belief (below min_attempts = 2). + a.observe(inh(2, 20), TruthValue::new(0.9, 0.9), Stamp::source(20)); + + let ranked = rank_epiphany_attractors(&a, 2); + assert_eq!(ranked.len(), 1); + assert_eq!(ranked[0].subject, 1); + } + + #[test] + fn ordering_is_deterministic_on_ties() { + let mut a = BeliefArena::default(); + + // Subject 5: 1 derived / 2 total = rate 0.5. + a.observe(inh(5, 50), TruthValue::new(0.9, 0.9), Stamp::source(50)); + assert!(a.admit_derived(inh(5, 51), TruthValue::new(0.9, 0.9), &[], 1)); + + // Subject 3: 1 derived / 2 total = rate 0.5 (same rate). + a.observe(inh(3, 30), TruthValue::new(0.9, 0.9), Stamp::source(30)); + assert!(a.admit_derived(inh(3, 31), TruthValue::new(0.9, 0.9), &[], 1)); + + let ranked = rank_epiphany_attractors(&a, 2); + assert_eq!(ranked.len(), 2); + assert_eq!(ranked[0].subject, 3, "tie broken by subject id ASC"); + assert_eq!(ranked[1].subject, 5); + } +} diff --git a/crates/lance-graph-planner/src/nars/mod.rs b/crates/lance-graph-planner/src/nars/mod.rs index 5c24efd21..829c6fa1f 100644 --- a/crates/lance-graph-planner/src/nars/mod.rs +++ b/crates/lance-graph-planner/src/nars/mod.rs @@ -8,6 +8,7 @@ pub mod belief; pub mod dissolution; pub mod elevation; +pub mod epiphany; pub mod facet_fold; pub mod inference; pub mod insight; @@ -20,9 +21,11 @@ pub mod truth; pub use belief::{Belief, BeliefArena, CStmt, Copula, ReviseOutcome, Stamp}; pub use dissolution::{detect_dissolution, should_elevate, staunen, wisdom, Dissolution}; pub use elevation::{elevate_field, Elevation}; +pub use epiphany::{rank_epiphany_attractors, EpiphanyAttractor}; pub use facet_fold::{cstmt_from_spo_facet, to_spo_facet}; pub use inference::NarsInference; pub use insight::{arena_graph_signals, detect, flow_state, InsightMush, Snapshot}; +pub use reach_out::{reach_out_integrate, FeltOutcome, ReachOutConfig}; pub use regulate::{regulate_cycle, CycleConfig, CycleOutcome}; pub use tactic_select::{tactic_for_bias, TacticChoice}; pub use tactics::{