```ts
/** Where each ruled library should land. */
export function chooseUpgrades(request: Record<string, unknown>): Record<string, unknown> {
  const installed = request.installed as Record<string, string>;
  const offers = request.offers as Record<string, string[]>;
  const rules = request.rules as any[];

  if (typeof installed !== 'object' || installed === null) {
    throw new Error('installed must be a mapping');
  }
  if (typeof offers !== 'object' || offers === null) {
    throw new Error('offers must be a mapping');
  }
  if (!Array.isArray(rules)) {
    throw new Error('rules must be a list');
  }

  const floors = new Map<string, string>();
  const ceilings = new Map<string, string>();

  for (const rule of rules) {
    if (typeof rule !== 'object' || rule === null) {
      throw new Error('rule must be a mapping');
    }
    const { package: packageName, min, max } = rule;
    if (typeof packageName !== 'string') {
      throw new Error('rule.package must be a string');
    }
    if (!offers.hasOwnProperty(packageName)) {
      throw new Error(`rule bounds a library the registry does not carry: ${packageName}`);
    }
    if (min > max) {
      throw new Error(`rule.min is above its max for package: ${packageName}`);
    }
    const low = floors.get(packageName);
    if (low === undefined || min < low) {
      floors.set(packageName, min);
    }
    const high = ceilings.get(packageName);
    if (high === undefined || max > high) {
      ceilings.set(packageName, max);
    }
  }

  const moves: { package: string; to: string; action: 'hold' | 'lift' | 'fetch' }[] = [];
  const snags: { package: string; why: 'none' | 'drop' }[] = [];

  for (const name of Array.from(floors.keys()).sort()) {
    const low = floors.get(name) as string;
    const high = ceilings.get(name) as string;
    const allowed = offers[name].filter(
      (release) => compareReleases(release, low) >= 0 && compareReleases(release, high) <= 0
    );
    if (allowed.length === 0) {
      snags.push({ package: name, why: 'none' });
      continue;
    }
    const here = installed[name];
    if (here !== undefined) {
      if (!offers[name].includes(here)) {
        throw new Error(`library running a release the registry no longer carries: ${name}`);
      }
      if (allowed.includes(here)) {
        moves.push({ package: name, to: here, action: 'hold' });
        continue;
      } else {
        const lift = allowed.find(release => compareReleases(release, here) > 0);
        if (lift !== undefined) {
          moves.push({ package: name, to: lift, action: 'lift' });
        } else {
          snags.push({ package: name, why: 'drop' });
        }
      }
    } else {
      const fetch = allowed[0];
      moves.push({ package: name, to: fetch, action: 'fetch' });
    }
  }

  return { moves, snags };
}

function compareReleases(a: string, b: string): number {
  const [aMajor, aMinor] = a.split('.').map(Number);
  const [bMajor, bMinor] = b.split('.').map(Number);
  if (aMajor !== bMajor) {
    return aMajor - bMajor;
  }
  return aMinor - bMinor;
}
```