2026-08-24 - the relational section grew, and two of it had never run

Item 5 on the backlog was "more relations". Reading the section first found something worth more than the additions: 018-relational/descriptors-distinct and close-is-not-idempotent had never executed a single instruction, on any target, host build included, since the day they were written.

Both declared OBS_CAP_FILE. Capabilities are granted in running order from nothing, and the check that grants OBS_CAP_FILE is in 040-file - twenty-two sections after this one. They did not fail. They reported

skip: a prerequisite capability was not established

which is what a platform genuinely without files reports. The message was true and it was about the wrong thing, and no amount of reading the report could tell those apart. Four loaders' worth of reports carried it. (D158)

obscene-tool caps gates it now: registry order, accumulate provides, report any requires nothing earlier can satisfy. Run against the tree with the requirement put back, it names the grant site rather than just the offence:

018-relational/descriptors-distinct wants OBS_CAP_FILE - granted later, by 040-file/open-rejects-missing

Seven new relations, and what they are aimed at

The existing ten all ask about one object. An implementation backing every event flag with a single global word passes all of them - the count counts, the bits set, the handles are distinct because handles are allocated separately from the state they name. Three of the new checks ask a second object whether it can see the first one's state, which is the only way that shape is visible, and it is the shape a subsystem takes when the API is needed before the implementation is.

Two more need a second thread, because mutual exclusion and thread identity cannot be measured from one. 030-thread/self and thread-identity-stable are both satisfied by a function returning the same constant to everyone - which makes every lock on the platform silently wrong, since they are all keyed on that value. Neither joins: scePthreadJoin blocks and 030-thread runs after this section, so a join here on a platform whose threads do not finish would take the checks that diagnose it. The child sets a flag last and the parent spins a bounded number of iterations. (D159)

The last two: two direct-memory allocations held at once must not name the same memory, and a file's position must advance by the number of bytes read said it returned, with a re-read of the same offset giving the same bytes.

Making them evidence

Four of the seven passed on the host immediately; three skipped for want of stubs, which by rule 5 means they were not evidence. So the host build grew real implementations: event-flag set/clear/poll over the table it already had, file operations forwarded to the real ones with /app0/eboot.bin redirected to /proc/self/exe, and a direct-memory allocator with a free list over a half-gigabyte arena of offsets that are never mapped.

host build before after
pass 101 112
skip 38 29

Ten checks moved off skip, including the two that had never run. All seventeen relational checks now pass against a known-good implementation. 020-memory/map went skip → fail - honest, and better: it was only skipping because the allocation ahead of it failed, and the host genuinely does not implement mapping.

The gates caught me twice on the way

obscene-tool guards found two of the new checks calling a symbol they had not tested the address of - sceKernelOpen and sceKernelCreateSema, both D058 to the letter. And the first host arena was sixteen megabytes, which made 020-memory/direct-size report partial: implausibly small against its 256 MiB threshold and left that check's pass branch unexercised on the one platform where the answer is known. Raised to half a gigabyte, which is not a plausible lie: nothing is ever mapped, so the span costs an integer, and the allocator really does hand out offsets across it and really does refuse past the end.