import { readFile } from "node:fs/promises"; import { fileURLToPath } from "node:url"; import type { PiCompactionConfig } from "./types.js"; // Bundle-level `config/` is the single source of truth: the extension reads // `config/pi-compaction.json` from inside the installed bundle (resolved // relative to this source file). It never reads `settings.json` and never // writes config at runtime. export const CONFIG_PATH = fileURLToPath( new URL("../../config/pi-compaction.json", import.meta.url), ); export const DEFAULT_CONFIG: PiCompactionConfig = { enabled: true, softRatio: 0.72, hardRatio: 0.9, targetRatio: 0.55, deterministic: { enabled: true, maxStubTokens: 1_024, }, semantic: { enabled: true, model: "default", thinking: "low", concurrency: 1, minInputChars: 4_000, maxInputChars: 24_000, maxRetries: 2, }, native: { enabled: true, }, }; function ratio(value: unknown, fallback: number): number { return typeof value === "number" && Number.isFinite(value) && value > 0 && value < 1 ? value : fallback; } function integer(value: unknown, fallback: number, min = 0, max = Number.MAX_SAFE_INTEGER): number { return typeof value === "number" && Number.isInteger(value) && value >= min && value <= max ? value : fallback; } function object(value: unknown): Record { return value !== null && typeof value === "object" && !Array.isArray(value) ? value as Record : {}; } export function normalizeConfig(value: unknown): PiCompactionConfig { const input = object(value); const deterministic = object(input.deterministic); const semantic = object(input.semantic); const native = object(input.native); const softRatio = ratio(input.softRatio, DEFAULT_CONFIG.softRatio); const hardRatio = Math.max(softRatio + 0.01, ratio(input.hardRatio, DEFAULT_CONFIG.hardRatio)); const targetRatio = Math.min(softRatio - 0.01, ratio(input.targetRatio, DEFAULT_CONFIG.targetRatio)); const thinking = ["off", "minimal", "low", "medium", "high", "xhigh"].includes(String(semantic.thinking)) ? semantic.thinking as PiCompactionConfig["semantic"]["thinking"] : DEFAULT_CONFIG.semantic.thinking; return { enabled: typeof input.enabled === "boolean" ? input.enabled : DEFAULT_CONFIG.enabled, softRatio, hardRatio: Math.min(0.99, hardRatio), targetRatio: Math.max(0.1, targetRatio), deterministic: { enabled: typeof deterministic.enabled === "boolean" ? deterministic.enabled : DEFAULT_CONFIG.deterministic.enabled, maxStubTokens: integer(deterministic.maxStubTokens, DEFAULT_CONFIG.deterministic.maxStubTokens, 32, 4_096), }, semantic: { enabled: typeof semantic.enabled === "boolean" ? semantic.enabled : DEFAULT_CONFIG.semantic.enabled, model: typeof semantic.model === "string" && semantic.model.trim() ? semantic.model.trim() : DEFAULT_CONFIG.semantic.model, thinking, concurrency: integer(semantic.concurrency, DEFAULT_CONFIG.semantic.concurrency, 1, 4), minInputChars: integer(semantic.minInputChars, DEFAULT_CONFIG.semantic.minInputChars, 0, 1_000_000), maxInputChars: integer(semantic.maxInputChars, DEFAULT_CONFIG.semantic.maxInputChars, 1_000, 1_000_000), maxRetries: integer(semantic.maxRetries, DEFAULT_CONFIG.semantic.maxRetries, 0, 5), }, native: { enabled: typeof native.enabled === "boolean" ? native.enabled : DEFAULT_CONFIG.native.enabled, }, }; } export async function loadConfig( configPath: string = CONFIG_PATH, ): Promise { try { const parsed = JSON.parse(await readFile(configPath, "utf8")); return normalizeConfig(parsed); } catch { return structuredClone(DEFAULT_CONFIG); } }