From fbdf6f5440f1f25cf39e63d79b7dc7d068634580 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 17 Jun 2026 07:54:28 +0000 Subject: [PATCH 1/3] =?UTF-8?q?feat(entropy=5Fladder):=20HHTL=20fork=20lad?= =?UTF-8?q?der=20=E2=80=94=20orthogonal=20leaf=20residue=20=E2=86=92=20Fri?= =?UTF-8?q?ston=20domain=20fork?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reduction-to-practice of a standing idea: if the orthogonal (helix/CAM-PQ) leaf residue is strong enough, free energy forks into another domain (an HHTL shift = new exploration). Unifies four vocabularies that are one 2-axis structure: entropy×energy Quadrant ≡ Csikszentmihalyi FlowState(challenge,skill) ≡ Friston model-vs-surprise ≡ Staunen↔Wisdom. - residue_surprise(mag, noise_floor, sigma_k): maps the orthogonal residue magnitude (the prediction error the in-domain centroid codebook fails to explain) to the challenge axis [0,1]. Below the noise floor = quantization (≈0); linear ramp to saturation over sigma_k·noise_floor. Threshold provenance per I-NOISE-FLOOR-JIRAK (Berry-Esseen wrong under CAM-PQ weak dependence); the ramp is an honest proxy pending Jirak calibration, not a claimed bound. - ForkAction {Commit, DescendDeeper, ForkBasin, ForkDomain} + fork_decision(...): bands the challenge−skill delta exactly like the shipped mul::flow_state_from (Anxiety >0.2, Flow |d|<0.15, Boredom <-0.2), then HHTL depth decides descend-vs-fork. ForkDomain (mint a new classid domain = the Friston model-switch) requires BOTH leaf depth AND challenge≫skill — the operator's "strong enough AT THE LEAF" invariant. Pure functions + one enum; no new struct/layer; composes with the existing Quadrant. 5 new lib tests + 2 doctests, including a cross-check that an Anxiety/ForkDomain residue lands in the high-entropy (Staunen/Confusion) half of the shipped Quadrant. clippy clean. Co-Authored-By: Claude --- src/hpc/entropy_ladder.rs | 179 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 179 insertions(+) diff --git a/src/hpc/entropy_ladder.rs b/src/hpc/entropy_ladder.rs index f27c5692..5979b1a6 100644 --- a/src/hpc/entropy_ladder.rs +++ b/src/hpc/entropy_ladder.rs @@ -210,6 +210,126 @@ pub fn entropy_class(h: f64) -> u8 { ((h * 4.0) as u8).min(3) } +// ── HHTL fork ladder — orthogonal leaf residue → Friston domain fork ────────── +// +// The unification (board canon): the entropy×energy `Quadrant`, Csikszentmihalyi's +// flow channel (`lance_graph_contract::mul::FlowState`), Friston free-energy +// minimization, and the Staunen↔Wisdom ladder are ONE 2-axis structure. The +// CHALLENGE axis is surprise = the magnitude of the orthogonal helix/CAM-PQ leaf +// residue the current domain's centroid codebook fails to explain; the SKILL axis +// is the in-domain codebook's remaining resolving capacity. The fork rule is the +// "anxiety escape": when challenge ≫ skill *at leaf depth*, the model cannot +// minimize free energy in-domain, so it switches the model — mint a new classid +// domain (an HHTL shift into a new exploration). This is the reduction-to-practice +// of "if the orthogonal leaf residue is strong enough, free energy forks into +// another domain." + +/// Map an orthogonal-residue magnitude to the surprise/challenge axis `[0, 1]`, +/// relative to the substrate noise floor. +/// +/// The helix/CAM-PQ leaf residue is the component left after the assigned centroid +/// (the "place") is subtracted — geometrically orthogonal to that centroid, so its +/// magnitude is the prediction error the current domain fails to explain. Below the +/// noise floor it is mere quantization (≈0 surprise); the excess scales linearly to +/// saturation over `sigma_k · noise_floor`. +/// +/// **Threshold provenance (`I-NOISE-FLOOR-JIRAK`):** `noise_floor` should be the +/// Berry-Esseen/Jirak weak-dependence bound (CAM-PQ contamination makes classic IID +/// Berry-Esseen wrong), and `sigma_k` the σ-multiple deemed "genuinely new". The +/// linear ramp here is an honest proxy pending a Jirak-derived calibration, not a +/// claimed bound. +/// +/// # Examples +/// ``` +/// use ndarray::hpc::entropy_ladder::residue_surprise; +/// assert!(residue_surprise(0.001, 0.004, 6.0) < 1e-12); // below floor → no surprise +/// assert!((residue_surprise(1.0, 0.004, 6.0) - 1.0).abs() < 1e-12); // saturated +/// let mid = residue_surprise(0.016, 0.004, 6.0); // excess 0.012 over span 0.024 +/// assert!((mid - 0.5).abs() < 1e-9); +/// ``` +#[inline] +pub fn residue_surprise(residue_mag: f64, noise_floor: f64, sigma_k: f64) -> f64 { + let nf = noise_floor.max(f64::MIN_POSITIVE); + let span = (sigma_k.max(f64::MIN_POSITIVE)) * nf; + let excess = (residue_mag - nf).max(0.0); + (excess / span).clamp(0.0, 1.0) +} + +/// What to do with a leaf residue, governed by the Csikszentmihalyi flow channel +/// (challenge = residue surprise, skill = in-domain codebook capacity). Mirrors +/// `lance_graph_contract::mul::FlowState`, projected onto the HHTL cascade. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +#[repr(u8)] +pub enum ForkAction { + /// **Boredom** — challenge ≪ skill: the domain over-explains. Commit (and the + /// caller may coarsen the address). + Commit = 0, + /// **Flow/Transition with depth remaining** — challenge ≈ skill: the codebook can + /// still reach the point; descend HEEL→HIP→TWIG→LEAF one tier. + DescendDeeper = 1, + /// **Flow/Transition at leaf depth** — resolvable, but no finer tier remains: a + /// sibling basin within the SAME classid codebook. + ForkBasin = 2, + /// **Anxiety at leaf depth** — challenge ≫ skill and irreducible in-domain: mint a + /// NEW classid domain (HHTL shift = new exploration). Friston: switch the + /// generative model when free energy can't be minimized within it. + ForkDomain = 3, +} + +/// The fork decision. `challenge = residue_surprise(residue_mag, noise_floor, +/// sigma_k)`; `in_domain_skill ∈ [0,1]` is the codebook's remaining resolving +/// capacity. The challenge↔skill delta is banded exactly like the shipped +/// `flow_state_from` (Anxiety `>0.2`, Flow `|δ|<0.15`, Boredom `<-0.2`), then the +/// HHTL depth decides descend-vs-fork: +/// +/// * **Boredom** → [`ForkAction::Commit`]. +/// * **Anxiety, depth < max** → [`ForkAction::DescendDeeper`] (apply skill at a finer +/// tier before declaring the residue irreducible — the fork is a *leaf* condition). +/// * **Anxiety, depth == max** → [`ForkAction::ForkDomain`] (the orthogonal leaf +/// residue is strong enough: free energy forks into a new domain). +/// * **Flow/Transition** → [`ForkAction::DescendDeeper`] while `depth < max`, else +/// [`ForkAction::ForkBasin`]. +/// +/// # Examples +/// ``` +/// use ndarray::hpc::entropy_ladder::{fork_decision, ForkAction}; +/// // Huge residue, low in-domain skill, already at the leaf → fork to a new domain. +/// let a = fork_decision(1.0, 0.1, 3, 3, 0.004, 6.0); +/// assert_eq!(a, ForkAction::ForkDomain); +/// // Same surprise but a coarse tier remains → descend first, don't fork yet. +/// assert_eq!(fork_decision(1.0, 0.1, 1, 3, 0.004, 6.0), ForkAction::DescendDeeper); +/// // Tiny residue → the domain over-explains → commit. +/// assert_eq!(fork_decision(0.002, 0.5, 3, 3, 0.004, 6.0), ForkAction::Commit); +/// ``` +pub fn fork_decision( + residue_mag: f64, in_domain_skill: f64, depth: u8, max_depth: u8, noise_floor: f64, sigma_k: f64, +) -> ForkAction { + let challenge = residue_surprise(residue_mag, noise_floor, sigma_k); + let skill = in_domain_skill.clamp(0.0, 1.0); + let delta = challenge - skill; + let at_leaf = depth >= max_depth; + if delta < -0.2 { + // Boredom — skill over-covers the challenge. + ForkAction::Commit + } else if delta > 0.2 { + // Anxiety — challenge exceeds skill. Fork only once the residue is a *leaf* + // residue; otherwise a finer tier may still resolve it. + if at_leaf { + ForkAction::ForkDomain + } else { + ForkAction::DescendDeeper + } + } else { + // Flow / Transition — matched. Resolve in-domain: descend if we can, else a + // sibling basin in the same codebook. + if at_leaf { + ForkAction::ForkBasin + } else { + ForkAction::DescendDeeper + } + } +} + #[cfg(test)] mod tests { use super::*; @@ -271,6 +391,65 @@ mod tests { assert_eq!(entropy_class(0.99), 3); } + #[test] + fn residue_surprise_floor_ramp_saturation() { + // Below the noise floor → quantization only → zero surprise. + assert!(residue_surprise(0.003, 0.004, 6.0) < 1e-12); + assert!(residue_surprise(0.004, 0.004, 6.0) < 1e-12); + // Linear ramp: excess / (sigma_k · nf). nf=0.004, span=0.024. + assert!((residue_surprise(0.016, 0.004, 6.0) - 0.5).abs() < 1e-9); + // Saturates at 1. + assert!((residue_surprise(0.028, 0.004, 6.0) - 1.0).abs() < 1e-9); + assert!((residue_surprise(10.0, 0.004, 6.0) - 1.0).abs() < 1e-9); + // Monotone in residue magnitude. + assert!(residue_surprise(0.01, 0.004, 6.0) < residue_surprise(0.02, 0.004, 6.0)); + } + + #[test] + fn fork_ladder_four_actions() { + let (nf, k) = (0.004, 6.0); + // Boredom: tiny residue, ample skill → commit. + assert_eq!(fork_decision(0.002, 0.6, 3, 3, nf, k), ForkAction::Commit); + // Anxiety at leaf: strong orthogonal leaf residue, low skill → fork domain. + assert_eq!(fork_decision(1.0, 0.1, 3, 3, nf, k), ForkAction::ForkDomain); + // Anxiety but a coarse tier remains → descend before forking (leaf condition). + assert_eq!(fork_decision(1.0, 0.1, 1, 3, nf, k), ForkAction::DescendDeeper); + // Flow at leaf (challenge ≈ skill): resolvable in-domain → sibling basin. + // challenge=0.5 (residue 0.016), skill=0.5 → delta 0 → Flow. + assert_eq!(fork_decision(0.016, 0.5, 3, 3, nf, k), ForkAction::ForkBasin); + // Flow with depth remaining → descend. + assert_eq!(fork_decision(0.016, 0.5, 0, 3, nf, k), ForkAction::DescendDeeper); + } + + #[test] + fn fork_domain_only_when_residue_is_strong_at_leaf() { + let (nf, k) = (0.004, 6.0); + // The operator's invariant: ForkDomain requires BOTH (a) leaf depth AND + // (b) a residue strong enough that challenge ≫ skill. Weaken either and the + // domain must NOT fork. + assert_eq!(fork_decision(1.0, 0.1, 3, 3, nf, k), ForkAction::ForkDomain); + // (a) not at leaf → descend, never fork. + assert_ne!(fork_decision(1.0, 0.1, 2, 3, nf, k), ForkAction::ForkDomain); + // (b) skill matches the (saturated) challenge → Flow, not Anxiety → basin. + assert_ne!(fork_decision(1.0, 0.9, 3, 3, nf, k), ForkAction::ForkDomain); + } + + #[test] + fn fork_anxiety_aligns_with_high_surprise_quadrant() { + // Cross-check the unification: an Anxiety/ForkDomain residue is high-challenge, + // so on the entropy×energy plane (challenge as entropy) it lands in the + // high-entropy half (Staunen at low energy / Confusion at high energy) — never + // Boredom/Wisdom. This ties ForkAction to the shipped Quadrant. + let challenge = residue_surprise(1.0, 0.004, 6.0); // saturated → 1.0 + assert!(challenge >= 0.5); + assert_eq!(Quadrant::classify(challenge, 0.1), Quadrant::Staunen); + assert_eq!(Quadrant::classify(challenge, 0.9), Quadrant::Confusion); + // And a Boredom/Commit residue is low-challenge → low-entropy half. + let calm = residue_surprise(0.002, 0.004, 6.0); // 0.0 + assert_eq!(Quadrant::classify(calm, 0.1), Quadrant::Boredom); + assert_eq!(Quadrant::classify(calm, 0.9), Quadrant::Wisdom); + } + /// Validation: entropy is a reliability proxy. Build a population of edges /// whose belief `(f, c)` is estimated from `n_obs` Bernoulli(p) draws, then /// measure each edge's empirical prediction accuracy against fresh draws. From eae4321dddb6eda5e45f0e33e1d73bdd93393135 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 17 Jun 2026 11:03:48 +0000 Subject: [PATCH 2/3] docs(blackboard): record HHTL fork-ladder decision + loose ends MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Per the ndarray agent protocol (update blackboard after completing work): log the entropy_ladder fork-ladder addition, the four-vocabulary unification, the honest [S] joints (real CoarseResidue feed, orthogonality arbitration, Jirak σ), and that the driver-side wire merges with lance-graph materialize's from_live step. Co-Authored-By: Claude --- .claude/blackboard.md | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/.claude/blackboard.md b/.claude/blackboard.md index a6cee9c9..b5a7e0d4 100644 --- a/.claude/blackboard.md +++ b/.claude/blackboard.md @@ -3,6 +3,43 @@ > **Read this first.** The "Polyglot Notebook" architecture below is a > separate/older program, not the current epoch. +## 2026-06-17 — DECISION: HHTL fork ladder coded in `hpc::entropy_ladder` (CONJECTURE) + +Reified the operator's standing idea — *if the orthogonal (helix/CAM-PQ) +leaf residue is strong enough, free energy forks into another domain +(HHTL shift = new exploration)* — as pure functions beside the existing +entropy/quadrant code. Unifies four vocabularies as one 2-axis structure: +`entropy_ladder::Quadrant(entropy,energy)` ≡ `lance-graph-contract::mul:: +FlowState(challenge,skill)` (Csikszentmihalyi) ≡ Friston model-vs-surprise +≡ Staunen↔Wisdom. + +- `residue_surprise(mag, noise_floor, sigma_k) → [0,1]` — orthogonal residue + magnitude (prediction error the in-domain centroid codebook fails to + explain) → challenge axis. Below floor = quantization (≈0); linear ramp + over `sigma_k·noise_floor`. Threshold provenance per `I-NOISE-FLOOR-JIRAK` + (Berry-Esseen wrong under CAM-PQ weak dependence); ramp is an honest proxy + pending Jirak calibration, **not** a claimed bound. +- `ForkAction {Commit, DescendDeeper, ForkBasin, ForkDomain}` + `fork_decision` + — bands challenge−skill exactly like the shipped `mul::flow_state_from` + (Anxiety >0.2, Flow |δ|<0.15, Boredom <-0.2); HHTL depth decides descend-vs- + fork. `ForkDomain` (mint a new classid = the Friston model-switch) requires + BOTH leaf depth AND challenge≫skill — the "strong enough AT THE LEAF" invariant. + +Layering kept honest: the `FlowState` *assessment* stays in lance-graph +(thinking); the fork *math* lives here in ndarray (substrate, where residue + +energy physically are) — per the Architecture Rule. Pure fns + one enum, no +struct/layer, composes with `Quadrant`. 5 lib + 2 doctests, clippy clean. +Branch `claude/jirak-math-theorems-harvest-rfii13`. + +**Loose ends (CONJECTURE → gated):** (a) feed the *real* `edge_codec:: +CoarseResidue` magnitude from the live codec into `fork_decision` (currently +caller-supplied); (b) `ForkDomain` vs `ForkBasin` should be arbitrated by +residue *orthogonality* (⊥ all in-domain centroids = genuinely new), not the +depth+delta proxy; (c) Jirak-derived σ threshold to replace the `sigma_k` +proxy. This driver-side wire merges with lance-graph `materialize`'s +`ThoughtCtx::from_live` step (same call-site). + + ## 2026-06-10 — DECISION: GUID prefix→shape routing crystallized (docs-only) The operator-pinned canonical GUID (`OGAR/CLAUDE.md`: hex dash-groups = From 786110aa78b6371660667e6b4ae0bdc37b658d9f Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 17 Jun 2026 11:11:41 +0000 Subject: [PATCH 3/3] fix(entropy_ladder): NaN guard on degenerate calibration + doc threshold accuracy MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PR #221 review: - Codex P2: residue_surprise returned NaN when noise_floor·sigma_k are both zero/non-positive — MIN_POSITIVE² underflows to 0.0, so a below-floor residue hit 0.0/0.0. Floor the span AFTER the multiply so the result is always finite and in [0,1]; the NaN otherwise defeated fork_decision's ±0.2 bands. Added a degenerate-calibration finiteness regression test. - CodeRabbit: the fork_decision doc + blackboard claimed the bands match flow_state_from "exactly (Flow |δ|<0.15)", but the implementation uses the ±0.2 Anxiety/Boredom boundaries and collapses Flow+Transition into one in-domain branch. Corrected both docs to state the implemented bands accurately (Anxiety δ>0.2, Boredom δ<-0.2, matched middle |δ|≤0.2 resolves in-domain). Code unchanged — the ±0.2 thresholds already match flow_state_from. 12 lib + 7 doctests green, clippy clean. Co-Authored-By: Claude --- .claude/blackboard.md | 6 ++++-- src/hpc/entropy_ladder.rs | 45 +++++++++++++++++++++++++++++---------- 2 files changed, 38 insertions(+), 13 deletions(-) diff --git a/.claude/blackboard.md b/.claude/blackboard.md index b5a7e0d4..9a6d263f 100644 --- a/.claude/blackboard.md +++ b/.claude/blackboard.md @@ -20,8 +20,10 @@ FlowState(challenge,skill)` (Csikszentmihalyi) ≡ Friston model-vs-surprise (Berry-Esseen wrong under CAM-PQ weak dependence); ramp is an honest proxy pending Jirak calibration, **not** a claimed bound. - `ForkAction {Commit, DescendDeeper, ForkBasin, ForkDomain}` + `fork_decision` - — bands challenge−skill exactly like the shipped `mul::flow_state_from` - (Anxiety >0.2, Flow |δ|<0.15, Boredom <-0.2); HHTL depth decides descend-vs- + — bands challenge−skill on the shipped `mul::flow_state_from` boundaries + (Anxiety δ>0.2, Boredom δ<-0.2; the matched middle |δ|≤0.2, which + flow_state_from splits into Flow/Transition, collapses to one in-domain + branch here); HHTL depth decides descend-vs- fork. `ForkDomain` (mint a new classid = the Friston model-switch) requires BOTH leaf depth AND challenge≫skill — the "strong enough AT THE LEAF" invariant. diff --git a/src/hpc/entropy_ladder.rs b/src/hpc/entropy_ladder.rs index 5979b1a6..ad58bae2 100644 --- a/src/hpc/entropy_ladder.rs +++ b/src/hpc/entropy_ladder.rs @@ -250,7 +250,11 @@ pub fn entropy_class(h: f64) -> u8 { #[inline] pub fn residue_surprise(residue_mag: f64, noise_floor: f64, sigma_k: f64) -> f64 { let nf = noise_floor.max(f64::MIN_POSITIVE); - let span = (sigma_k.max(f64::MIN_POSITIVE)) * nf; + // Guard the span AFTER the multiply: `MIN_POSITIVE * MIN_POSITIVE` underflows to + // 0.0, so a degenerate (zero/negative) calibration would make `excess / span` + // evaluate `0.0 / 0.0 = NaN` and defeat the `fork_decision` bands. Floor the + // product itself so the result is always finite and in `[0, 1]`. + let span = (sigma_k.max(f64::MIN_POSITIVE) * nf).max(f64::MIN_POSITIVE); let excess = (residue_mag - nf).max(0.0); (excess / span).clamp(0.0, 1.0) } @@ -278,17 +282,21 @@ pub enum ForkAction { /// The fork decision. `challenge = residue_surprise(residue_mag, noise_floor, /// sigma_k)`; `in_domain_skill ∈ [0,1]` is the codebook's remaining resolving -/// capacity. The challenge↔skill delta is banded exactly like the shipped -/// `flow_state_from` (Anxiety `>0.2`, Flow `|δ|<0.15`, Boredom `<-0.2`), then the -/// HHTL depth decides descend-vs-fork: +/// capacity. The challenge↔skill delta `δ` is banded on the shipped +/// `flow_state_from` boundaries — **Anxiety `δ>0.2`** and **Boredom `δ<-0.2`** — +/// then HHTL depth decides descend-vs-fork. The matched middle (`-0.2 ≤ δ ≤ 0.2`, +/// which `flow_state_from` further splits into Flow `|δ|<0.15` and Transition) is +/// uniformly "resolvable in-domain" here, so this ladder collapses Flow+Transition +/// into one branch — the Flow/Transition distinction does not change the action. /// -/// * **Boredom** → [`ForkAction::Commit`]. -/// * **Anxiety, depth < max** → [`ForkAction::DescendDeeper`] (apply skill at a finer -/// tier before declaring the residue irreducible — the fork is a *leaf* condition). -/// * **Anxiety, depth == max** → [`ForkAction::ForkDomain`] (the orthogonal leaf -/// residue is strong enough: free energy forks into a new domain). -/// * **Flow/Transition** → [`ForkAction::DescendDeeper`] while `depth < max`, else -/// [`ForkAction::ForkBasin`]. +/// * **Boredom** (`δ<-0.2`) → [`ForkAction::Commit`]. +/// * **Anxiety** (`δ>0.2`), `depth < max` → [`ForkAction::DescendDeeper`] (apply skill +/// at a finer tier before declaring the residue irreducible — fork is a *leaf* +/// condition). +/// * **Anxiety** (`δ>0.2`), `depth == max` → [`ForkAction::ForkDomain`] (the orthogonal +/// leaf residue is strong enough: free energy forks into a new domain). +/// * **Flow/Transition** (`|δ|≤0.2`) → [`ForkAction::DescendDeeper`] while +/// `depth < max`, else [`ForkAction::ForkBasin`]. /// /// # Examples /// ``` @@ -405,6 +413,21 @@ mod tests { assert!(residue_surprise(0.01, 0.004, 6.0) < residue_surprise(0.02, 0.004, 6.0)); } + #[test] + fn residue_surprise_degenerate_calibration_is_finite() { + // Codex #221: zero/negative calibration must not yield NaN (MIN_POSITIVE² + // underflows to 0.0 → the span guard must run AFTER the multiply). Every + // result stays finite and in [0, 1], so fork_decision's bands stay valid. + for &(mag, nf, k) in &[(0.0, 0.0, 0.0), (0.5, 0.0, 0.0), (0.0, -1.0, -1.0), (1.0, 0.0, 6.0), (0.0, 0.004, 0.0)] + { + let s = residue_surprise(mag, nf, k); + assert!(s.is_finite() && (0.0..=1.0).contains(&s), "got {s} for ({mag},{nf},{k})"); + } + // And the downstream decision still lands in a real band (not the NaN path). + let a = fork_decision(0.0, 0.5, 3, 3, 0.0, 0.0); + assert!(matches!(a, ForkAction::Commit | ForkAction::ForkBasin | ForkAction::DescendDeeper)); + } + #[test] fn fork_ladder_four_actions() { let (nf, k) = (0.004, 6.0);