diff --git a/.claude/board/AGENT_LOG.md b/.claude/board/AGENT_LOG.md index 31b23b00..4b96443c 100644 --- a/.claude/board/AGENT_LOG.md +++ b/.claude/board/AGENT_LOG.md @@ -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). diff --git a/.claude/board/EPIPHANIES.md b/.claude/board/EPIPHANIES.md index 382cbb5d..4b505c48 100644 --- a/.claude/board/EPIPHANIES.md +++ b/.claude/board/EPIPHANIES.md @@ -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). diff --git a/.claude/board/TECH_DEBT.md b/.claude/board/TECH_DEBT.md index 5de0c584..d615fd15 100644 --- a/.claude/board/TECH_DEBT.md +++ b/.claude/board/TECH_DEBT.md @@ -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. diff --git a/crates/lance-graph-planner/src/thinking/style.rs b/crates/lance-graph-planner/src/thinking/style.rs index e15cb4dc..fa1477d8 100644 --- a/crates/lance-graph-planner/src/thinking/style.rs +++ b/crates/lance-graph-planner/src/thinking/style.rs @@ -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, + exploration: 0.60, + ..FieldModulation::default() + }, } } } @@ -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(); + } + } }