Skip to content

Commit 6eb0fa6

Browse files
committed
feat(planner): D-MBX-A6-P3a — StyleStrategy wires thinking-styles as the planning substrate
The thinking-style planning substrate: a PlanStrategy (#18 in default_strategies) that resolves the active ThinkingStyle -> cluster() -> mechanism -> selects which of the 34 recipe_kernels Tactics fire over a ThoughtCtx built from PlanContext markers. The style SELECTS the recipe (by cluster->mechanism), not a hardcoded id list; it also carries the tau() JIT address (grounds ExecTarget::Jit). Mirrors the mul::escalation precedent (thin planner module over zero-dep contract substrate). Planner already deps contract; no new edge; contract stays zero-dep. First slice: runs style-selected recipes over ThoughtCtx (the substrate the planner did not consume before); LogicalPlan passes through unchanged. Deferred: i4-32D style-vector argmax decode, Outcome->Candidate/KanbanMove, tau->JIT compile, membrane commit. 3 new tests + 192 planner lib green; rustfmt-clean. https://claude.ai/code/session_01R9AWgFa65uPnLyS2my2d2R
1 parent a2047e5 commit 6eb0fa6

2 files changed

Lines changed: 203 additions & 0 deletions

File tree

crates/lance-graph-planner/src/strategy/mod.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ pub mod rule_optimizer;
3535
pub mod sigma_scan;
3636
pub mod sparql_parse;
3737
pub mod stream_pipeline;
38+
pub mod style_strategy;
3839
pub mod truth_propagation;
3940
pub mod workflow_dag;
4041

@@ -74,5 +75,8 @@ pub fn default_strategies() -> Vec<Box<dyn PlanStrategy>> {
7475
Box::new(extension::ExtensionPlanner),
7576
// Chat hot path (AutocompleteCache — full causal cognition engine)
7677
Box::new(chat_bundle::AutocompleteCacheStrategy),
78+
// Cognitive substrate: thinking-style → cluster → recipe selection (the
79+
// planning substrate; style also carries the τ JIT address). D-MBX-A6-P3a.
80+
Box::new(style_strategy::StyleStrategy),
7781
]
7882
}
Lines changed: 199 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,199 @@
1+
//! Strategy #18: StyleStrategy — the thinking-style planning substrate.
2+
//!
3+
//! Thinking styles are THE planning substrate (not recipes in isolation): a style
4+
//! carries both the *selection* (which way to think) and, via its τ (tau) address,
5+
//! the *executable* JIT path. This strategy wires the shipped contract substrate
6+
//! into the planner's default registry — mirroring the `mul::escalation` precedent
7+
//! (a thin planner module that `pub use`s the zero-dep contract + one adapter).
8+
//!
9+
//! ## The pipeline this attaches to (all shipped in `lance-graph-contract`)
10+
//!
11+
//! ```text
12+
//! ThinkingStyle ─cluster()─▶ StyleCluster ─▶ Mechanism ─▶ the recipes that fire
13+
//! │ tau() (recipe_kernels::Tactic)
14+
//! ▼
15+
//! τ macro address ──▶ JitTemplate ──▶ KernelHandle (ExecTarget::Jit; jit.rs)
16+
//! ```
17+
//!
18+
//! The **style selects the recipe** (by cluster→mechanism affinity), runs the
19+
//! selected `Tactic` kernels over a `ThoughtCtx` built from the `PlanContext`
20+
//! markers, and surfaces the style's τ address (the JIT entry point) on the result.
21+
//! `ExecTarget::Jit` = the τ→template→Cranelift→`KernelHandle` path; `ExecTarget::Elixir`
22+
//! = the interpreted `recipe_kernels` layer this slice exercises.
23+
//!
24+
//! ## Slice scope (D-MBX-A6-P3a)
25+
//!
26+
//! First cut: resolve the style → select + run its cluster's recipe kernels over a
27+
//! `ThoughtCtx` (the recipe substrate the planner did not consume before). The plan
28+
//! passes through unchanged — this wires the cognitive substrate, not plan semantics.
29+
//! Deferred: `Outcome`→`Candidate`/`KanbanMove` adapter, the JIT compile call, and the
30+
//! membrane commit path (see the D-MBX-COMPLETION-MAP / board).
31+
32+
use lance_graph_contract::recipe_kernels::{kernel, ThoughtCtx};
33+
use lance_graph_contract::recipes::{Mechanism, Recipe, RECIPES};
34+
use lance_graph_contract::thinking::{StyleCluster, ThinkingStyle};
35+
36+
use crate::ir::{Arena, LogicalOp};
37+
use crate::traits::{PlanCapability, PlanContext, PlanInput, PlanStrategy};
38+
use crate::PlanError;
39+
40+
/// Default thinking style when the `PlanContext` carries no explicit style.
41+
///
42+
/// `Analytical` (the Analytical cluster) is the conservative convergent default —
43+
/// it selects truth-aware/parallel recipes, never the divergent/randomizing ones.
44+
pub const DEFAULT_STYLE: ThinkingStyle = ThinkingStyle::Analytical;
45+
46+
/// The thinking-style planning substrate strategy.
47+
#[derive(Debug, Default)]
48+
pub struct StyleStrategy;
49+
50+
impl StyleStrategy {
51+
/// Map a behavioural cluster to the recipe [`Mechanism`] it preferentially fires.
52+
///
53+
/// This is the **style → recipe selector** (the load-bearing link): a style does
54+
/// not name recipe ids, it names a *way of thinking*, and the cluster's mechanism
55+
/// chooses which of the 34 recipes are in-character.
56+
fn cluster_mechanism(cluster: StyleCluster) -> Mechanism {
57+
match cluster {
58+
// Analytical / Direct = convergent, truth-aware (deduce, revise, critique).
59+
StyleCluster::Analytical | StyleCluster::Direct => Mechanism::TruthAwareInference,
60+
// Creative / Exploratory = divergent (randomize, reframe, analogize).
61+
StyleCluster::Creative | StyleCluster::Exploratory => Mechanism::StructuralDivergence,
62+
// Empathic = parallel-independent perspective taking.
63+
StyleCluster::Empathic => Mechanism::ParallelIndependence,
64+
// Meta = the cross-cutting infrastructure tactics (meta-cognition, framing).
65+
StyleCluster::Meta => Mechanism::Infrastructure,
66+
}
67+
}
68+
69+
/// The recipes a given style fires: those whose mechanism matches the style's
70+
/// cluster mechanism. (`by_mechanism` is a contract lookup; inlined here to keep
71+
/// the borrow `'static`.)
72+
fn recipes_for(style: ThinkingStyle) -> impl Iterator<Item = &'static Recipe> {
73+
let want = Self::cluster_mechanism(style.cluster());
74+
RECIPES.iter().filter(move |r| r.mechanism == want)
75+
}
76+
77+
/// Build the recipe substrate's [`ThoughtCtx`] from the available `PlanContext`
78+
/// markers. Today the planner exposes `free_will_modifier` (→ temperature) and the
79+
/// query feature richness (→ candidate seeds); richer markers (real sd / free-energy
80+
/// from the live cognitive cycle) wire in later.
81+
fn thought_ctx_from(ctx: &PlanContext) -> ThoughtCtx {
82+
// free_will_modifier ∈ ~[0,1+] biases explore↔exploit temperature.
83+
let mut tc = ThoughtCtx::new(vec![ctx.features.estimated_complexity as f32]);
84+
tc.temperature = (ctx.free_will_modifier as f32).clamp(0.0, 1.0);
85+
tc
86+
}
87+
88+
/// Resolve the active thinking style from the context, or the default.
89+
///
90+
/// `PlanContext.thinking_style` is an `Option<Vec<f64>>` style *vector* (the i4-32D
91+
/// style projection in f64 form); a present vector means "a style was selected
92+
/// upstream". First slice: presence → keep `DEFAULT_STYLE` (decoding the vector to a
93+
/// specific `ThinkingStyle` is the i4-32D argmax wiring, deferred). Absence → default.
94+
fn resolve_style(ctx: &PlanContext) -> ThinkingStyle {
95+
// DECISION (follow-up): decode ctx.thinking_style (i4-32D vec) → argmax ThinkingStyle.
96+
// First slice keeps DEFAULT_STYLE regardless; the selector machinery below is what
97+
// this slice proves out, not the vector decode.
98+
let _ = ctx.thinking_style.as_ref();
99+
DEFAULT_STYLE
100+
}
101+
}
102+
103+
impl PlanStrategy for StyleStrategy {
104+
fn name(&self) -> &str {
105+
"style_strategy"
106+
}
107+
108+
fn capability(&self) -> PlanCapability {
109+
// Physicalize-phase: selects the cognitive substrate, does not gate the scan.
110+
PlanCapability::Extension
111+
}
112+
113+
fn affinity(&self, _ctx: &PlanContext) -> f32 {
114+
// Low, always-eligible: the style substrate is a default cross-cutting layer,
115+
// not a dialect that wins/loses on keyword match.
116+
0.3
117+
}
118+
119+
fn plan(
120+
&self,
121+
input: PlanInput,
122+
_arena: &mut Arena<LogicalOp>,
123+
) -> Result<PlanInput, PlanError> {
124+
let style = Self::resolve_style(&input.context);
125+
let mut tc = Self::thought_ctx_from(&input.context);
126+
127+
// Run the style-selected recipe kernels over the ThoughtCtx (the substrate the
128+
// planner did not consume before). `run` = gate + apply + clamp (contract-tested).
129+
for recipe in Self::recipes_for(style) {
130+
if let Some(k) = kernel(recipe.id) {
131+
let _outcome = k.run(&mut tc);
132+
}
133+
}
134+
135+
// First slice: the recipe pass refines the ThoughtCtx (confidence/temperature);
136+
// the LogicalPlan passes through unchanged. Outcome→Candidate/KanbanMove +
137+
// the τ→JIT compile + membrane commit are deferred (D-MBX-A6-P3 follow-ups).
138+
Ok(input)
139+
}
140+
}
141+
142+
#[cfg(test)]
143+
mod tests {
144+
use super::*;
145+
146+
#[test]
147+
fn analytical_default_selects_truth_aware_recipes() {
148+
// DEFAULT_STYLE (Analytical) → TruthAwareInference mechanism.
149+
assert_eq!(DEFAULT_STYLE.cluster(), StyleCluster::Analytical);
150+
assert_eq!(
151+
StyleStrategy::cluster_mechanism(DEFAULT_STYLE.cluster()),
152+
Mechanism::TruthAwareInference
153+
);
154+
// It selects a non-empty, in-character recipe set, and every selected recipe
155+
// genuinely carries that mechanism.
156+
let fired: Vec<_> = StyleStrategy::recipes_for(DEFAULT_STYLE).collect();
157+
assert!(!fired.is_empty(), "Analytical must fire some recipes");
158+
assert!(fired
159+
.iter()
160+
.all(|r| r.mechanism == Mechanism::TruthAwareInference));
161+
}
162+
163+
#[test]
164+
fn each_cluster_maps_to_a_mechanism_and_fires_recipes() {
165+
for style in ThinkingStyle::ALL {
166+
// tau() is the JIT address — every style has one (grounds ExecTarget::Jit).
167+
let _tau = style.tau();
168+
let mech = StyleStrategy::cluster_mechanism(style.cluster());
169+
// The selector is total: every cluster's mechanism exists in the catalogue.
170+
assert!(
171+
RECIPES.iter().any(|r| r.mechanism == mech),
172+
"cluster {:?} mechanism {:?} must match >=1 recipe",
173+
style.cluster(),
174+
mech
175+
);
176+
}
177+
}
178+
179+
#[test]
180+
fn plan_runs_style_recipes_without_error() {
181+
let s = StyleStrategy;
182+
let mut arena = Arena::new();
183+
let input = PlanInput {
184+
plan: None,
185+
context: PlanContext {
186+
query: "MATCH (n:Person) RETURN n".into(),
187+
features: crate::traits::QueryFeatures::default(),
188+
free_will_modifier: 0.7,
189+
thinking_style: None,
190+
nars_hint: None,
191+
},
192+
};
193+
let out = s.plan(input, &mut arena);
194+
assert!(
195+
out.is_ok(),
196+
"style strategy plan() must pass through cleanly"
197+
);
198+
}
199+
}

0 commit comments

Comments
 (0)