2026-09-03 - (/loop) R9 was already closed, and twenty-one new differential cases

tests             1974  ->  1974   (cases are data, not test functions)
differential cases 109  ->   130

R9, which turned out to be done

The plan item read: move learn's D180 refusal into check() so hand-edited TOML cannot bypass it. Reading it, the refusal did look like it lived only on the learn path - KnowledgeFile::merge returns faults and the command declines.

It does not. FunctionKnowledge::provenance_faults carries the same rule -

records behaviour but does not say how it is known

Asked rather than imitated. A behaviour claim with no known_by was appended to libc.toml and the suite was run:

"handEditedBypass: records behaviour but does not say how it is known"
test result: FAILED. 21 passed; 1 failed

Restored, green again. R9 is closed and the plan was wrong about it - which is worth a line rather than a code change, because the next reader of that plan would otherwise spend the afternoon re-deriving it.

Twenty-one differential cases, chosen for the bugs they catch

strncasecmp had one case and strcasecmp one. strstr had four, none with a repeated prefix. memset had never been given a value above 127.

Case folding, where the trap is that [ and { differ by exactly 0x20. So do @ and the backtick. An implementation that ORs 0x20 instead of asking whether the byte is a letter calls them equal; glibc answers negative:

REF|out|strcasecmp/bracket-and-brace|sign|negative
REF|out|strcasecmp/high-byte-is-greater|sign|positive

The second is the other half: beyond ASCII the comparison is on unsigned char, so \x80 is greater than a - backwards for anything folding through a signed char.

strstr with repeated prefixes, where a search that resumes from where it stopped rather than one past where it started misses the match:

strstr/repeated-prefix   aab  in  aaab       -> offset 1
strstr/false-start       abcabd in abcabcabd -> offset 3

Plus the bounds: needle longer than subject, a match at the final possible offset, empty subject against a non-empty needle, and both empty.

memset conversion. The value is converted to unsigned char, so 255 writes ff and 321 writes 41 - a truncation, not a clamp, and not a range error:

REF|out|memset/high-value|buffer|ffffff646566004040404040
REF|out|memset/value-above-a-byte|buffer|414163646566004040404040

Also six more strncasecmp bounds: a zero bound compares nothing however different the strings, differing exactly at the bound against one past it, and a bound running past the NUL.

All 130 agree, and that is checked rather than assumed

Orbistoun matches the reference on every one. Which on its own is the weak claim - so one of the new records was corrupted and the suite run again:

"strcasecmp/bracket-and-brace: sign is negative, expected zero"

Named, not merely counted. So the new cases are genuinely compared, and orbistoun's strcasecmp really does ask whether a byte is a letter rather than reaching for 0x20.

The two standing gates did their job unchanged: every_recorded_case_can_be_rebuilt would have failed had any new shape been unbuildable, and agreed + diverges == rebuilt ties the comparison count to the case list, so a checker that quietly stopped comparing would fail.

The test count did not move, because these are data rows and not #[test] functions. Saying so beats a number that implies otherwise.

State

cargo test --workspace green - 117 suites, 1974 tests, 0 failures. clippy --tests clean, fmt clean, identity scan clean.

Nothing committed. The day holds worklogs 292-332 and D466-D484.

Next: the shared stub table (D484) - build_thunks over a set of modules, the (module, symbol index) to slot mapping carried to relocation, then the relocation pass.