Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .claude/board/AGENT_LOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
## 2026-07-17 — TD-PLANNER-STYLE-DEFAULT-DRIFT-1 PAID — fill the 5 planner default_modulation families from canonical (main thread, no subagents)

- **Task:** pay the debt D-TSC-1b measured — the planner's 5 `_ => FieldModulation::default()` style families diverge from the canonical `UNIFIED_STYLES`≡`StyleParams` tables on resonance/fan_out/exploration.
- **Deliverable:** `style.rs` `default_modulation` — 5 explicit arms (`Convergent`/`Systematic`/`Divergent`/`Diffuse`/`Peripheral`) with canonical values on the 3 measured dims, `..FieldModulation::default()` for the 4 planner-specific dims (no canonical source → not fabricated); `_` fallback REMOVED → exhaustive match (future variant = compile error, no more silent default). +2 regression tests.
- **Verified:** `cargo test -p lance-graph-planner` 218 lib (+2) green; `cargo clippy -p lance-graph-planner --all-targets -D warnings` exit 0; fmt clean. Reverted an accidental Metacognitive `speed_bias` edit before commit (kept existing arms byte-identical).
- **Board:** TECH_DEBT TD-PLANNER-STYLE-DEFAULT-DRIFT-1 marked PAID; EPIPHANIES `E-PLANNER-STYLE-DRIFT-PAID-1`. Behavior change (correctness): the 5 families now search with real per-family params. Branch `claude/review-claude-board-files-nhqgx1`; PR pending.

## 2026-07-17 — D-TSC-1b style-table agreement probe + D-TRI-2 NO-GO scoping — measurement, shipped

- **Task:** the next unblocked slice after #711. Scoped D-TRI-2 (12-step ↔ 12-family agreement) → **NO-GO, mint-blocked** (both vectors are codebook views of the unbuilt minted register — one Opus scoping agent, file:line evidence). Pivoted to the genuinely-unblocked cousin: measure the 3 shipped 12-family param tables' agreement with the `jc::reliability` battery (closes the loop on #709/#710).
Expand Down
18 changes: 18 additions & 0 deletions .claude/board/EPIPHANIES.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,21 @@
## 2026-07-17 — E-THERMOMETER-ENCODE-OVERFLOW-1 — `thermometer_encode(1.0)` overflowed u8 (`1u8 << 8`) — `to_fingerprint()` panicked in debug for any full-scale modulation dim; pre-existing, surfaced by Codex on #713

