# ADR 0235: The default native loader is invisible to bundlers

> Source: https://github.com/max-listov/stitchkit/blob/master/docs/decisions/0235-default-native-loader-is-invisible-to-bundlers.md

---

# ADR 0235 — The default native loader is invisible to bundlers

## Context

`stitchkit/server`, `stitchkit/files` and `stitchkit/process` reach the Darwin process-identity
code through the exclusive lock. The packaged loader (`darwin-native.cjs`) required both
`.node` addons by literal path, so every Bun bundle of these entrypoints, on any operating system,
gained two Darwin addon outputs: `bun build --outfile` of a Linux server app failed with
`cannot write multiple output files without an output directory`. The portable entrypoints
carried a platform-native build-graph dependency, against I6 (the core stays portable) and
I7 (optional machinery stays leaf-isolated).

The static loader was added so compiled and companion artifacts carry the addon without help,
and the `stitchkit/files/packaging` plugin (ADR 0233, 0234) was built on the same loader.

## Decision

1. **The default loader computes its addon path** from its own directory at run time
   (`require(__dirname + '/native/darwin-<arch>.node')`). A bundler does not follow it: bundles of
   `server`, `files` and `process` are one JS file with no `.node` output on every OS. Run
   unbundled from the installed package, it loads the addon exactly as before.
   (Amended by ADR 0252: Bun inlines `__dirname` as the build machine's path, so the loader locates
   itself through `module.filename` and refuses inside a bundle with stage `packaging`.)
2. **Only the packaging plugin produces a static loader.** `createNativePackaging` replaces the
   loader's contents with one that requires the addon by literal path, which the bundler then embeds
   (`delivery: 'embedded'`) or leaves as a companion (`delivery: 'companion'`). One generator,
   `nativeLoaderSource(assets, resolution)`, writes both forms from the same asset graph.
3. **The packaging leaf stays, evolving, as the single way to carry an addon in an artifact.**
   Without it the Darwin backend of an unpackaged bundle is `unavailable` where the addon is not found
   beside the original package files, never evidence that an owner died. Nothing else in the
   package delivers a static loader, so there is no second path.
4. **Lock liveness on macOS keeps the addon-based process identity.** The packaging plugin is what
   makes that identity reachable from a bundled artifact.

## Rejected alternatives

- **Read process start time and boot session without an addon** (`sysctl kern.boottime`,
  `ps -o lstart=`) on the reclaim path, leaving the addon to contained files only. `lstart` has
  one-second resolution and a locale-dependent format, every reclaim spawns two processes, and the
  result is a second, unverifiable macOS implementation of the same evidence. It also would not
  remove the packaging need: the contained-file tools in `agent-runtime` need the addon too.
- **Remove the packaging leaf and its companion/universal recipes.** Consumers delivering Darwin
  artifacts would lose the only supported way to embed an addon, and the default loader would
  still need some route into a bundle.
- **Keep the static default and document it.** Every portable bundle would still carry Darwin
  assets and Linux `--outfile` builds would stay broken.

## Consequences

- A Darwin consumer that bundled or compiled these entrypoints and relied on automatic embedding
  builds with the plugin; this is a breaking change with a migration (`docs/guide/upgrading.md`).
- Linux and other portable bundles need no change.
- Darwin behavior (embedded, companion and universal artifacts, backend unavailable in an
  unpackaged bundle) is exercised only by the Darwin consumer lane and CI jobs; the Linux gates
  prove the bundle graph (one file, no `.node`) and the loader text.
