No Python left

Seventeen scripts, 4,358 lines, now zero. Every one is an obscene-tool subcommand, 202 tests, and nothing in scripts/, the Makefile or CI invokes an interpreter. Four languages: C is the product, GLSL is forced by the GPU probes, sh is glue and a documented choice, Rust is everything else.

Every single port found something. Not a formatting difference - a fault that had been producing a confident wrong answer:

tool fault effect
guards symbol column had to be an identifier 8 rows never examined, incl. the blind prober
guards runner assumed check_* the blind prober's is run_bulk
counts id matched [0-9]{3}-[a-z]+/ 2 checks uncounted: 134 claimed, 136 real
counts X\((\w+)\) over the census a macro parameter counted as a symbol: 39,549 vs 39,548
counts split on L( matched inside …LIBKERNEL(: 382 libraries vs 372
doccheck PROPOSED never enforced a shipped proposal stayed listed forever
mine read_to_string on invalid UTF-8 two shadPS4 files skipped, 58 rows lost
mine eighth source undocumented ps4libdoc/known_names.txt missing entirely
mine docstring denied its own converter identifiers normalise on record; the prose said otherwise
surface ten names in two mutually-exclusive lists the generator refused its own definition and could not run
surface a note hand-added to the output a working generator would have deleted it

The surface one is the sharpest. It was never gated, so nobody knew it had stopped working; a generator whose output is committed and whose input nobody runs has already diverged, you just cannot see it yet. Everything generated is gated now.

Four data files came out of the code: surface.txt, gpu-surface.tsv, font.txt, and the corpus files that were already there. Each was moved by exporting from the Python rather than retyped, and the one column I did retype - the tri-state "on RDNA2" - I got wrong immediately, because '?' is truthy in Python and became yes on six operations. That is the argument for relocating data rather than transcribing it, made concrete twice.

Verification was always the same shape: run both, diff the output, drive the difference to zero. surface.h, corpus.h, nids.h, font.c, gpu_shaders.gen.h, the compatibility table, both corpus files, and the ps5-gap report all reproduce byte-for-byte; the decision index and the GPU census differ only in the attribution line I deliberately changed. Where that was not possible - the protocol checker - both versions were fed the same thirteen corruptions and both caught all thirteen. Agreement on clean input proves nothing.

Two dependencies were added, both for the same stated reason as sha1: serde_json, because the firmware descriptions are JSON and escapes and nesting are where a hand-rolled reader is quietly wrong. Everything else is parsed by hand, because line-oriented tables split more simply than they match.

And a recurring lesson worth naming: whitespace nobody can see is still content. A blank line between a note and its macro; a trailing newline that split a macro across two lines; a block separator indistinguishable from a blank line the prose wanted; nine spaces of source indentation that made twelve header lines parse as symbol names; and # as a comment marker in a file where # is a lit pixel. Five separate corrections, one cause.