D185 - mkself works, and the loader that rejected it first is why

The container is built and a real loader takes it: shadPS4 accepts the magic, reads the entry table, extracts the module, resolves its imports and runs the suite to its end record - 36,257 records, tally 454/7/37/17, the same shape as the same module run as a plain ELF. The eboot chain is proven rather than asserted.

It did not work first time, and the failure is the useful part.

A rejection said more than the table it came from

The first container was accepted as far as LoadSegment, then died on UNREACHABLE(). shadPS4's SELF path does not walk entries in order; it looks for one whose blocked bit is set and reads bits 20..31 as a program header index:

if (seg.IsBlocked()) {
    auto phdr_id = seg.GetId();
    ...
}
UNREACHABLE();

Every entry had props = 0, so nothing was blocked, nothing matched, and it fell off the end.

Both fields were already in data/self-format.tsv - segment_flag/blocked and segment_flag/id, recorded from the readers on the first pass. The table was right and the builder wrote zeros into the field the table described. Having the fact written down is not the same as using it, and nothing in the pipeline could have caught that: the container was structurally valid, the sizes all computed, and it was wrong.

mkmodule's header comment says every fixup here came from a rejection, not a specification. This is the same sentence one layer out, and it arrived within an hour of the table being written.

What the rejection also settled

Cross-referencing the reader against the writer showed the two are describing one thing under two vocabularies:

shadPS4 reads OpenOrbis writes bit
IsBlocked() has_blocks 11
GetId() mask 0xFFF segment_index mask 0xFFFF 20
IsOrdered/Encrypted/Signed/Compressed order/encrypted/signed/compressed 0,1,2,3

So segment_flag in the table and entry_prop are the same field. That is recorded in the rows rather than merged, because the two masks genuinely differ and neither can be shown wrong from here.

One constant needed care: block size is stored as ilog2(bytes) - 12, so 16KiB is written as 2. Storing the byte count fits the four-bit field for small values and truncates for real ones - a container a loader would accept and then read from the wrong offset.

What this does and does not prove

It proves the container is well-formed enough for a previous-generation loader, which is what the format table describes. It says nothing yet about the current console, and the whole point of the shape is to reach that one. A build from this table remains a hypothesis until something current accepts or rejects it - and a rejection will be worth as much as this one was.

Status: decided. eboot is a real target, its CI job no longer tolerates failure, and the job checks the emitted header against the constants the table names.