preferences: wire 6 more latent pref fields through validation

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.<field> 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 <noreply@anthropic.com>
This commit is contained in:
Mikael Hugo 2026-04-19 08:25:59 +02:00
parent 63e87e8e86
commit 56130c2e39

View file

@ -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<string, unknown>;
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"]);