Verdict on this Mac: PASS. Linux: PENDING. The workflow step is scripts/check-abi.sh. It has not run on Ubuntu for this tree.
tools/abi/generate.py reads clang -ast-dump=json and clang -dM -E for the allowlisted slice of this OTP's erl_nif.h. A C probe prints sizeof and offsetof. That probe is compiled by the check script and is not an input in priv/native/link-inputs.txt.
native/mojoler/abi/otp_nif.mojo is the committed output. python3 tools/abi/generate.py --check diffs it. A requested triple other than the host is refused. File-scope alias does not parse in Mojo 1.1.0, so version numbers and dirty flags are functions.
experiments/stage0/poc_direct.mojo is the pre-extraction poc.mojo. The live file imports the generated module and still implements Elixir.Mojoler.POC.add/2. native/twice.mojo uses the same module and does not declare ErlNifFunc or ErlNifEntry.
scripts/check-imports.sh fails the build of experiments/abi/negative/bad_import.mojo because enif_make_lonq is not on the allowlist. E20 records the successful check, including dirty flags ok.
Descriptor lifetime
nif_init callocs the entry and the function table and returns the entry pointer. OTP stores it. Unload is not a safe place to free it: the loader can still read the struct while the library is mapped. Both blocks live until the OS reaps the process.
A second load_nif of the same module returns {:error, {:reload, _}} (E14). Growth is therefore two blocks per successfully loaded module per OS process, not per call. Stage 3 frees a different allocation, the Mojoler.Own private box, in unload, with free. That box is not the entry.