From 4ddb9ca8a5149cb45615cc70cb7489e9a325c8c0 Mon Sep 17 00:00:00 2001 From: Jeremy Date: Sat, 4 Apr 2026 15:01:15 -0500 Subject: [PATCH] fix(gsd): add codebase validation in validatePreferences so preferences are not silently dropped The codebase preferences block was accepted as a known key but never validated or assigned in validatePreferences(), causing all user-configured codebase defaults to be silently discarded. Adds validation for exclude_patterns (string[]), max_files (positive int), and collapse_threshold (positive int) with unknown-key warnings and 4 new tests. --- .../extensions/gsd/preferences-validation.ts | 45 ++++++++++++++ .../extensions/gsd/tests/preferences.test.ts | 62 +++++++++++++++++++ 2 files changed, 107 insertions(+) diff --git a/src/resources/extensions/gsd/preferences-validation.ts b/src/resources/extensions/gsd/preferences-validation.ts index 57a715521..21afe285d 100644 --- a/src/resources/extensions/gsd/preferences-validation.ts +++ b/src/resources/extensions/gsd/preferences-validation.ts @@ -857,5 +857,50 @@ export function validatePreferences(preferences: GSDPreferences): { } } + // ─── Codebase Map ────────────────────────────────────────────────── + if (preferences.codebase !== undefined) { + if (typeof preferences.codebase === "object" && preferences.codebase !== null) { + const cb = preferences.codebase as Record; + const validCb: import("./preferences-types.js").CodebaseMapPreferences = {}; + + if (cb.exclude_patterns !== undefined) { + if (Array.isArray(cb.exclude_patterns) && cb.exclude_patterns.every((p: unknown) => typeof p === "string")) { + validCb.exclude_patterns = cb.exclude_patterns as string[]; + } else { + errors.push("codebase.exclude_patterns must be an array of strings"); + } + } + if (cb.max_files !== undefined) { + const mf = typeof cb.max_files === "number" ? cb.max_files : Number(cb.max_files); + if (Number.isFinite(mf) && mf >= 1) { + validCb.max_files = Math.floor(mf); + } else { + errors.push("codebase.max_files must be a positive integer"); + } + } + if (cb.collapse_threshold !== undefined) { + const ct = typeof cb.collapse_threshold === "number" ? cb.collapse_threshold : Number(cb.collapse_threshold); + if (Number.isFinite(ct) && ct >= 1) { + validCb.collapse_threshold = Math.floor(ct); + } else { + errors.push("codebase.collapse_threshold must be a positive integer"); + } + } + + const knownCbKeys = new Set(["exclude_patterns", "max_files", "collapse_threshold"]); + for (const key of Object.keys(cb)) { + if (!knownCbKeys.has(key)) { + warnings.push(`unknown codebase key "${key}" — ignored`); + } + } + + if (Object.keys(validCb).length > 0) { + validated.codebase = validCb; + } + } else { + errors.push("codebase must be an object"); + } + } + return { preferences: validated, errors, warnings }; } diff --git a/src/resources/extensions/gsd/tests/preferences.test.ts b/src/resources/extensions/gsd/tests/preferences.test.ts index ff150440d..7c263ef26 100644 --- a/src/resources/extensions/gsd/tests/preferences.test.ts +++ b/src/resources/extensions/gsd/tests/preferences.test.ts @@ -461,3 +461,65 @@ test("experimental.rtk defaults to off in new project preferences", () => { assert.notEqual(prefs, null); assert.equal(prefs!.experimental?.rtk, undefined); }); + +// ── Codebase Map Preferences ───────────────────────────────────────────────── + +test("codebase preferences validate and pass through correctly", () => { + const result = validatePreferences({ + codebase: { + exclude_patterns: ["docs/", "fixtures/"], + max_files: 1000, + collapse_threshold: 15, + }, + }); + assert.equal(result.errors.length, 0); + assert.deepEqual(result.preferences.codebase?.exclude_patterns, ["docs/", "fixtures/"]); + assert.equal(result.preferences.codebase?.max_files, 1000); + assert.equal(result.preferences.codebase?.collapse_threshold, 15); +}); + +test("codebase preferences reject invalid types", () => { + const result = validatePreferences({ + codebase: { + exclude_patterns: "not-an-array" as any, + max_files: -5, + collapse_threshold: 0, + }, + }); + assert.ok(result.errors.some(e => e.includes("exclude_patterns must be an array"))); + assert.ok(result.errors.some(e => e.includes("max_files must be a positive"))); + assert.ok(result.errors.some(e => e.includes("collapse_threshold must be a positive"))); +}); + +test("codebase preferences warn on unknown keys", () => { + const result = validatePreferences({ + codebase: { + exclude_patterns: ["docs/"], + unknown_key: true, + } as any, + }); + assert.equal(result.errors.length, 0); + assert.ok(result.warnings.some(w => w.includes('unknown codebase key "unknown_key"'))); + assert.deepEqual(result.preferences.codebase?.exclude_patterns, ["docs/"]); +}); + +test("codebase preferences parse from markdown frontmatter", () => { + const content = [ + "---", + "version: 1", + "codebase:", + " exclude_patterns:", + ' - "docs/"', + ' - ".cache/"', + " max_files: 800", + " collapse_threshold: 10", + "---", + ].join("\n"); + const prefs = parsePreferencesMarkdown(content); + assert.notEqual(prefs, null); + const result = validatePreferences(prefs!); + assert.equal(result.errors.length, 0); + assert.deepEqual(result.preferences.codebase?.exclude_patterns, ["docs/", ".cache/"]); + assert.equal(result.preferences.codebase?.max_files, 800); + assert.equal(result.preferences.codebase?.collapse_threshold, 10); +});