Skip to content

Commit 22e50f5

Browse files
committed
fix(materialize): resolve PR #515 review — free_energy primary, rest needs low F
Six review findings on the F→34→F loop, all valid: 1/4 select_tactic: free_energy is now the SOLE primary axis (mechanism band), with dissonance demoted to a +1 tie-weight toward inference tactics. The prior `dissonance >= 0.5` short-circuit overrode the surprise-chosen mechanism, breaking the module's own materialization criterion for any contradicted base. Mechanism match now scores +5 so it strictly outweighs bucket(2)+tier(1)+reconcile(1). 2 materialize: settle/attend (sd↓, dissonance↓, confidence↑) now apply ONLY on a tactic that actually fired — a blocked tactic must not fake progress; the loop ends rather than spin on an unchanged state. 5 rest now requires gate FLOW AND surprise < HOMEOSTASIS_FLOOR — a cool gate with unresolved surprise is not rest. Added ATTEND_GAIN (0.35) so confidence rises each fired step, guaranteeing reported surprise descends (active inference), not just sd decay. 6 bounded Vec::with_capacity(max_steps.min(64)). 3 EPIPHANIES E-MATERIALIZED-AWARENESS-1 wording: "perturbing the awareness encoding" → "the surprise/free-energy signal"; rest condition corrected to FLOW && surprise<floor. Tests: added a dissonance=0.7 regression (surprise stays causal under contradiction); already_at_rest now sets confidence=0.95/dissonance=0.0 so the stricter rest condition holds. 6/6 green, clippy clean. Co-Authored-By: Claude <noreply@anthropic.com>
1 parent ab7f203 commit 22e50f5

2 files changed

Lines changed: 68 additions & 24 deletions

File tree

.claude/board/EPIPHANIES.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,9 @@
33
**Status:** FINDING for the criterion + loop (reduction-to-practice **shipped**: `lance-graph-contract::materialize`, 6 tests green, zero-dep/offline). The broader "this is the system's awareness" reading stays **[NOVEL — probe-gated]**: no prior art by construction (ours — the 2³-rung→NARS-candidate→34-tactic dispatch loop), validity established by the perturbation probe, not citation.
44
**Confidence:** High on the criterion + the shipped loop; the wire to the *real* substrate (driver-side `ThoughtCtx::from_live` + version-diff provenance) is the gated next step.
55

6-
**The criterion (falsifiable).** *Awareness materializes iff perturbing the awareness encoding changes which tactic fires.* If dispatch is invariant to the awareness state, the awareness is a **dead label** — "awareness that can never materialize." `materialize::awareness_is_causal(base, lo_f, hi_f)` is the predicate; the test `awareness_free_energy_is_causal_in_dispatch` is the green falsifier, and `non_awareness_fields_are_inert` is its specificity control (candidates/beliefs must NOT steer dispatch).
6+
**The criterion (falsifiable).** *Awareness materializes iff perturbing the surprise/free-energy signal changes which tactic fires.* If dispatch is invariant to the awareness state, the awareness is a **dead label** — "awareness that can never materialize." `materialize::awareness_is_causal(base, lo_f, hi_f)` is the predicate; the test `awareness_free_energy_is_causal_in_dispatch` is the green falsifier, and `non_awareness_fields_are_inert` is its specificity control (candidates/beliefs must NOT steer dispatch).
77

