import { readFile } from "node:fs/promises"; import { join } from "node:path"; import { MODES, type Mode, type UltraConfig } from "../types.js"; import { effectiveExcludedPaths } from "../security/excluded-defaults.js"; export const PROJECT_OVERLAY_FILE = join(".ultra", "project.json"); const ALLOWED_FIELDS = ["excludePaths", "weeklyCreditBudget", "dailyCreditBudget", "acceptanceCommand", "forbidTopologies", "scopePaths"] as const; export interface ProjectOverlay { excludePaths: string[]; weeklyCreditBudget?: number; dailyCreditBudget?: number; acceptanceCommand?: string; forbidTopologies: Mode[]; scopePaths: string[]; } function strings(value: unknown, label: string, max: number): string[] { if (!Array.isArray(value) || value.length > max || value.some((item) => typeof item !== "string" || !item.trim() || item.length > 512)) throw new Error(`UltraPi project overlay ${label} must be an array of non-empty paths`); return value as string[]; } function positive(value: unknown, label: string): number { if (typeof value !== "number" || !Number.isFinite(value) || value <= 0 || value > 1_000_000_000) throw new Error(`UltraPi project overlay ${label} is out of bounds`); return value; } export function parseProjectOverlay(value: unknown): ProjectOverlay { if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("UltraPi project overlay must be an object"); const raw = value as Record; for (const key of Object.keys(raw)) { if (!(ALLOWED_FIELDS as readonly string[]).includes(key)) throw new Error(`UltraPi project overlay field ${key} is not supported; an overlay may only narrow known limits`); } const forbid = raw.forbidTopologies === undefined ? [] : strings(raw.forbidTopologies, "forbidTopologies", 8); for (const mode of forbid) if (!(MODES as readonly string[]).includes(mode)) throw new Error(`UltraPi project overlay forbidTopologies contains an unknown topology ${mode}`); if (raw.acceptanceCommand !== undefined && (typeof raw.acceptanceCommand !== "string" || !raw.acceptanceCommand.trim() || raw.acceptanceCommand.length > 2_000)) throw new Error("UltraPi project overlay acceptanceCommand must be a bounded command"); return { excludePaths: raw.excludePaths === undefined ? [] : strings(raw.excludePaths, "excludePaths", 64), ...(raw.weeklyCreditBudget === undefined ? {} : { weeklyCreditBudget: positive(raw.weeklyCreditBudget, "weeklyCreditBudget") }), ...(raw.dailyCreditBudget === undefined ? {} : { dailyCreditBudget: positive(raw.dailyCreditBudget, "dailyCreditBudget") }), ...(raw.acceptanceCommand === undefined ? {} : { acceptanceCommand: raw.acceptanceCommand as string }), forbidTopologies: forbid as Mode[], scopePaths: raw.scopePaths === undefined ? [] : strings(raw.scopePaths, "scopePaths", 32), }; } export function assertNotWeaker(base: UltraConfig, effective: UltraConfig): void { const budget = (config: UltraConfig, key: "weeklyCreditBudget" | "dailyCreditBudget") => config.budgets[key] ?? Number.POSITIVE_INFINITY; for (const key of ["weeklyCreditBudget", "dailyCreditBudget"] as const) { if (budget(effective, key) > budget(base, key)) throw new Error(`UltraPi project overlay cannot raise budgets.${key}`); } const baseForbidden = new Set(base.forbiddenTopologies ?? []); for (const mode of baseForbidden) { if (!(effective.forbiddenTopologies ?? []).includes(mode)) throw new Error(`UltraPi project overlay cannot re-enable topology ${mode}`); } const baseExcluded = new Set(base.projectExcludedPaths ?? []); for (const path of baseExcluded) { if (!(effective.projectExcludedPaths ?? []).includes(path)) throw new Error(`UltraPi project overlay cannot remove exclusion ${path}`); } } export function applyProjectOverlay(base: UltraConfig, overlay: ProjectOverlay): UltraConfig { const budgets = { ...base.budgets }; for (const key of ["weeklyCreditBudget", "dailyCreditBudget"] as const) { const proposed = overlay[key]; const current = budgets[key]; if (proposed !== undefined) budgets[key] = current === undefined ? proposed : Math.min(current, proposed); } const effective: UltraConfig = { ...base, budgets, projectExcludedPaths: effectiveExcludedPaths(base.projectExcludedPaths, overlay.excludePaths), forbiddenTopologies: [...new Set([...(base.forbiddenTopologies ?? []), ...overlay.forbidTopologies])], ...(overlay.acceptanceCommand ? { pinnedAcceptanceCommand: overlay.acceptanceCommand } : {}), ...(overlay.scopePaths.length ? { projectScopePaths: overlay.scopePaths } : {}), }; assertNotWeaker(base, effective); return effective; } export async function loadProjectOverlay(cwd: string, base: UltraConfig): Promise<{ config: UltraConfig; applied: boolean }> { let text: string; try { text = await readFile(join(cwd, PROJECT_OVERLAY_FILE), "utf8"); } catch (error) { if ((error as NodeJS.ErrnoException).code === "ENOENT" || (error as NodeJS.ErrnoException).code === "ENOTDIR") return { config: base, applied: false }; throw error; } return { config: applyProjectOverlay(base, parseProjectOverlay(JSON.parse(text) as unknown)), applied: true }; }