**Status:** SHIPPED (`crates/lance-graph-planner/src/thinking/style.rs`, +2 tests, 220 planner lib green, clippy exit 0). A pre-existing latent bug the D-TSC-1b follow-through (E-PLANNER-STYLE-DRIFT-PAID-1) made more prevalent and Codex (P2, #713) caught.

- **The bug:** `thermometer_encode` computed `(1u8 << levels).wrapping_sub(1)` where `levels = (value.clamp(0,1) * 8.0) as u8 ∈ 0..=8`. At `value == 1.0`, `levels == 8` and `1u8 << 8` is a **shift overflow** — PANIC in debug, mis-encode to `0` in release (shift masked to `8 & 7 = 0`). The intended top-bin code is `2^8 - 1 = 0xFF`.
- **Already live before this PR:** `to_fingerprint()` (the real `api.rs::plan_with_style` path) feeds several shipped families' full-scale dims through it — `Exploratory` `exploration = 1.0` / `fan_out = 20` (→ 20/20 = 1.0), `Focused` `depth_bias = 1.0`, `Exploratory` `breadth_bias = 1.0`. So `plan_with_style(Exploratory)` already panicked in debug; nobody had exercised the path. Filling `Peripheral fan_out = 20` (E-PLANNER-STYLE-DRIFT-PAID-1) added another trigger on the line Codex reviewed.
- **The fix:** compute the thermometer code in `u16` (`((1u16 << levels) - 1) as u8`) so `levels == 8 → 0xFF`. Regression tests: `thermometer_encode_saturates_at_full_scale` (0.0→0x00, 1.0→0xFF, over-range clamps, monotone) + `to_fingerprint_never_panics_for_any_family` (the user-visible path across all 12). The second test would have caught the pre-existing bug.
- Lesson: a full-scale (`1.0`) input is a boundary the `× N as uN then shift` idiom silently overflows; the safe form computes `2^k − 1` one width up. Cross-ref E-PLANNER-STYLE-DRIFT-PAID-1 (same PR).

## 2026-07-17 — E-PLANNER-STYLE-DRIFT-PAID-1 — the D-TSC-1b measured drift is retired: the planner's 5 silent-default style families now carry canonical values, and the match is exhaustive (TD-PLANNER-STYLE-DEFAULT-DRIFT-1 PAID)

**Status:** SHIPPED (`crates/lance-graph-planner/src/thinking/style.rs`, +2 regression tests, 218 planner lib green, clippy `-D warnings` exit 0). The measurement→fix loop closed same-day: D-TSC-1b (E-D-TRI-2-MINT-BLOCKED-1) MEASURED the drift; this pays it.

- **The fix:** `default_modulation`'s `_ => FieldModulation::default()` fallback is replaced by 5 explicit arms (`Convergent`/`Systematic`/`Divergent`/`Diffuse`/`Peripheral`) carrying the canonical `UNIFIED_STYLES`≡`StyleParams` resonance_threshold/fan_out/exploration; the match is now exhaustive over all 12 `StyleFamily` variants.
- **Measured-only discipline:** only the 3 dims the probe measured are filled from canonical; the 4 planner-specific dims (`depth/breadth/noise/speed_bias`, no canonical source) stay at `..FieldModulation::default()` — nothing fabricated. This is the honest boundary: the measurement licenses fixing exactly what it measured, no more.
- **The exhaustiveness upgrade is the durable win:** the `_ => default` arm is precisely what let the drift go silent (a family the author forgot silently got the flat default). Removing it makes a future unhandled `StyleFamily` variant a COMPILE error — the anti-pattern class (`I-LEGACY-API-FEATURE-GATED`-adjacent: same function, silently-wrong output for un-enumerated inputs) can't recur here.
- **Behavior change (correctness):** the 5 families now search with real per-family params instead of the flat default; re-running D-TSC-1b would show Mode A ≈ Mode B. Numeric half of census question O5 answered. Cross-ref `E-D-TRI-2-MINT-BLOCKED-1` (the measurement), `TD-PLANNER-STYLE-DEFAULT-DRIFT-1` (PAID), `D-TSC-1` (the M9 dedup this completes for the planner table).

## 2026-07-17 — E-D-TRI-2-MINT-BLOCKED-1 — D-TRI-2's two 12-vectors are BOTH codebook views of the unbuilt minted register; the probe is not runnable pre-mint. The genuinely-unblocked cousin is measuring the THREE shipped 12-family param tables' agreement (D-TSC-1b, RUN)

**Status:** SHIPPED (D-TSC-1b probe `crates/jc/examples/style_table_agreement.rs`, exit-0, transcription spot-checked against all three sources). The D-TRI-2 NO-GO is scoping-only (no code); the unblocked cousin was BUILT + RUN with a measured verdict (below). (`D-TSC-1b` — the D-TSC-1 dedup MEASURED; distinct from the pre-existing `D-TSC-2` = OGAR cognition-domain mint.) Uses the merged `jc::reliability` battery (#709/#710).
Expand Down
4 changes: 4 additions & 0 deletions .claude/board/TECH_DEBT.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# Technical Debt Log — Open + Paid (double-entry, append-only)

## TD-PLANNER-STYLE-DEFAULT-DRIFT-1 — PAID (2026-07-17, same day as surfaced)

**Paid** in `crates/lance-graph-planner/src/thinking/style.rs` `default_modulation`. The 5 formerly-`_ => default()` families (`Convergent`/`Systematic`/`Divergent`/`Diffuse`/`Peripheral`) now carry the canonical `UNIFIED_STYLES`≡`StyleParams` values on the three MEASURED dims: `Convergent` 0.75/4/0.10, `Systematic` 0.70/5/0.10, `Divergent` 0.40/10/0.70, `Diffuse` 0.45/8/0.40, `Peripheral` 0.20/20/0.60. The **4 planner-specific dims** (`depth_bias`/`breadth_bias`/`noise_tolerance`/`speed_bias`) have NO canonical source, so they stay at `FieldModulation::default()` via `..default()` struct-update — filled only what was measured, fabricated nothing. The `_` fallback is REMOVED → the match is exhaustive over all 12 `StyleFamily` variants (a future variant is now a compile error, not a silent default). Regression test `formerly_defaulted_families_match_canonical_on_measured_dims` pins the 5 families' 3 dims + asserts the 4 unmeasured dims stayed at default; `every_family_has_a_nondefault_or_explicit_modulation` pins exhaustiveness. 218 planner lib tests green (+2), clippy `-D warnings` exit 0. Behavior change: the 5 families now search with their real per-family resonance/fan_out/exploration instead of the flat default — a correctness improvement (the drift D-TSC-1b measured is retired; re-running the probe would now show Mode A ≈ Mode B). Numeric half of census question O5 answered. See `E-PLANNER-STYLE-DRIFT-PAID-1`.

## TD-PLANNER-STYLE-DEFAULT-DRIFT-1 — the planner's 5 uncalibrated `default_modulation` families are the sole measured divergence from the canonical style tables (2026-07-17, surfaced by D-TSC-1b)

**Surfaced by the D-TSC-1b agreement probe** (`crates/jc/examples/style_table_agreement.rs`). Measured with `jc::reliability`: the driver `UNIFIED_STYLES` and thinking-engine `StyleParams` tables are byte-identical (A≡B, Pearson/Spearman = 1.0), and the planner `FieldModulation::default_modulation`'s **7 explicitly-written families agree with them at IDENTITY level** (ICC(2,1) 0.90–0.98). The ONLY divergence: the 5 families the planner leaves to `_ => FieldModulation::default()` — `convergent`, `systematic`, `divergent`, `diffuse`, `peripheral` — all get the flat `(resonance 0.7, fan_out 6, exploration 0.3)` default, but those families are extreme in the canonical tables (e.g. `peripheral` = 0.20/20/0.60, `divergent` = 0.40/10/0.70). Folding them in drops all-12 ICC(2,1) to 0.71 (resonance) / 0.71 (fan_out) = AMBIGUOUS. **PAYOFF:** add the 5 missing explicit arms to `default_modulation` (`lance-graph-planner/src/thinking/style.rs:74-142`) sourced from `StyleFamily`'s canonical params — or have the planner read `StyleFamily::params()` / `UNIFIED_STYLES` directly instead of a local partial table (the true M9-dedup close). Retires the numeric half of open census question O5 (`persona-vs-rung-ladder.md`). Not paid here — D-TSC-1b is measurement-only (probe, no behavior change); this is the follow-through it revealed.
Expand Down
160 changes: 156 additions & 4 deletions crates/lance-graph-planner/src/thinking/style.rs
Original file line number Diff line number Diff line change
Expand Up @@ -136,8 +136,48 @@ impl PlannerStyleExt for StyleFamily {
speed_bias: 0.3,
exploration: 0.5,
},
// Defaults for remaining styles
_ => FieldModulation::default(),
// The 5 families below were silently `FieldModulation::default()`
// (flat 0.7/6/0.3) until D-TSC-1b (TD-PLANNER-STYLE-DEFAULT-DRIFT-1)
// measured that this diverged from the canonical `UNIFIED_STYLES` /
// thinking-engine `StyleParams` tables (which the driver + engine
// agree on perfectly, and the planner's own 7 explicit arms match at
// IDENTITY level). The three MEASURED dims (resonance_threshold,
// fan_out, exploration) are now sourced from that canonical table;
// the 4 planner-specific dims (depth/breadth/noise/speed bias) have
// NO canonical source, so they stay at `FieldModulation::default()`
// via struct-update rather than being fabricated. The match is now
// exhaustive (no `_` fallback) so a future `StyleFamily` variant is a
// compile error here instead of another silent default.
Self::Convergent => FieldModulation {
resonance_threshold: 0.75,
fan_out: 4,
exploration: 0.10,
..FieldModulation::default()
},
Self::Systematic => FieldModulation {
resonance_threshold: 0.70,
fan_out: 5,
exploration: 0.10,
..FieldModulation::default()
},
Self::Divergent => FieldModulation {
resonance_threshold: 0.40,
fan_out: 10,
exploration: 0.70,
..FieldModulation::default()
},
Self::Diffuse => FieldModulation {
resonance_threshold: 0.45,
fan_out: 8,
exploration: 0.40,
..FieldModulation::default()
},
Self::Peripheral => FieldModulation {
resonance_threshold: 0.20,
fan_out: 20,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Avoid assigning Peripheral a fan_out that overflows fingerprints

When callers use the public plan_with_style(..., StyleFamily::Peripheral) path, api.rs converts this modulation through to_fingerprint(), which normalizes fan_out as fan_out as f64 / 20.0; with this new value that passes exactly 1.0 into thermometer_encode, where (1u8 << 8) panics in debug builds and encodes incorrectly in release builds. Peripheral previously fell back to fan_out = 6, so this change adds a new user-visible panic/wrong strategy-vector case unless the encoder is fixed or this value is kept below the saturation boundary.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confirmed and fixed at the root cause in b3db4df. You're right that Peripheral fan_out = 20to_fingerprint20/20.0 = 1.0thermometer_encode(1.0) overflows. But the bug is in the encoder, not the value: thermometer_encode did (1u8 << levels).wrapping_sub(1) with levels = (value*8) as u8 ∈ 0..=8, and 1u8 << 8 overflows u8 (debug panic / release mis-encode to 0). It was already latentExploratory (exploration = 1.0, fan_out = 20) and Focused (depth_bias = 1.0) feed 1.0 through the same path, so plan_with_style(Exploratory) already panicked in debug; my Peripheral change just added another trigger on the reviewed line.

Rather than keep Peripheral below the boundary (which would reintroduce the D-TSC-1b drift this PR fixes), I fixed the encoder: ((1u16 << levels) - 1) as u8, so the top bin is a correct 0xFF (8 set bits) instead of overflowing. Added to_fingerprint_never_panics_for_any_family (exercises the real plan_with_style path across all 12 families — would have caught the pre-existing bug) + thermometer_encode_saturates_at_full_scale. 220 planner lib tests green.


Generated by Claude Code

exploration: 0.60,
..FieldModulation::default()
},
}
}
}
Expand Down Expand Up @@ -248,6 +288,118 @@ pub fn select_from_mul(mul: &MulAssessment) -> StyleFamily {
}

