git mv packages/business -> packages/organization (@mosaic/organization). The old package stays as one-line re-export shims of each moved module, with src/business.mjs re-exporting src/organization-config.mjs and src/cli.mjs forwarding to the moved executable. BusinessError is OrganizationConfigError (one constructor, name kept BusinessError); OrganizationError stays distinct. The bus adapter gains organization-context.mjs (busOrganization), with business.mjs (busBusiness) as the same function. Import consumers, manifests, the lockfile, scripts/mosaic, stack-copy.sh COPY_PATHS (both roots), seat sandbox RUNNER_FILES and the copy closure walker (organization-imports.mjs, both roots required) move to the new path. v1 serialization, v1 writers and the stage A fixtures are unchanged; the stage A contract tests stay where they were and pass as before.
95 lines
4.5 KiB
JavaScript
95 lines
4.5 KiB
JavaScript
// 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;
|
|
}
|