Files
jason.woltje a8d9eecf65 organization: move packages/business to packages/organization (rename r2 stage B1)
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.
2026-11-09 11:47:48 -06:00

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;
}