fn thermometer_encode(value: f64) -> u8 {
let clamped = (value.clamp(0.0, 1.0) * 8.0) as u8;
(1u8 << clamped).wrapping_sub(1)
// Number of "on" bits, 0..=8. `value == 1.0` yields 8 (the top bin).
let levels = (value.clamp(0.0, 1.0) * 8.0) as u8;
// Thermometer code for `levels` set bits = `2^levels - 1` (0x00..=0xFF).
// At `levels == 8` this is 0xFF, but `1u8 << 8` overflows u8 — it PANICS in
// debug (shift overflow) and mis-encodes in release (shift masked to 0 → 0).
// Any modulation dim of exactly 1.0 hits this: e.g. Exploratory's
// `exploration = 1.0` / `fan_out = 20` (→ 20/20 = 1.0) and Focused's
// `depth_bias = 1.0`. Compute in u16 so the top bin is a correct 0xFF.
((1u16 << levels) - 1) as u8
}

#[cfg(test)]
mod tests {
use super::*;
use lance_graph_contract::style_family::StyleFamily;

/// Regression for TD-PLANNER-STYLE-DEFAULT-DRIFT-1 (surfaced by the D-TSC-1b
/// agreement probe): the 5 families that were silently
/// `FieldModulation::default()` (flat 0.7/6/0.3) now carry the canonical
/// `UNIFIED_STYLES` / thinking-engine `StyleParams` values on the three
/// MEASURED dims (resonance_threshold, fan_out, exploration). The 4
/// planner-specific dims have no canonical source and stay at default.
#[test]
fn formerly_defaulted_families_match_canonical_on_measured_dims() {
// (family, resonance_threshold, fan_out, exploration) — from
// engine_bridge.rs UNIFIED_STYLES == cognitive_stack.rs StyleParams
// (D-TSC-1b proved these two are byte-identical).
let cases: [(StyleFamily, f64, usize, f64); 5] = [
(StyleFamily::Convergent, 0.75, 4, 0.10),
(StyleFamily::Systematic, 0.70, 5, 0.10),
(StyleFamily::Divergent, 0.40, 10, 0.70),
(StyleFamily::Diffuse, 0.45, 8, 0.40),
(StyleFamily::Peripheral, 0.20, 20, 0.60),
];
let default = FieldModulation::default();
for (fam, res, fan, expl) in cases {
let m = fam.default_modulation();
assert!(
(m.resonance_threshold - res).abs() < 1e-9,
"{fam:?} resonance_threshold = {} != canonical {res}",
m.resonance_threshold
);
assert_eq!(m.fan_out, fan, "{fam:?} fan_out");
assert!(
(m.exploration - expl).abs() < 1e-9,
"{fam:?} exploration = {} != canonical {expl}",
m.exploration
);
// No longer the flat default triple (the drift D-TSC-1b measured).
assert!(
(m.resonance_threshold, m.fan_out, m.exploration)
!= (
default.resonance_threshold,
default.fan_out,
default.exploration
),
"{fam:?} still equals the flat default triple"
);
// The 4 unmeasured planner-specific dims correctly stay at default.
assert_eq!(m.depth_bias, default.depth_bias, "{fam:?} depth_bias");
assert_eq!(m.breadth_bias, default.breadth_bias, "{fam:?} breadth_bias");
assert_eq!(
m.noise_tolerance, default.noise_tolerance,
"{fam:?} noise_tolerance"
);
assert_eq!(m.speed_bias, default.speed_bias, "{fam:?} speed_bias");
}
}

/// The match in `default_modulation` is now exhaustive over all 12
/// `StyleFamily` variants — none falls through to a silent default.
#[test]
fn every_family_has_a_nondefault_or_explicit_modulation() {
// Just calling it for all 12 must not panic and every family resolves.
for fam in StyleFamily::ALL {
let _ = fam.default_modulation();
}
}

/// `thermometer_encode` must not overflow at the top bin (`value == 1.0`),
/// where the intended code is a full `0xFF` (8 set bits). The old
/// `1u8 << 8` panicked in debug / mis-encoded to 0 in release. (Codex P2
/// on #713 — surfaced by Peripheral's canonical `fan_out = 20`, but already
/// latent for Exploratory `exploration = 1.0` / Focused `depth_bias = 1.0`.)
#[test]
fn thermometer_encode_saturates_at_full_scale() {
assert_eq!(thermometer_encode(0.0), 0x00);
assert_eq!(
thermometer_encode(1.0),
0xFF,
"top bin must be all bits set"
);
assert_eq!(thermometer_encode(2.0), 0xFF, "over-range clamps to 0xFF");
// Monotone non-decreasing across the range (thermometer property).
let mut prev = 0u8;
for i in 0..=16 {
let code = thermometer_encode(i as f64 / 16.0);
assert!(
code >= prev,
"thermometer must be monotone: {code} < {prev}"
);
prev = code;
}
}

/// `to_fingerprint()` must not panic for ANY family's default modulation —
/// the real user-visible path (`api.rs::plan_with_style`) that the encoder
/// overflow would have crashed in debug builds.
#[test]
fn to_fingerprint_never_panics_for_any_family() {
for fam in StyleFamily::ALL {
let _fp = fam.default_modulation().to_fingerprint();
}
}
}
Loading