From ea8894a05741524e18c769a1bcd13b246f27052a Mon Sep 17 00:00:00 2001 From: Christopher Monsanto Date: Sun, 23 Aug 2026 01:33:46 -0400 Subject: [PATCH] Check a built tree against what the server serves deploy.json5 rsyncs xy/ and xyicons/ with --delete-after now, so the built tree is authoritative and anything on the server the build does not make is about to go. That is the point, and it is also the whole risk: the two sets had drifted into holding 101 files nothing here knew about, and the only reason that was ever visible is that someone went and listed the directory. So list it as a step. Run the deploy's own rsync with -n and read the deleting lines back, sorting them into the content stamps left over from when these sets published through Manifest, the one-time backlog of names nothing asks for any more, and whatever is left, which is a hole in the build. Exit non-zero on the third. Today it prints 1650 stamped and 97 backlog for xy/, 1552 and 4 for xyicons/, and nothing unexplained. Once the deploy has run, the first two go to zero and BACKLOG can go with them; anything the check turns up after that is a regression. Co-Authored-By: Claude Opus 5 (1M context) --- tools/spritedata/coverage.ts | 82 ++++++++++++++++++++++++++++++++++++ 1 file changed, 82 insertions(+) create mode 100644 tools/spritedata/coverage.ts diff --git a/tools/spritedata/coverage.ts b/tools/spritedata/coverage.ts new file mode 100644 index 00000000..a95e31dd --- /dev/null +++ b/tools/spritedata/coverage.ts @@ -0,0 +1,82 @@ +import {spawnSync} from 'node:child_process'; + +// What a smogon deploy is about to delete off the server: +// +// node tools/deploy/index.ts run smogon.build.ts -o /tmp/xyout +// node tools/spritedata/coverage.ts /tmp/xyout [smogon:/smog2/sprites] +// +// deploy.json5 rsyncs xy/ and xyicons/ with --delete-after, so the built tree +// is authoritative and anything the server holds that the build doesn't make +// is about to go. Some of that is meant to: the content-stamped leftovers from +// when these two sets published through Manifest, and the pre-rename spellings +// below that nothing has asked for in years. Anything else is a hole in the +// build, and this finds it before rsync does. +// +// names.ts answers the same question against pokemon-showdown; this one +// answers it against what is actually being served. + +let dir = process.argv[2]; +let remote = process.argv[3] ?? 'smogon:/smog2/sprites'; +if (!dir) { + throw new Error('usage: node tools/spritedata/coverage.ts [remote]'); +} + +// A stamped name: Manifest's 8 characters of RFC4648 base32 before the +// extension. Lowercase published names can't collide with it. +let STAMPED = /-[A-Z2-7]{8}\.[a-z0-9]+$/; + +// The one-time backlog this first authoritative deploy clears. Delete these +// once it has run; anything still matching them after that is a surprise. +// +// The cap spellings, hoopa-alt and darmanitan-zen-galar predate PS's renames +// to Pikachu-Alola, Hoopa-Unbound and Darmanitan-Galar-Zen, all of which the +// build publishes; nothing in smogon.com or forum names the old ones. The +// strawberry sweet is spelled as the bare cream both upstream and in chatot's +// list. The pngs are shadowed by the gifs the build ships. The subdirectories +// are XY-era scratch that no build has ever written. +let BACKLOG = [ + /^(\.gitignore|manifest\.json)$/, + /^pikachu-(kanto|hoenn|sinnoh|unova|kalos|alola)cap\.gif$/, + /^(hoopa-alt|darmanitan-zen-galar)\.(gif|png)$/, + /^alcremie-[a-z-]+-strawberry\.gif$/, + /^(lokix|melmetal|meltan|pawmi|quaquaval|syclant)\.png$/, + /^(aback|afront|asback|asfront|back|mini|sback|sfront)\//, +]; + +let holes = 0; +for (let set of ['xy', 'xyicons']) { + let rsync = spawnSync('rsync', [ + '-n', '-a', '--delete-after', '--itemize-changes', + `${dir}/${set}/`, `${remote}/${set}`, + ], {encoding: 'utf8'}); + if (rsync.status !== 0) { + throw new Error(`rsync ${set}: ${rsync.stderr.trim() || `exit ${rsync.status}`}`); + } + + let stamped = 0; + let backlog = 0; + let unexplained = []; + for (let line of rsync.stdout.split('\n')) { + let m = /^\*deleting +(.*)$/.exec(line); + if (!m) continue; + let name = m[1]!; + if (STAMPED.test(name)) { + stamped++; + } else if (BACKLOG.some(re => re.test(name))) { + backlog++; + } else { + unexplained.push(name); + } + } + + process.stdout.write(`${set}: ${stamped} stamped, ${backlog} backlog, ${unexplained.length} unexplained\n`); + for (let name of unexplained) { + process.stdout.write(` ${name}\n`); + } + holes += unexplained.length; +} + +if (holes) { + process.stderr.write(`${holes} served names the build does not make\n`); + process.exitCode = 1; +}