feat: implement remaining PRD features (10 beads)

Complete the PRD feature set with shell gating pipeline, cache
improvements, layout enhancements, and diagnostics:

- Shell: exec_gated with allowlist/denylist, circuit breaker, env merge
- Shell: parallel prefetch via std::thread::scope for cold renders
- Cache: TTL jitter (FNV-1a), config hash namespace, garbage collection
- Cache: diagnostic tracking (hit/miss, age) for dump-state
- Layout: gradual drop strategy (one-by-one vs tiered)
- Layout: render budget timer with graceful priority-based degradation
- Layout: breakpoint hysteresis to prevent preset toggling
- Width: detection source tracking for diagnostics
- CLI: --no-cache, --no-shell, --clear-cache, env var overrides
- Diagnostics: enhanced --dump-state with section timing and cache stats

Closes: bd-3oy, bd-62g, bd-khk, bd-3q1, bd-ywx, bd-3l2,
        bd-2vm, bd-1if, bd-2qr, bd-30o, bd-3ax, bd-3uw, bd-4b1

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Taylor Eernisse
2026-02-06 15:59:15 -05:00
parent 73401beb47
commit 4c9139ec42
15 changed files with 1198 additions and 138 deletions

View File

@@ -128,6 +128,7 @@ pub enum ColorMode {
pub struct Breakpoints {
pub narrow: u16,
pub medium: u16,
pub hysteresis: u16,
}
impl Default for Breakpoints {
@@ -135,6 +136,7 @@ impl Default for Breakpoints {
Self {
narrow: 60,
medium: 100,
hysteresis: 2,
}
}
}
@@ -660,7 +662,11 @@ pub fn deep_merge(base: &mut Value, patch: &Value) {
// ── Config loading ──────────────────────────────────────────────────────
/// Load config: embedded defaults deep-merged with user overrides.
pub fn load_config(explicit_path: Option<&str>) -> Result<(Config, Vec<String>), crate::Error> {
/// Returns (Config, warnings, config_hash) where config_hash is 8-char hex MD5
/// of the merged JSON (for cache namespace invalidation on config change).
pub fn load_config(
explicit_path: Option<&str>,
) -> Result<(Config, Vec<String>, String), crate::Error> {
let mut base: Value = serde_json::from_str(DEFAULTS_JSON)?;
let user_path = explicit_path
@@ -687,12 +693,24 @@ pub fn load_config(explicit_path: Option<&str>) -> Result<(Config, Vec<String>),
));
}
// Compute config hash from merged JSON before deserialize consumes it
let config_hash = compute_config_hash(&base);
let mut warnings = Vec::new();
let config: Config = serde_ignored::deserialize(base, |path| {
warnings.push(format!("unknown config key: {path}"));
})?;
Ok((config, warnings))
Ok((config, warnings, config_hash))
}
/// MD5 of the merged JSON value, truncated to 8 hex chars.
/// Deterministic: serde_json produces stable output for the same Value.
fn compute_config_hash(merged: &Value) -> String {
use md5::{Digest, Md5};
let json_bytes = serde_json::to_string(merged).unwrap_or_default();
let hash = Md5::digest(json_bytes.as_bytes());
format!("{:x}", hash)[..8].to_string()
}
fn xdg_config_path() -> Option<std::path::PathBuf> {