Drop-in façades and migration
A drop-in reproduces its incumbent's API and behaviour, never its mechanism; burgee migrate moves only the imports the compatibility oracle grades level, and refuses the rest file by file.
What it is
Each family package publishes drop-ins: subpaths that export an incumbent's API over the
family's own implementation. burgee/commander for commander, roundel/chalk for chalk,
flagstaff/ora for ora, linegauge for string-width, and 26 specifiers in all. Moving to
one is an import change and nothing else:
- import { Command } from 'commander';
+ import { Command } from 'burgee/commander';burgee migrate makes that change across a project, for every drop-in that is
ready.
Why it exists
A replacement nobody can adopt without a rewrite is not a replacement. A drop-in lets a program move one import at a time and keep working at every step — and once it has moved, the program is on the family's mechanisms: one output policy, one exit registry, one width function, and the agent surfaces that come with burgee's engine.
The rule: API and behaviour, never mechanism
A drop-in reproduces what the incumbent does — its exports, its return values, its exit
codes, its output — because that is what the incumbent's own test suite grades. It does not
reproduce how the incumbent does it when a layer of the family owns that job.
burgee/src/yargs/cliui.ts is the model the rule cites: a yargs façade that takes width,
strip and wrap from linegauge and keeps only yargs' own layout. The rule is stated, and
enforced, by
inline-implementation-lock.test.ts:
a façade that measures text by hand, reads NO_COLOR itself or spawns a process with
node:child_process fails it, unless the file is a row in the lock's KNOWN table with the
reason — which is how burgee/commander keeps commander's own colour rule, because commander's
suite grades exactly that rule.
The façades are also never wrappers: burgee/commander is written over burgee's engine, and
no published package depends on commander. The real incumbents are development dependencies of
this repository, installed only to be compared against — by the compatibility oracle and the
benchmarks.
Three more locks hold the surface:
- a drop-in exports every name its incumbent exports, types included, except the gaps listed by
name
(
drop-in-type-surface-lock.test.ts); require()of a drop-in returns the same kind of value asrequire()of its incumbent (drop-in-require-shape-lock.test.ts);- the table
burgee migraterewrites by is exactly the set of pairs the oracle grades, and the table on Migrate is exactly the one in the code (migrate-drop-ins-lock.test.ts).
How a drop-in reaches 100%
The compatibility oracle runs the incumbent's
own test suite against the drop-in. The rate is passed / reference, where the reference is
the number of cases the same suite registers against the real incumbent (the control), so a
file that fails to load cannot shrink the denominator. Three thresholds use that number:
| Threshold | Condition | What it gates |
|---|---|---|
| level | passes at least as many cases as the incumbent passes against itself | whether burgee migrate rewrites the import (D-137) |
| full | passes every case of the reference | a yes in a capability matrix that cites the grade |
| 1.0 | a rate of 100% | the front-end's own 1.0 release; below it, never described as compatible |
A drop-in that is graded but not level — cosmiconfig, dotenv, clack and term-img today — is
reported by migrate as partial, with its grade, and left alone. The rates are on
Compatibility.
burgee migrate
It rewrites import specifiers and nothing else: import … from, bare import, export … from, import() and require() with a literal string. Everything else in a file is left
byte for byte. Its safety rules:
- Only level drop-ins. The mapping is derived from the graded table, not typed by hand.
- Only the graded major. An incumbent installed, or declared, on another major is reported
under
offMajorwith both versions, and its files are skipped. - A file is rewritten whole or not at all. One refused specifier leaves the whole file as it
was. The refusals are
deep-import(commander/lib/command.js),non-literal-specifier(require(name)),unknown-export(a value import of a name the drop-in does not export) andrequire-of-default(arequire()whose two sides would return different kinds of value). A type-only import of a missing name iskepton the incumbent, not refused. - No dirty tree. Without
--force, uncommitted changes stop the run before anything is written, so the rewrite is one reviewable diff.--dry-runwrites nothing, so it skips that check. - It never edits
package.json. It prints the one command that adds the family packages and removes the incumbents that are no longer imported, for the package manager the lockfile names. - It exits 1 when anything was refused, with the whole report still printed, so an agent can run it unattended and branch on the result.
Each rule is a case in
migrate.test.ts:
refuses a deep import and leaves the file exactly as it was, leaves a file entirely unchanged when one specifier in it is refused, leaves an incumbent on another major alone, and says which versions, refuses a dirty tree, and names the fix, writes nothing under --dry-run, and still reports what it would do and reports RUNTIME when anything was refused.
Run it
A project with two level drop-ins and one deep import:
{ "name": "hello", "dependencies": { "chalk": "^6.0.0", "commander": "^15.0.0" } }import chalk from 'chalk';
import { Command } from 'commander';
const program = new Command('hello');
program.action(() => console.log(chalk.green('hi')));
program.parse();import { Help } from 'commander/lib/help.js';
export const help = new Help();files: 1
imports: 2
mapped: [{"from":"commander","to":"burgee/commander","imports":1,"files":1},{"from":"chalk","to":"roundel/chalk","imports":1,"files":1}]
refused: [{"file":"legacy.mjs","line":1,"specifier":"commander/lib/help.js","reason":"deep-import"}]
kept: []
detected: {"declared":["commander","chalk"],"imported":["chalk","commander"]}
dependencies: {"before":["commander","chalk"],"removable":["chalk"],"after":1,"add":["burgee","roundel"]}
graded: [{"host":"chalk","reference":58,"passed":58,"rate":1,"control":58},{"host":"commander","reference":1360,"passed":1360,"rate":1,"control":1360}]
partial: []
offMajor: []
next: npm install burgee roundel && npm uninstall chalk
dryRun: true
changed: false
exitCode: 1cli.mjs would move; legacy.mjs is refused and stays as it was, so commander stays
installed and only chalk is removable.
Where the rules live
- Migrate — the full table of what moves, and every case it leaves alone.
- Spec: burgee-migrate; decision D-137 (what "level" means).
- Each package's Drop-ins page and its Coming from guides, such as Coming from chalk.
- burgee vs commander and burgee vs yargs.
Configuration and precedence
seniority's one fixed order — flag, env, config file, package.json field, default — resolved by a pure function that records where every value came from, so --explain can say what won and what it beat.
How we prove claims
Every number the family publishes comes from a file a command produced: incumbent suites graded against a control, baselines that only ratchet, weight bands, capability matrices whose every cell cites its evidence, and the locks that fail when prose outruns the measurement.