From 56130c2e39dc4ecf86f73628149c205531799102 Mon Sep 17 00:00:00 2001 From: Mikael Hugo Date: Sun, 19 Apr 2026 08:25:59 +0200 Subject: [PATCH] preferences: wire 6 more latent pref fields through validation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Same class of bug as the service_tier fix: preference fields declared in SFPreferences type and consumed by feature code, but never copied into the validated output, so they silently become undefined when set in PREFERENCES.md. Found by diffing validated. vs the interface declarations: - forensics_dedup (boolean) — /sf forensics issue de-dup opt-in - stale_commit_threshold_minutes (number) — doctor safety-commit cadence - widget_mode ("full"|"small"|"min"|"off") — dashboard widget sizing - slice_parallel ({ enabled?, max_workers? }) — slice-level parallelism - modelOverrides (Record) — per-model capability patches - safety_harness ({ enabled?, evidence_collection?, ... }) — LLM safety Validation is kind-appropriate: primitives get type + range checks, nested objects get object-shape guards with pass-through for now. Consumer sites already treat missing fields as optional, so landing shallow validation first is safe. Co-Authored-By: Claude Sonnet 4.6 --- .../extensions/sf/preferences-validation.ts | 73 +++++++++++++++++++ 1 file changed, 73 insertions(+) diff --git a/src/resources/extensions/sf/preferences-validation.ts b/src/resources/extensions/sf/preferences-validation.ts index c7c4c253b..111eb105e 100644 --- a/src/resources/extensions/sf/preferences-validation.ts +++ b/src/resources/extensions/sf/preferences-validation.ts @@ -309,6 +309,79 @@ export function validatePreferences(preferences: SFPreferences): { } } + // ─── forensics_dedup ──────────────────────────────────────────────── + if (preferences.forensics_dedup !== undefined) { + validated.forensics_dedup = !!preferences.forensics_dedup; + } + + // ─── stale_commit_threshold_minutes ───────────────────────────────── + if (preferences.stale_commit_threshold_minutes !== undefined) { + const raw = Number(preferences.stale_commit_threshold_minutes); + if (Number.isFinite(raw) && raw >= 0) { + validated.stale_commit_threshold_minutes = Math.floor(raw); + } else { + errors.push("stale_commit_threshold_minutes must be a non-negative number (minutes; 0 = disabled)"); + } + } + + // ─── widget_mode ──────────────────────────────────────────────────── + if (preferences.widget_mode !== undefined) { + const valid = new Set(["full", "small", "min", "off"]); + if (typeof preferences.widget_mode === "string" && valid.has(preferences.widget_mode)) { + validated.widget_mode = preferences.widget_mode as SFPreferences["widget_mode"]; + } else { + errors.push("widget_mode must be one of: full, small, min, off"); + } + } + + // ─── slice_parallel ───────────────────────────────────────────────── + // Shallow validation: object-shape check + primitive field coercion. + // Deeper structural checks can come later; the goal here is to stop + // silently dropping the preference. + if (preferences.slice_parallel !== undefined) { + const sp = preferences.slice_parallel; + if (typeof sp === "object" && sp !== null && !Array.isArray(sp)) { + const v: SFPreferences["slice_parallel"] = {}; + const anySp = sp as Record; + if (anySp.enabled !== undefined) v.enabled = !!anySp.enabled; + if (anySp.max_workers !== undefined) { + const n = Number(anySp.max_workers); + if (Number.isFinite(n) && n >= 1) { + v.max_workers = Math.floor(n); + } else { + errors.push("slice_parallel.max_workers must be a positive integer"); + } + } + validated.slice_parallel = v; + } else { + errors.push("slice_parallel must be an object"); + } + } + + // ─── modelOverrides ───────────────────────────────────────────────── + // Per-model capability overrides. Deep-merged into built-in profiles at + // consumer sites — here we just confirm the shape and pass through. + if (preferences.modelOverrides !== undefined) { + const mo = preferences.modelOverrides; + if (typeof mo === "object" && mo !== null && !Array.isArray(mo)) { + validated.modelOverrides = mo as SFPreferences["modelOverrides"]; + } else { + errors.push("modelOverrides must be an object keyed by model ID"); + } + } + + // ─── safety_harness ───────────────────────────────────────────────── + // Rich nested config. Pass-through with an object-shape guard; field-level + // validation can land alongside the features that consume them. + if (preferences.safety_harness !== undefined) { + const sh = preferences.safety_harness; + if (typeof sh === "object" && sh !== null && !Array.isArray(sh)) { + validated.safety_harness = sh as SFPreferences["safety_harness"]; + } else { + errors.push("safety_harness must be an object"); + } + } + // ─── Search Provider ───────────────────────────────────────────── if (preferences.search_provider !== undefined) { const validSearchProviders = new Set(["brave", "tavily", "ollama", "combosearch", "native", "auto"]);