8-
**The wire that was missing (now built).** The 34 tactics (`recipe_kernels`, the canonical "34" — the ndarray `hpc/styles/*` set is divergent/registry-less and is NOT canonical) were dispatch *targets* with no selector and an open loop (they ran only in an example against a toy ctx; the driver loop ran the *12* threshold ordinals, leaving the rich 34 inert). `materialize` adds: (a) **`select_tactic`** — awareness→id, with `free_energy` (surprise) as the **primary** axis so dispatch tracks awareness by construction; (b) **`materialize`** — the closed loop: select → `Tactic::run` (folds `delta_conf`) → settle the gate (dispersion/contradiction decay) → recompute surprise → re-dispatch; **rest is guaranteed** when the CollapseGate reaches FLOW (`sd<SD_FLOW`), because attending decays dispersion monotonically. "The shader can't resist the thinking" made literal.
8+
**The wire that was missing (now built).** The 34 tactics (`recipe_kernels`, the canonical "34" — the ndarray `hpc/styles/*` set is divergent/registry-less and is NOT canonical) were dispatch *targets* with no selector and an open loop (they ran only in an example against a toy ctx; the driver loop ran the *12* threshold ordinals, leaving the rich 34 inert). `materialize` adds: (a) **`select_tactic`** — awareness→id, with `free_energy` (surprise) as the **primary** axis so dispatch tracks awareness by construction; (b) **`materialize`** — the closed loop: select → `Tactic::run` (folds `delta_conf`) → settle the gate (dispersion/contradiction decay) → recompute surprise → re-dispatch; **rest is reached** when the CollapseGate is in FLOW (`sd<SD_FLOW`) **and** residual surprise falls below `HOMEOSTASIS_FLOOR` (0.2) — a cool gate with unresolved surprise is not rest. For a *firing* chain this is guaranteed: attending decays dispersion and raises confidence each fired step, so both `sd` and surprise descend monotonically; a *blocked* tactic ends the run (re-dispatch of an unchanged state cannot unblock it). "The shader can't resist the thinking" made literal. The settle/attend updates fire only on a tactic that actually fired (review #515: a blocked tactic must not fake progress; `free_energy` stays the primary dispatch axis even under contradiction — `dissonance` is a lower-weight secondary, not an override).
99

1010
**Prior-art positioning (not competitors — background for the disclosure).** NOTEARS / PCMCI / DCDI / ICP / SEA are adjacent observational/interventional *discovery* methods (arXiv 1803.01422 / 1702.07007 / 2007.01754 / 1501.01332 / 2402.01929); our loop does not *discover* a DAG — it dispatches reasoning over recorded/candidate structure and lets NARS revise. **Operating boundary respected, [G]:** Janzing-Schölkopf (0804.3678) — Shannon-symmetric, colliders-only observationally; full orientation needs mechanism asymmetry (so dispatch never claims identified orientation, only revisable candidates).
1111

crates/lance-graph-contract/src/materialize.rs

Lines changed: 66 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -19,14 +19,15 @@
1919
//!
2020
//! ```text
2121
//! awareness state ──select_tactic──► one of the 34 ──run──► fold delta_conf
22-
//! ▲ │ settle gate (sd↓, dissonance↓)
22+
//! ▲ │ on fire: sd↓, dissonance↓, confidence↑
2323
//! └────────────── recompute free-energy ◄──────────────────┘
24-
//! rest when the CollapseGate enters FLOW (sd < SD_FLOW) surprise resolved.
24+
//! rest when the CollapseGate is in FLOW (sd < SD_FLOW) AND surprise < floor.
2525
//! ```
2626
//!
2727
//! Awareness is not *read by* a controller that decides to think; it *is* the
28-
//! gradient that selects the next tactic. The loop rests when the gate settles —
29-
//! guaranteed, because attending decays dispersion each fired step.
28+
//! gradient that selects the next tactic. The loop rests when the gate settles and
29+
//! surprise is resolved — guaranteed for a firing chain, because attending decays
30+
//! both dispersion and surprise each fired step.
3031
//!
3132
//! Zero-dep, deterministic, offline-tested. This is the reduction-to-practice for
3233
//! the 2³-rung → NARS-candidate → 34-tactic doctrine; persisting the dispatch trace
@@ -47,6 +48,11 @@ pub const HOMEOSTASIS_FLOOR: f32 = 0.2;
4748
const SETTLE_SD: f32 = 0.85;
4849
/// Per-fired-step contradiction relaxation — engaging a tactic reconciles split.
4950
const SETTLE_DISSONANCE: f32 = 0.6;
51+
/// Per-fired-step confidence gain — attending to a tactic moves confidence toward
52+
/// 1 (active inference: engaging the world resolves uncertainty). Without it, a
53+
/// fired tactic with zero `delta_conf` would leave surprise pinned and the loop
54+
/// could ride `sd` decay alone; this guarantees reported surprise also descends.
55+
const ATTEND_GAIN: f32 = 0.35;
5056

5157
/// Re-derive the CollapseGate state from dispersion (`ThoughtCtx::gate_state` is
5258
/// private; the thresholds `SD_FLOW`/`SD_BLOCK` are public).
@@ -69,16 +75,20 @@ fn gate_of(sd: f32) -> GateState {
6975
/// `rung` (depth → difficulty tier) are secondary modulators. Deterministic; scores
7076
/// every recipe by metadata match and takes the lowest id on a tie.
7177
pub fn select_tactic(ctx: &ThoughtCtx) -> u8 {
72-
// What kind of reasoning does this awareness state call for?
73-
let want_mech = if ctx.dissonance >= 0.5 {
74-
Mechanism::TruthAwareInference // a contradiction wants revision/abduction
75-
} else if ctx.free_energy >= 0.66 {
78+
// PRIMARY: what kind of reasoning does the surprise level call for? `free_energy`
79+
// alone picks the mechanism — this is the causal axis (the materialization
80+
// criterion). It is NOT overridden by any other field; a contradicted state with
81+
// low surprise still wants routine work, and high surprise still wants a leap.
82+
let want_mech = if ctx.free_energy >= 0.66 {
7683
Mechanism::StructuralDivergence // high surprise wants a creative leap
7784
} else if ctx.free_energy >= 0.33 {
7885
Mechanism::TruthAwareInference // mid surprise wants inference
7986
} else {
8087
Mechanism::ParallelIndependence // low surprise: routine parallel work
8188
};
89+
// SECONDARY: a contradiction nudges toward revision/abduction, but only as a
90+
// tie-weight — it never replaces the surprise-chosen mechanism.
91+
let wants_reconciliation = ctx.dissonance >= 0.5;
8292
// Where should it execute? (the gate picks the hardware bucket)
8393
let want_bucket = match gate_of(ctx.sd) {
8494
GateState::Block => Bucket::Gate,
@@ -99,15 +109,20 @@ pub fn select_tactic(ctx: &ThoughtCtx) -> u8 {
99109
for id in 1..=34u8 {
100110
if let Some(r) = recipe(id) {
101111
let mut score = 0;
112+
// Primary mechanism match outweighs bucket(2) + tier(1) + reconcile(1).
102113
if r.mechanism == want_mech {
103-
score += 3;
114+
score += 5;
104115
}
105116
if r.bucket == want_bucket {
106117
score += 2;
107118
}
108119
if r.tier == want_tier {
109120
score += 1;
110121
}
122+
// Secondary contradiction signal: a faint pull toward inference tactics.
123+
if wants_reconciliation && r.mechanism == Mechanism::TruthAwareInference {
124+
score += 1;
125+
}
111126
if score > best_score {
112127
best_score = score;
113128
best_id = id;
@@ -150,34 +165,50 @@ pub struct Trace {
150165
pub final_free_energy: f32,
151166
}
152167

153-
/// **The closed `F → 34 → F` loop.** Each step: if the gate is in FLOW the loop
154-
/// rests (surprise resolved); else select a tactic from the awareness state, run it
155-
/// (folding `delta_conf` into confidence), settle the gate (dispersion + contradiction
156-
/// decay — attending reconciles), and recompute surprise. `max_steps` bounds the run;
157-
/// rest is *guaranteed* within `~log_{1/SETTLE_SD}(sd/SD_FLOW)` fired steps because
158-
/// dispersion decays monotonically into FLOW.
168+
/// **The closed `F → 34 → F` loop.** Each step: recompute surprise; if the gate is
169+
/// in FLOW *and* surprise fell below [`HOMEOSTASIS_FLOOR`] the loop rests; else select
170+
/// a tactic from the awareness state and run it. A tactic that *fired* settles the
171+
/// gate (dispersion + contradiction decay) and raises confidence ([`ATTEND_GAIN`],
172+
/// active inference); a *blocked* tactic changes nothing, so the loop stops rather
173+
/// than spin on an unchanged state. `max_steps` bounds the run; for a firing chain,
174+
/// rest is reached within `~log_{1/SETTLE_SD}(sd/SD_FLOW)` steps because dispersion
175+
/// and surprise decay monotonically.
159176
pub fn materialize(ctx: &mut ThoughtCtx, max_steps: usize) -> Trace {
160-
let mut steps = Vec::with_capacity(max_steps);
177+
// Bound the up-front allocation: a settling run is short (~log decay), so don't
178+
// reserve for a pathological `max_steps` the loop will never reach.
179+
let mut steps = Vec::with_capacity(max_steps.min(64));
161180
for _ in 0..max_steps {
162-
if gate_of(ctx.sd) == GateState::Flow {
181+
ctx.free_energy = recompute_free_energy(ctx);
182+
// Rest only when the gate is in FLOW *and* surprise actually resolved — a
183+
// cool gate with residual surprise is not rest, it must keep dispatching.
184+
if gate_of(ctx.sd) == GateState::Flow && ctx.free_energy < HOMEOSTASIS_FLOOR {
163185
break; // settled — the shader rests
164186
}
165187
let id = select_tactic(ctx);
166188
let Some(tactic) = kernel(id) else {
167189
break; // unreachable: id is always 1..=34
168190
};
169191
let out = tactic.run(ctx); // folds out.delta_conf into ctx.confidence
170-
ctx.sd *= SETTLE_SD; // attending settles dispersion → toward FLOW
171-
ctx.dissonance *= SETTLE_DISSONANCE;
172-
ctx.free_energy = recompute_free_energy(ctx);
192+
if out.fired {
193+
// Only a tactic that actually fired settles the gate and resolves
194+
// surprise — a blocked tactic changed nothing, so don't fake progress.
195+
ctx.sd *= SETTLE_SD; // attending settles dispersion → toward FLOW
196+
ctx.dissonance *= SETTLE_DISSONANCE;
197+
ctx.confidence =
198+
(ctx.confidence + ATTEND_GAIN * (1.0 - ctx.confidence)).clamp(0.0, 1.0);
199+
}
173200
steps.push(Step {
174201
tactic_id: id,
175202
fired: out.fired,
176203
delta_conf: out.delta_conf,
177204
});
205+
if !out.fired {
206+
break; // a blocked tactic won't unblock on re-dispatch of the same state
207+
}
178208
}
209+
ctx.free_energy = recompute_free_energy(ctx);
179210
Trace {
180-
rested: gate_of(ctx.sd) == GateState::Flow,
211+
rested: gate_of(ctx.sd) == GateState::Flow && ctx.free_energy < HOMEOSTASIS_FLOOR,
181212
final_confidence: ctx.confidence,
182213
final_free_energy: ctx.free_energy,
183214
steps,
@@ -219,6 +250,15 @@ mod tests {
219250
awareness_is_causal(&b, 0.1, 0.9),
220251
"free_energy must steer dispatch — else awareness is a dead label"
221252
);
253+
// Regression: surprise stays causal even for a contradicted base. dissonance
254+
// is a SECONDARY tie-weight, not an override — it must not pin the mechanism
255+
// and erase free_energy's steering (the bug review #515 caught).
256+
let mut contradicted = base();
257+
contradicted.dissonance = 0.7;
258+
assert!(
259+
awareness_is_causal(&contradicted, 0.1, 0.9),
260+
"free_energy must steer dispatch even under contradiction"
261+
);
222262
// Sweep free_energy: dispatch must take ≥ 2 distinct tactics (not stuck).
223263
let ids: BTreeSet<u8> = (0..=10)
224264
.map(|i| {
@@ -314,9 +354,13 @@ mod tests {
314354

315355
#[test]
316356
fn already_at_rest_dispatches_nothing() {
317-
// FLOW on entry (sd < SD_FLOW) ⇒ no surprise ⇒ no dispatch (the shader rests).
357+
// FLOW on entry (sd < SD_FLOW) AND resolved surprise ⇒ no dispatch (rest).
358+
// Rest now requires free_energy < floor, so the base must be confident and
359+
// uncontradicted as well as gate-cool — a cool gate alone is not rest.
318360
let mut c = base();
319361
c.sd = 0.05;
362+
c.confidence = 0.95;
363+
c.dissonance = 0.0;
320364
let trace = materialize(&mut c, 64);
321365
assert!(trace.rested && trace.steps.is_empty());
322366
}

0 commit comments

Comments
 (0)