# P0 decision log

Decisions made during the P0 research engagement (June 2026), in the order
they were settled. Each cites the experiment or source that decided it.
Experiment IDs (E0–E7) are defined in [`benchmarks.md`](benchmarks.md) and
[`README.md`](README.md); the harness lives in [`experiments/`](experiments/).

| ID | Decision | Decided by |
| --- | --- | --- |
| D-P0-1 | **Linux dylibs are plain glibc dynamic builds, made natively on Linux runners, shipped as a self-contained bundle: the `.so` linked with rpath `$ORIGIN` plus the stripped Swift runtime `.so` set staged beside it.** Two tools were ruled out empirically: the Static Linux SDK (musl) supports no dynamic linking at all (cannot emit a `.so`; right tool for P7's static executable only), and `--static-swift-stdlib` is not honored for shared-library products (hard link error with Foundation — static archives added without their `-L` — and a silent no-op otherwise). Bundle sizes, stripped: 9.4 MB stdlib-only · 16 MB + FoundationEssentials · 61 MB + full Foundation/ICU. Verified end to end under `oven/bun` (no Swift on host), glibc floor 2.17. | swift.org docs; E6 |
| D-P0-2 | **CI pins Swift 6.3.x (current stable).** 6.4 branched 2026-05-04 and ships ~Sept 2026; research ran locally on 6.4-dev (Xcode 27 beta) without source changes, so the upgrade path is routine. Pin via `.swift-version` (swiftly) and revisit at 6.4 GA. | toolchain survey ([`toolchain.md`](toolchain.md)) |
| D-P0-3 | **Ownership contract is copy-then-free** (JS copies the payload, then calls `ad_free` in `finally`). The `toArrayBuffer`-deallocator alternative survived but reclaimed nothing under `Bun.gc(true)` — +50 MB RSS over 10 k × 4 KB hand-offs. GC-dependent reclamation of native memory is not acceptable for a server. | E3 dealloc probe |
| D-P0-4 | **Errors travel in-band in the result header** (status ≠ 0, payload = UTF-8 message). No `ad_last_error()`: a side-channel is either a data race (static buffer) or a second ownership path (allocated buffer), and it breaks the single-return/single-free symmetry. | design review |
| D-P0-5 | **Allocator symmetry is `malloc`/`free`.** `UnsafeMutableRawPointer.allocate`/`deallocate` requires matching alignment at the deallocation site (UB otherwise); `malloc` guarantees 16-byte alignment, which the header layout exploits. | design review |
| D-P0-6 | **`JSCallback` is banned for P0–P6.** Documented crash history (Bun #17157 et al.); nothing in the phased plan needs native→JS calls — JS always pulls. | bun:ffi survey |
| D-P0-7 | **bun:ffi is the primary bridge; Node-API is the documented contingency.** bun:ffi is officially experimental, but the measured overhead (3.3 ns/call) is excellent, the kill switch contains the blast radius, and the `@_cdecl` C surface is bridge-agnostic: pivoting means adding a thin napi C shim that wraps the same `ad_*` functions plus a loader swap — zero Swift changes. Pivot triggers: a crash or leak attributable to bun:ffi in CI or production, or upstream deprecation. | E0–E3; bun docs |
| D-P0-8 | **Result header is 16 bytes**: `[u64 payloadLen LE][u32 status LE][u8 formatId][3 reserved]`, payload at offset 16. The earlier draft (`[u64][u8 status]`, payload at 9) broke 8-byte alignment for numeric payload views. formatId (0=bytes, 1=UTF-8, 2=JSON) gives each export a payload-schema escape hatch without a second ABI version. | design review; E3 |
| D-P0-9 | **Compiled binaries embed the dylib** (`import … with { type: "file" }`); `dlopen` accepts the embedded `$bunfs` path directly — no materialization logic needed. Dev and server installs load a sidecar file from the repo/dist tree instead. | E5 |
| D-P0-10 | **Release dylibs are ad-hoc signed (the arm64 linker already does it) and published with sha256 sidecars**, matching the existing binary-release convention. On macOS 27, dlopen from Bun is gated by neither quarantine nor signature flavor because Bun ships `com.apple.security.cs.disable-library-validation`; integrity therefore rests on the sha256 sidecars and the loader's path allowlist. Revisit (Developer ID + notarization) when P7 produces hardened-runtime Swift executables, which do enforce library validation. | E7 |
| D-P0-11 | **JSON crosses the boundary only on cold paths.** A JSON round-trip through the dylib costs ~7.7× the pure-JS equivalent at both 4 KB and 64 KB — double serialization dominates. Hot paths use binary payloads (raw bytes / packed numerics). | E3 |
| D-P0-12 | **Native artifacts are exempt from the snapshot determinism gate.** Swift release builds are not bit-reproducible across runs/machines today; the gate (sha256 re-build diff) stays scoped to corpus archives. Tracked as future work, not a P0 blocker. | ci design ([`ci.md`](ci.md)) |
| D-P0-13 | **The JS thread is the only caller.** bun:ffi calls execute on the JS thread; the probe keeps no mutable global state, and contract v0 requires exported functions to be pure transforms of their inputs (allocation aside). Worker-thread use (each worker dlopens the same `.so` into one process-wide Swift runtime) is explicitly deferred — revisit before P5 reader-pool work. | design review |
