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"]);