feat(FN-1423): add promptOverrides to project settings

- Add promptOverrides configuration to project settings with null-as-delete semantics
- Add core prompt-key catalog with resolver primitives in packages/core/src/prompt-overrides.ts
- Export prompt key catalog, resolution functions, and settings merge helpers from @fusion/core
- Add parity tests for promptOverrides settings handling
- Add settings export/import coverage for promptOverrides
- Add settings export tests verifying promptOverrides round-trip
- Update settings reference documentation with promptOverrides section
- Add store methods for loading/exporting project settings with prompt overrides
This commit is contained in:
gsxdsm
2026-04-09 18:21:29 -07:00
parent 9b30cc323d
commit a9e8073d2a
9 changed files with 1167 additions and 0 deletions

View File

@@ -650,6 +650,46 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
// Handle null values as "delete this key from settings"
// This allows the frontend to explicitly clear a setting by sending null
// (since JSON.stringify drops undefined keys, we use null as a sentinel)
// Handle special null-as-delete semantics for promptOverrides
const incomingPromptOverrides = (projectPatch as Record<string, unknown>)["promptOverrides"];
if (incomingPromptOverrides === null) {
// promptOverrides: null → clear the entire promptOverrides object
delete (config.settings as unknown as Record<string, unknown>)["promptOverrides"];
delete (projectPatch as Record<string, unknown>)["promptOverrides"];
} else if (
incomingPromptOverrides !== undefined &&
typeof incomingPromptOverrides === "object" &&
incomingPromptOverrides !== null
) {
// promptOverrides: { key: value } → merge with existing, treating null values as delete
const incomingMap = incomingPromptOverrides as Record<string, unknown>;
const existingMap = ((config.settings as unknown as Record<string, unknown>)["promptOverrides"] as Record<string, string>) ?? {};
const mergedMap: Record<string, string> = { ...existingMap };
for (const [key, value] of Object.entries(incomingMap)) {
if (value === null) {
// null → delete this specific key
delete mergedMap[key];
} else if (typeof value === "string" && value !== "") {
// non-empty string → set this key
// Empty strings are treated as "clear" and not stored
mergedMap[key] = value;
}
// Empty strings are silently ignored (treated as "clear")
}
// If merged map is empty, remove the entire promptOverrides
if (Object.keys(mergedMap).length === 0) {
delete (config.settings as unknown as Record<string, unknown>)["promptOverrides"];
delete (projectPatch as Record<string, unknown>)["promptOverrides"];
} else {
(config.settings as unknown as Record<string, unknown>)["promptOverrides"] = mergedMap;
(projectPatch as Record<string, unknown>)["promptOverrides"] = mergedMap;
}
}
// Handle null values for other top-level keys (non-promptOverrides)
for (const key of Object.keys(projectPatch)) {
if ((projectPatch as Record<string, unknown>)[key] === null) {
delete (config.settings as unknown as Record<string, unknown>)[key];