// Decision 145: pure print-only conversion. System values are supplied by the // existing read-only config loader; this module never reads credentials or writes. import { checkModels, checkLimits } from "./launch.mjs"; import { requireObject, rejectUnknownKeys } from "./util.mjs"; import { refuse } from "./errors.mjs"; import { validateBusinessDocument } from "./organization-config.mjs"; export function migrateLaunch(document, mapping, file, { rolesDir, system = null }) { const out = structuredClone(document); if (!out.launch?.max) { validateBusinessDocument(out, file, { rolesDir }); return out; } requireObject(mapping, "migration mapping"); rejectUnknownKeys(mapping, ["models", "families", "selections", "instances"], "migration mapping"); const models = checkModels(mapping.models); requireObject(mapping.families, "migration families"); requireObject(out.launch.max, "old launch.max"); for (const field of ["instances", "selections"]) { if (Object.hasOwn(mapping, field)) requireObject(mapping[field], `migration ${field}`); } for (const key of Object.keys(mapping.instances ?? {})) { if (!Object.hasOwn(out.roles, key)) refuse(`unknown migration instance ${key}`); } const limits = { models: Object.create(null) }; const used = new Set(); for (const [family, count] of Object.entries(out.launch.max)) { const key = mapping.families[family]; if (typeof key !== "string" || !Object.hasOwn(models, key)) { refuse(`unknown migration mapping for family ${family}`); } const entry = models[key]; const tuple = JSON.stringify([entry.provider, entry.model]); if (used.has(tuple)) refuse("ambiguous migration: two family limits map to the same provider/model"); used.add(tuple); limits.models[entry.provider] ??= Object.create(null); limits.models[entry.provider][entry.model] = count; } const checkedLimits = checkLimits(limits); const previous = new Map(); for (const [instance, role] of Object.entries(out.roles)) { const inherited = role.vars?.model === undefined; let old = role.vars?.model; let provider = null; if (inherited) { old = system?.["execution.model"]; provider = system?.["execution.provider"]; if (typeof old !== "string" || !old || typeof provider !== "string" || !provider) { refuse(`inherited model for ${instance} requires valid system config`); } } previous.set(instance, old); let selected = mapping.instances?.[instance] ?? mapping.selections?.[old]; if (selected === undefined) { const matches = Object.entries(models).filter(([, entry]) => { return entry.model === old && (!inherited || entry.provider === provider); }); if (matches.length !== 1) refuse(`unknown or ambiguous model mapping for ${instance}`); selected = matches[0][0]; } if (typeof selected !== "string" || !Object.hasOwn(models, selected)) { refuse(`unknown or ambiguous model mapping for ${instance}`); } const entry = models[selected]; if (entry.model !== old || (inherited && entry.provider !== provider)) { refuse(`migration mapping changes ${instance}'s ${inherited ? "inherited provider/model" : "external model"}`); } role.vars = { ...role.vars, model: selected }; } for (const instance of [out.launch.by, ...out.launch.instances]) { const selected = models[out.roles[instance].vars.model]; const old = previous.get(instance); const families = Object.keys(out.launch.max).filter(f => old.toLowerCase().includes(f.toLowerCase())); if (families.length !== 1) refuse(`unknown or ambiguous family mapping for launchable ${instance}`); const constrained = models[mapping.families[families[0]]]; if (selected.provider !== constrained.provider || selected.model !== constrained.model) { refuse(`ambiguous migration mapping for launchable ${instance}`); } } for (const family of Object.keys(out.launch.max)) { const entry = models[mapping.families[family]]; for (const [instance, old] of previous) { if (!old.toLowerCase().includes(family.toLowerCase())) continue; const actual = models[out.roles[instance].vars.model]; if (old !== entry.model || actual.provider !== entry.provider) { refuse(`ambiguous migration: family ${family} spans multiple provider/model pairs`); } } } const { max, ...launch } = out.launch; out.launch = { ...launch, models, limits: checkedLimits }; validateBusinessDocument(out, file, { rolesDir }); return out; }