Commit Graph

604 Commits

Author SHA1 Message Date
Claude
02ff6940cf Send the upload a manifest, and then only what it asks for
Every deploy tarred the whole published tree into `smogonctl assets upload`:
a few hundred megabytes of stamped files the home already had, and a 512-byte
header for each of the thousands of links under __meta/. A served name carries
a content hash and the tree is add-only, so a name the home has is a file that
needn't travel.

The deploy now opens the same stdin with a manifest -- every name with its
size, every link -- and reads the reply off the command's stdout: the
positions it hasn't got. Only those and the __meta/ files are then tarred
into the same stream. The format is binary and length prefixed, so a name is
whatever bytes a tar would carry for it.

The reply is read strictly from the first byte: anything else on stdout, the
command's own refusal or a shell profile printing over ssh, is relayed whole,
stdin is closed so the command sees the end of the stream, and the deploy
fails with its exit code. The progress bar counts what travels. `run --tar`
still writes a bare tar, which the upload also takes.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014b7csvcRmyygDYQun3mE6E
2026-09-12 17:29:38 +00:00
Christopher Monsanto
9bf0d1c1e7 Merge pull request #11 from smogon/claude/interactive-deploy-progress-bar-njnbln
Draw a progress bar for deploy on a terminal
2026-09-12 15:49:04 +02:00
Claude
93ce1f50f9 Draw a progress bar for deploy on a terminal
A deploy's last phase is a long silent wait: thousands of files packed
into an upload or copied into a staging tree, with nothing between the
line naming the command and the line after it finishes.

Draw a bar over it when stderr is a terminal, and nothing at all when it
isn't -- piped or under CI a redraw per file is noise in the log, and
the stdout lines the deploy already prints are the record either way.

The tar entries a pack hands out are counted by their entry callbacks,
which wait on the pack draining into whoever is reading it, so the bar
follows what the upload command has taken rather than what has been read
off disk. The staging and -o paths count files as they land.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DRBNA4JH27Eq3cgJpzCLV2
2026-09-12 12:04:48 +00:00
Marty
36b143e61b Add new sprites 2026-09-11 17:50:36 -04:00
Marty
0e1f57f423 Add missing Wonder Launcher items 2026-09-06 13:07:51 -04:00
Christopher Monsanto
9510791a4f Give xyicons the same sprites as minisprites
The two sets are one set of pictures read two ways, but only minisprites
carried the items: xyicons published the gen 6 icons and stopped there, so
a name that answered in one directory was a 404 in the other. The trim rule
the items want is the one the deprecated block had kept commented since the
unstamped sets were retired.

Both sets are now spelled in one place, since what belongs in either is
decided once.
2026-08-29 03:39:05 -04:00
Christopher Monsanto
8233dbfb89 Publish the rest of the bag for the forum
:rare candy: and 281 other items have been dead tokens on the forum since
a55f31b3 (Dec 2021) moved the bag items the smogdex has no use for out of
src/minisprites/items and into src/_uncategorized/items, where their names were
replaced by a fresh numbering and nothing has built them since. The forum
resolves a token against forumsprites' manifest, so an item that isn't in the
manifest is left as literal text.

The names are recoverable exactly: every one of the 285 files matches, byte for
byte, a file in newsrc/minisprites/items at 741c73f2^, which data/items.json
named at that same commit. That reading agrees with the tree the 2020 import
built them from, src/canonical/minisprites/items, on all 285.

They go in src/minisprites/otheritems rather than back into items, because
items is what the smogdex draws on -- the sheet bakes a css class per name --
and the dex has no use for a Bicycle. forumsprites globs both, since a forum
token is a name and nothing else.

Three of the 285 don't come: Unknown, the empty-slot placeholder, would claim
the name the pokemon has (:unknown:, exactly the collision the type labels held
back last commit), and two files are byte-identical second copies of Growth
Mulch and Item Urge, which the 2021 renumber left behind in place of Item Drop
and Oak's Parcel. Those two pictures are in git and nothing here restores them;
the three files stay in _uncategorized.

284 published names, since one envelope answers to Gram 1, 2 and 3. Nothing
else the deploy ships changes: the sheet, minisprites, and the other manifests
are byte for byte what they were.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-26 20:26:13 -04:00
Christopher Monsanto
fe0a89f445 Publish the gen 5 type labels for the forum
src/_uncategorized/{canonical,noncanonical}/ui/types/gen5 has held these since
2021 -- Stellar since 2024 -- and nothing has ever built them. The only build
that ever published type labels at all took gen 4's, into PS, and it went with
the PS build in a919602c.

They ship the way forumsprites does: stamped copies in the served tree and a
manifest beside them, no links mirror, because the reader looks a name up
rather than composing a path.

The two source directories publish into one. Which of them a label came from
says whether the games drew it in this style, which is a fact about the picture
and not about the reader: Fairy and Stellar are types the forum renders like
any other, and gen 5 simply never labelled them.

Unknown is held back. The one build that published it renamed it to `???`,
which is PS's name for the type; what the forum calls it is for the forum to
say, and it can be added under that name when there is one.

20 published names, each a stamped file, and one manifest beside them. The
sources ship verbatim -- a label is already the picture, at 32x12 and a couple
hundred bytes -- so the set has no rules of its own.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-25 18:01:06 -04:00
Christopher Monsanto
d6082eedfc Give the gen 4 CAP prevos a dp sprite
The dex serves /dex/dp/pokemon/embirch/ and the sprite lookup logs a miss for
it, which is not a dex bug: Embirch is an NFE from a gen 4 CAP process, so it
belongs on a gen 4 page. It was drawn in BW's day, like the other twelve prevos
the gen 4 CAPs were given after the fact.

That is what GEN4_CAPS is already for. dp/ has no CAP art of its own -- gen 4
never drew any of it, and nothing in a gen 5 filename says a sprite is a CAP --
so the set names the eleven by hand and lifts them out of the gen 5 stills.
Name the prevos there too, and the same rule draws them: fifteen files, the
thirteen species plus the female slots on Flarelm and Protowatt, each trimmed
the way its evolution is.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-25 16:05:29 -04:00
Christopher Monsanto
c9117ac5af Call magick by its own name, and stop gifsicle narrating
`magick convert` is ImageMagick 7 running its 6 compatibility entry point,
which since 7.1.1 prints a deprecation warning on every invocation. The build
runs it about eleven thousand times, so a deploy is eleven thousand lines of
it, and the one warning worth reading -- gifsicle's, when a rule hands it more
colors than a global colormap holds -- is buried in them. Drop `convert` and
give gifsicle -w.

Neither is a free rename: v7 semantics differ from the compatibility mode's,
and gifsicle's rule identity is its command line, so the -w rebuilds every gif
in the tree. `refactor --record` before and `refactor` after says 21847 files,
0 changed, so nothing published moves and no upload follows this.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-23 20:50:36 -04:00
Christopher Monsanto
71a062cbb0 Shrink the oversized auto avatars into the box instead of cropping them
96x96 is the games' own sprite size and most of the pool is drawn inside it,
but 40 of the 702 slots are not: the animation's own canvas is wider or
taller, up to Lugia's 153x97, and -extent trimmed those to the box. That
does not frame a picture, it cuts one -- Thundurus-Therian lost 16px off
each side, Venomoth both wingtips, sliced flat at the frame edge.

The claim that the set the forum used to carry did the same holds for 39 of
the 40, which were clipped there too, but not for Venomoth: its animation
sat in 71x71 with margin on both sides and publishes at 96x74 today. Samurott
and Lugia were clipped in 2019 and are clipped harder now.

So -resize "96x96>" ahead of the extent. It only ever shrinks, so the 660
files already inside the box come out byte for byte what they were, and the
40 that don't arrive whole instead of trimmed. Resampling a 16-color picture
invents shades and the frames leave the resize carrying thousands of them,
which doubles what those 40 weigh (Thundurus-Therian 215KB -> 474KB);
-dither None -colors 64 puts them back on a palette at 298KB. 64 is above
every source palette in the pool, so it is a no-op on everything the resize
doesn't touch, bar Tomohawk, which the old pipeline quietly wrote two colors
short of its 28 and which now keeps them.

Checked over every source in the pool: 40 files change, no animation's union
leaves the box any more, and gifsicle -O3 alters no pixel of any of them.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-23 20:15:16 -04:00
Christopher Monsanto
8c3bdd0453 Merge branch 'worktree-minisprites-links' 2026-08-23 19:22:16 -04:00
Christopher Monsanto
ef3e155e44 Publish the minisprites folder again, on links instead of a hash
b698ac88 dropped the minisprites: they shipped under a whole-set content hash
with a pointer file naming it, nothing had read them or the pointer since 2023,
and they were named by PS id where the smogdex names sprites by smogon alias, so
a reader could not have found one anyway.

The mirror answers both halves. A link is what a reader that composes a path
from the sprite alone actually needs, so the hash directory and the pointer go
and nothing has to be read before asking for a file; and the names are the ones
every other icon set here publishes -- item aliases for the items, the borrowed
gen 6 icons for the pokemon -- which is what the sheet built from these same
pictures already spells in its css.

The set ships the sources verbatim rather than the webps 7dce71cb encoded them
into, so what answers to one of these names is a png, as the links that ask for
one have always spelled it. That leaves the set with no rules of its own: a
minisprite is already the picture, and xyicons/ is the trimmed reading of the
same files.

2139 published names are new, each a stamped file with a link naming it, and one
manifest beside them.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-23 19:19:22 -04:00
Christopher Monsanto
0d214ecc77 Build the forum's auto avatars, and publish them as a slot table
The forum has carried its default avatars as 702 committed gifs since its
initial commit in 2019: bytes in the wrong repo, and the last sprite
collection it hadn't moved onto a manifest. The sources are all here, so
build them.

They are gen 5 animations on a uniform 96x96 box. The box is not decorative:
XenForo scales an <img> to fill its avatar container, so a sprite published
at its own aspect would arrive stretched. It is also the games' own sprite
size, so nothing is scaled up, and the 45 animations that overflow it are
cropped to it, which is what the 2019 set did too.

The pool is an index, not a set. The forum picks a slot with
crc32(username) % count, so a name added, removed or respelled reassigns
every user's avatar -- which is why rules/avatars.ts spells the pool out
instead of globbing src/sprites/gen5, and why these publish under the PS ids
the 2019 set used (`hooh`, not `ho-oh`) rather than the smogon names every
other set here publishes. publishedName(sn, psid) spells all but two of
them; Arceus-Normal and Swanna-2 were second copies of a picture the pool
already carries, and now share its file.

What ships is that index rather than a name -> url manifest, because nothing
looks an avatar up: __meta/avatars/manifest.json is the directory listing
scandir() used to return, as an array of urls. Two things about that listing
are the index and so are reproduced rather than tidied away. It sorted
filenames with the extension attached, which orders arceus-bug.gif ahead of
arceus.gif where the bare names sort the other way, and disagrees with the
manifest order at 68 of the 702 slots. And it led with `.` and `..`, which
counted toward the divisor and named no avatar: those two rendered a broken
image for two of every 704 usernames, and now get Substitute, which the pool
already carries, so they cost nothing but the slot.

The animations themselves change, unavoidably. The 2019 files came from an
earlier generation of these sources: 649 of the 693 that still resolve have
a different frame count, and no coalesced frame survives. Same Pokemon, same
slot, same framing, re-animated.

Manifest grows url(), which write() already composed inline, so a set can
publish another projection of the same mapping.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-23 19:03:06 -04:00
Christopher Monsanto
40289407df Lay the smogdex sheet out on a grid, so the css is an index
Every one of the 2141 rules carried its own background-image,
background-repeat and a pair of coordinates, so the stylesheet shipped at
403,824 bytes -- larger than the 401,390 byte webp it pointed at -- and the
browser parsed six declarations per sprite on every dex page.

Pack the sheet as a stack of uniform grids instead, one region per grid,
each cell the size of its region's largest sprite. A sprite's place in the
sheet is then its index and nothing else, and the css is two rules of
geometry plus `.sprite-<name>{--i:N;--w:W}`. The width stays per sprite so
inline flow doesn't move; the height is the cell's, and the sprite is
centred down it, so it sits where its own box used to.

The padding a uniform cell wastes is nearly free -- lossless webp encodes
transparency for almost nothing -- and the sheet in fact came out slightly
smaller than the tight pack it replaces.

    css   403,824 -> 74,013 raw, 18,249 -> 13,646 brotli
    webp  401,390 -> 399,568

Items are their own region because their cell is 24x24 against pokemon's
40x30, which is what the dex sizes its item columns on. The dex marks which
is which by putting `sprite` on the element beside the `sprite-<name>` it
already had, and `item` too for an item.

Two sprites claiming one name used to mean the last one read won, silently;
it now throws. The layout moves to its own module so it can be tested, and
gets the sheet's first tests.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-23 17:32:00 -04:00
Christopher Monsanto
d910d20b26 Build the older-gen sprite sets
rb, rg, y, c, rs, dp and bw have been served out of /smog2/sprites since
before this repo existed: the frozen output of the tup pipeline, uploaded
once and root-owned ever since. The sources are all here, so build them.

Each set is one generation's fronts under the name the smogdex, the forum
and chatot compose from a dex alias. None of the three reads a manifest, so
these ride the LINKS mirror the way xy/ does.

A collection is named for one game and sourced from several: `-g` says which
games a picture is the copy for, and the layers are claimed in preference
order, which is how rs/ gets Deoxys-Attack out of FireRed and dp/ the Rotom
appliances out of Platinum. That flag is Gigantamax everywhere the grammar
isn't naming a gen 1-4 game, so what a rule builds is named for what it
publishes, with the game already spent on picking the collection.

The first-source-wins loop the xy set carried inline is now firstWins(),
since every layered set wants it. Its output is unchanged.

Against what is being served today, this drops the names nothing in the
repo can draw -- jynx-old and four MissingNo in y/ -- and the legacy numeric
ones, 201-a and 498-water and the rest, which the same sprites now answer to
as unown-a and arceus-water. dp/arceus-* was 96x96 art that is not gen 4's
at all; the real one was hiding under 498-*.
2026-08-23 16:32:26 -04:00
Christopher Monsanto
e3d3b1c7f0 Merge the two build files, and stamp the smogon sets
f278ed4d gave up the stamps on xy/ and xyicons/ because the smogdex composes
sprites/xy/charizard.gif itself and reads no manifest, so a stamped tree was one
it couldn't read. A mirror of the tree in links answers that without asking the
dex to change: the files land under stamped names with a manifest beside them,
and every bare name is a link naming the stamped file. The mirror rides in
__meta/links/ rather than the served tree, because the tree is add-only -- a
name in it is promised never to change -- and a link is repointed on every
upload; the upload diverts __meta/ to assets-meta/ and retargets each link at
where its asset landed on that side.

That leaves nothing separating the two build files, so they become one. Both
already imported rules/publish.ts, and the coverage rule is per deploy name over
a whole buildFile's outputs, so one file is also one deploy: everything ships
through the asset upload, and the two rsync entries go with the untracked
deploy.json5 they lived in.

Manifest keeps where each copy landed rather than the composed url, which is
what a link needs and what write() composes from anyway, and takes the tree it
publishes into rather than just that tree's url, so `sprites` is written once
and a Dest names a place under it. That is also what keeps the mirror from
repeating it: the far side files a key's metadata under a directory of its own,
and assets-meta/sprites/links/sprites/ says it twice. The url-less Manifest mode
the rsync sets used goes, having no caller left, and xyicons's byName map was
only ever a duplicate check, which Manifest.copy performs, so it collapses into
spritecopy.

3614 published names are untouched, 3207 move to a stamped name carrying the
same bytes, and 3209 are new: the mirror and the two manifests.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-23 15:28:43 -04:00
Christopher Monsanto
a648c9e622 Let a deploy publish a link as well as a file
A served name carries a content hash, so a reader that composes a fixed path
can't find one. The way out is to publish the set twice, once under the stamped
names and once under the bare ones, and a link is the one thing the queue
couldn't say: Copy dereferences and Write has bytes.

Add a Symlink op beside them. What ctx.symlink takes is not a link target but
the destination of the thing being named, since a published tree is unpacked
somewhere else and its halves can land apart; the path between the two is
computed here and is what gets written. It takes the same duplicate-dst and
absolute-dst checks the other ops do, plus one of its own -- resolved lexically
against the directory it sits in, a target has to stay inside the tree, since
the receiving side extracts as root and a link that walks out of it is the same
write to anywhere that an absolute member name is.

run() makes a real symlink in every mode, clearing the name first so a rerun
over an existing tree behaves the way copy and write already do, and pack()
emits a symlink member with no body, which is what a stream reader on the other
end sees.

refactor digests bytes, and a link has none; what it publishes is the name it
points at, so that is what it records and what a retarget shows up as.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-23 15:22:45 -04:00
Christopher Monsanto
79f0ef6497 Lend the borrowed icons to the sheet and forumsprites too
The alias table sat in smogon.build.ts, so only xyicons/ got it. Two other sets
publish the same directory and had the same holes: the smogdex sheet, where the
dex asks for a sprite- class per species it renders, and forumsprites. Neither
had a class or a file for a totem, a Gourgeist size or Mimikyu-Busted.

So the table belongs beside the names, not beside one set that uses them. It
moves to data/lib as ICON_ALIASES with an iconNames() to apply it, which the
sheet calls in place of smogonNames() and the two deploys reach through a
NameOpts on publishedNames(). Keyed by the name holding the icon now, since
that is the direction all three consumers read it in. It stays out of
smogonNames(): src/models has real art for every one of these, and in xy/ the
lending name would be claimed by the base sprite and shadow the forme's own.

Greninja-Bond goes the other way, into SPECIES_ALIASES, because it is not an
icon shortage. Battle Bond's form looks like any other Greninja, and the tree
agrees: all seven -obond files under src/, in gen5, pmd and previews/gen9, are
byte for byte their base, and no model was ever drawn. xy/ was serving the gen 5
rendition of a Greninja; it now serves the animation, and the seven duplicates
go.

The sheet gains 22 classes, forumsprites 22 files. xy/ changes one file,
greninja-bond.gif, which is now greninja.gif.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-23 02:08:40 -04:00
Christopher Monsanto
da25d70f78 Add a refactor verb, for what a change did to the published files
This repo owns xy/ and xyicons/ now, so nothing else writes there and asking
rsync what it would delete answers a question nobody has. The one worth asking
is whether an edit changed what we ship, which until now meant materializing a
deploy twice and diffing 300M of gifs.

Nothing has to be materialized. A finish already ends in an ActionQueue holding
one op per published file, and the bytes behind each are identified already: a
built artifact's CAS path spells its own digest, so only raw sources get read.
Digest those, keep the map in .build/ beside the rest of the build state, and a
comparison is a dictionary diff.

    node tools/deploy/index.ts refactor --record   # on what you are comparing to
    node tools/deploy/index.ts refactor            # after the change

prints + for a name that appeared, - for one that went, M for one whose bytes
moved, and exits non-zero if any did. Which is tup's refactor a layer out: it
checked that a Tupfile edit left the build graph alone, and this checks the
names and bytes the deploy blocks decide, where a lost alias or a rename
actually shows up.

Recording rather than failing when a file has no baseline yet, so the first run
in a checkout is useful instead of an error.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-23 02:01:02 -04:00
Christopher Monsanto
c076da9c0b Delete the coverage tool, and keep the targets out of the tree
rsync -n already answers the question the tool was wrapping, and it was the
only thing here that had to know where a deploy points, which is exactly what
deploy.json5 stays uncommitted to avoid. Say so in the README, and leave its
example a placeholder rather than a real target.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-23 01:54:30 -04:00
Christopher Monsanto
a919602cc2 Delete the PS build and the pokemon-showdown tooling
deploy.json5 has two targets, assets and smogon. ps.build.ts has none: nothing
it produces has shipped from this repo, and the ani/ rsync it was written for
lives in the README as an example. Everything it pulled along came with it.
tools/sheet is its montage tool and had no other caller.
ps-pokemon.dexnums.mjs, 1125 generated lines, existed only to hand its sheet a
dex number after data/species.json went away. tools/spritedata/{dexnums,names,
pokedex,rename}.ts all read a pokemon-showdown checkout, which is a dependency
nothing in either surviving deploy has.

The branch ps-build points at the commit before this, so it is one checkout
away if the PS side ever comes back.

tools/deploy/coverage.ts is the exception and moves rather than goes. It reads
no PS data and generates no table; it reports what a deploy would delete off
the server, which is what makes --delete-after safe to run, and it only lived
in tools/spritedata because that is where I put it.

Both remaining deploys materialize unchanged: xy/ 1651 names, xyicons/ 1556,
assets byte for byte. psid() keeps its caller in the smogdex sheet, which still
needs PS's spelling of a name even with the PS deploy gone.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-23 01:50:21 -04:00
Christopher Monsanto
7493d948e6 Name the icons that share one, instead of deriving them
The last commit answered "these 22 formes have no icon" with a generated
418-line table of every forme's parent and a loop that walked it, which is a lot
of apparatus for a list that fits on a screen and changes when a generation
does. Write the list.

Same 1556 files out of xyicons/, byte for byte. The two throws are the part the
derivation gave for free: an alias whose source icon has gone, and an alias for
a name that has since been drawn, are both mistakes worth stopping the build
for rather than quietly serving the wrong picture.

data/lib/formes.ts and tools/spritedata/formes.ts go; nothing else read either.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-23 01:45:36 -04:00
Christopher Monsanto
593f1c5c2d Drop the Alcremie sweets again
Serving alcremie-caramel-swirl-berry is not the same as anything asking for it,
and nothing does. PS's pokedex.js gives Alcremie 8 cosmetic formes, cream only,
and the dex reads no cosmetic forme at all: dex.pokemon_cosmetic_formes is
filled on import and then read by nothing under dex/, so the site never composes
one of these names. The only list of the 54 is chatot's, hardcoded in
chatot/src/helpers/constants.ts under "COSMETIC FORMES", and chatot asks
play.pokemonshowdown.com first and falls back to smogon.com only on a 404, which
for these never comes, because PS serves all 54 itself.

So the 62 files on the server are read by nothing, and pulling them in was the
sync-from-the-server-for-its-own-sake this was meant to avoid. They join the
backlog the deploy clears. The -k axis goes with them: nothing else needed a
cosmetic to publish, and the grammar should not carry an axis on the strength of
one set of files nobody reads.

xy/ is 1651 names, and the backlog coverage.ts accounts for grows from 97 to 151.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-23 01:38:26 -04:00
Christopher Monsanto
ea8894a057 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) <noreply@anthropic.com>
2026-08-23 01:33:46 -04:00
Christopher Monsanto
f92f04716a Fall back to the nearest forme with an icon
xyicons/ is built from src/minisprites/pokemon/gen6 alone, which holds no
totem, no Mimikyu-Busted and no Gourgeist or Pumpkaboo size forme. Nor does
anywhere else in the tree: home/ has the size formes as 70x112 HOME renders
against gen6's 20x26 XY icons, and no directory anywhere has a totem icon at
all. There is nothing upstream to take either, since PS ships icons as one
dexnum-indexed sheet rather than as files per name.

That is because the games drew those formes no icon of their own. PS's client
sends each to the nearest forme that has art, and ps-pokemon.sheet.mjs already
carries the list, commented out, under "alt forms with duplicate icons". Walk
the same chain rather than leave 22 names 404ing.

The chain cannot be read off a published name, since a base name carries dashes
too (kommo-o, mr-mime, porygon-z), and dropping one word blindly strands
Toxtricity-Low-Key on a Toxtricity-Low that does not exist. So
tools/spritedata/formes.ts writes data/lib/formes.ts, pointing each forme at the
longest run of its leading words that names a forme of its own, and at the base
species otherwise.

xyicons/ gains 22: the 12 totems, Mimikyu-Busted, the six Gourgeist and
Pumpkaboo sizes, Greninja-Bond, Rockruff-Dusk and Pichu-Spiky-Eared. Each is
byte for byte the sprite it fell back to. Togedemaru-Totem is among them and the
server has never had it, so that one is a gain rather than a restoration. Only
this set walks the chain; xy/ has real art for all 22. The species PS keeps but
nobody ever drew, missingno and the 15 pokestar entries, have no ancestor with
an icon and so still get nothing.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-23 01:33:32 -04:00
Christopher Monsanto
35102330aa Alias Toxtricity's Low-Key Gigantamax to the Amped one
PS carries the species and the dex asks for toxtricity-low-key-gmax, so the
build owes a picture for it, but the games drew one Gigantamax Toxtricity and
not two. PS's own icon sheet says as much: ps-pokemon.sheet.mjs records
`toxtricitylowkeygmax: 1320 + 69` among the "alt forms with duplicate icons",
the slot toxtricitygmax already sits in, and play.pokemonshowdown.com serves no
low-key gmax animation under any spelling. Nothing under src/ has a
-olow_key-g gif either, so there is nothing to import and nothing worth
drawing.

Publish the Amped sprite under both names. SPECIES_ALIASES exists for exactly
this shape and nothing else claims the name, so it costs one entry, and xy/,
xyicons/, the forumsprites manifest and the smogdex sheet pick it up together
through smogonNames(): 2118 css classes become 2119, and no existing name
moves.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-23 01:33:16 -04:00
Christopher Monsanto
3a2ecf9e12 Give the grammar a cosmetic axis, and import Alcremie's sweets
The smogon side serves 62 Alcremie animations the build has never made,
alcremie-caramel-swirl-berry and its kin, one per cream and sweet. PS models
the sweet nowhere in pokedex.js, so spriteFiles() will never name one, and
nothing under src/ has ever held one: they live on that server and nowhere
else, which is what --delete-after would have taken.

The sweet cannot ride inside -o. The two readings of a name disagree about
where the cream's words divide, `caramel-swirl` against `caramelswirl`, but
agree about the boundary in front of the sweet, `-berry` for both. Fold the
sweet into the forme and whichever reading you spell it for, the other one
comes out wrong. So -k carries it, published after the forme and before the
variant flags, and formatFilename now leads with every part publishedName
spells rather than with -o alone, for the reason it always led with -o: a
forme's sprites should sort together.

The art comes from play.pokemonshowdown.com/sprites/ani, which serves all 54
combinations the games draw, at the 54x73 the creams here already use. Of the
62 on the smogon server, 8 are <cream>-strawberry: strawberry is the default
sweet, spelled as the bare cream both upstream and in the list chatot keeps, so
nothing asks for those and they go with the other leftovers.

Vanilla cream ships as an explicit -ovanilla_cream, because PS's base Alcremie
publishes as `alcremie` while the sweets are named off the cream.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-23 01:32:25 -04:00
Christopher Monsanto
f278ed4d73 Publish the smogon sets unstamped
xy/ and xyicons/ rsync into /smog2/sprites, and the dex composes those urls
itself, sprites/xy/charizard.gif, with no lookup in between. Manifest
content-stamps every copy and writes `name.ext` -> the stamped filename beside
it, which is worth something only to a reader that consults the manifest, and
nothing on the dex side does yet. A deploy of these two sets therefore lands a
tree the dex cannot read.

Copy under the published name with no stamp, and drop the manifests, which
have no second name left to record. The duplicate check survives the loss:
ActionQueue already refuses two copies to one path, so the collision
Manifest.copy used to throw on is an invalid queue instead, and the
published-name layering of the last commit is still what keeps xy/ clear of
it. 1650 files under xy/, 1533 under xyicons/.

assets/ keeps its Manifest and its stamps, since the smogdex reads those urls
out of __meta rather than composing them.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-23 00:53:33 -04:00
Christopher Monsanto
61df5387d2 Layer the xy set on published names, not filenames
xy/ takes the first source that offers a sprite, keyed until now on the
filename. That held while gen 5 was only there for the CAPs the models never
got: a CAP has one file and one name. The Smogon Sprite Project's batch broke
it. gen 5 now carries a file per forme slot, and the slots the games gave a
single sprite share a published name, so the six Minior meteors and the two
Zygarde Power Construct slots each arrived as their own filename asking for a
name the models had already published. The manifest rejected the first of
them, `duplicate sprite name minior-meteor.gif`, and the deploy stopped there.

Key the layering on the published name, which is what has to be unique, and
lift the name off rules/publish.ts as publishedNames() so both the layering
and spritecopy read it from one place. Sprites that publish nothing (back,
shiny, asymmetrical, and the x literals) come back as no names and copy
nothing, as before.

xy/ gains 13 sprites, all of them from the new gen 5 files, and is now 1650
names against the 1637 it held before the batch. Seven are Gigantamax:
Blastoise, Cinderace, Inteleon, Rillaboom, Urshifu, Urshifu-Rapid-Strike and
Venusaur, which is exactly what models/ lacks of the 33 Gigantamax formes gen 5
draws. The other six are Greninja-Bond, Pikachu-World, Ribombee-Totem,
Rockruff-Dusk, Sneasel-Hisui-F and Xerneas-Neutral. Everything else in the
batch is a species the models, gen9species or champions already cover.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-23 00:45:25 -04:00
Christopher Monsanto
c16546739d Rename the new gen 5 sprites onto encoded names
The Smogon Sprite Project's gen 5 sprites arrived under the packed ids that
"Name sprites by name" had just retired, s<num*32 + formeNum>, so
s10209-b-s.png where the tree now wants ssharpedo-omega-b-s.png. Nothing
validates a filename any more, so they built and published as-is: the xy/
manifest took its names off the stem and called Sharpedo-Mega "10209" and
Gigantamax Meowth "1664-gmax".

Apply the mapping tools/spritedata/rename.ts applied, over src/sprites/gen5
alone and only for the stems that are still ids. 2716 files, all png, bytes
unchanged; the 391 pngs and 3344 gifs already named were left alone. Every
name the ids resolve to is one tools/spritedata/names.ts prints, and none of
them collides with a file already in the directory under either extension, so
the gifs still win the xy/ copy wherever a species has both.

The 33 files flagged -g are Gigantamax, which the data treats as a flag and
not a forme, so they ride on the base name rather than gaining an -o.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-23 00:35:39 -04:00
Christopher Monsanto
bcabe6d37e Publish Meowstic under the name the smogdex asks for
PS calls the male Meowstic just "Meowstic" -- baseForme M, with Meowstic-F
as the alt forme -- and every published name here falls out of the PS data,
so the tree ships meowstic and meowstic-f. The smogdex splits the pair
evenly and calls that entry Meowstic-M: dex-sync's getName renames it on
import, and toAlias turns that into meowstic-m, which is what the page
background, the sprite- sheet class, and the (minisprite:) embeds all ask
for. Nothing produced that name, so the dex has been living off hand-placed
leftovers under xy/ and xyicons/ that no build has touched in years and that
rsync has no reason to delete.

Give the smogon side an alias table, as the items already have, and publish
the sprite under both names. Only that side gets it: PS asks for meowstic
and the filename already says meowstic. This is the whole of the
disagreement -- the other four renames in getName are a move, an accent, and
two spaces that slug back to what we emit -- so one entry covers it.

The sheet had its own copy of publishedName inline; it wants the aliases
too, so it now calls the shared one. No name changes: the two agreed on
every file in the tree, since none carries both -g and -f.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016TEPwEpwMrUFN8Paa9Tb8r
2026-08-23 00:30:28 -04:00
Christopher Monsanto
0bbbc36053 Name sprites by name, and delete the id table
src/ named 48442 sprites by a packed number, s<num*32 + formeNum> for
species and i<num> for items, so s193-b-s.gif is Shiny Charizard-Mega-X's
back sprite and there was no way to know that without looking it up in
data/species.json. The table was the only thing making the tree
navigable, and it existed only because the filenames were opaque.

Put the name in the filename and the table has no job left, so this does
both. A name is encoded rather than spelled: a run of non-alphanumerics
becomes _ when it separates words and vanishes otherwise, decomposing
first so accents are dropped on purpose rather than by accident of how
the source spelled them. Recording where the words divide is what lets
smogon read a name as ho-oh and PS read it as hooh off the same file,
with nothing looking anything up. Formes ride in -o, which keeps the
boundary the two readings differ across and groups a forme's sprites
together under ls.

s/i/x stay, so a directory can hold species and items with rules
matching them apart, and so xatu isn't mistaken for an escape. The x
escape's only other job was telling parseFilename not to resolve a name,
which nothing does now.

Where PS's own formeOrder gives one name to several of the games' forme
slots, nothing tells them apart, so the lowest slot keeps the bare name
and the rest carry -c: 16 files, all in previews/gen9.

The three jobs the table had left, each somewhere narrower now: published
names come off the filename; the 12 items that ship under a second name
are a literal in data/lib; base-forme sheet slots moved to
ps-pokemon.dexnums.mjs last commit. Nothing validates a filename any
more, which get() used to do by throwing; tools/spritedata/names.ts
prints what should exist so a directory can be checked against it.

The smogon and ps deploys materialize byte for byte identically. assets
moves five forumsprites keys, because toSmogonAlias was missing a /g and
so dashed only the first space: deep-seascale, deep-seatooth,
gold-bottlecap, prz-cureberry and psn-cureberry gain their missing
dashes, and the sprite bytes behind them are unchanged. That bug cannot
survive the encoding, which cannot tell which boundary came first. The
smogdex sheet relaid out, since spritesmith packs in glob order; its
2117 css class names are identical.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MQicnpaee6kQXGG7uVExLS
2026-08-23 00:30:23 -04:00
Christopher Monsanto
ded9d487d0 Take the icon sheet's base-forme slots from a generated dex table
ps-pokemon.sheet.mjs reads entry.num out of data/species.json to place a
base species on the sheet, which is the last thing in the build that
needs a number from that table. Move it to ps-pokemon.dexnums.mjs,
generated from pokemon-showdown alongside the hand-kept alt-forme index
table the file already carries.

The sheet is nameSensitive and rebuilt: pokemonicons-sheet.png hashes
the same before and after, and all three deploys materialize byte for
byte identically.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MQicnpaee6kQXGG7uVExLS
2026-08-23 00:29:41 -04:00
Christopher Monsanto
a282c8a643 Bring the data table generators into the repo
data/species.json and data/items.json were generated by scripts that
lived outside the tree, so nothing here recorded where the tables came
from or how to refresh them. Port both into tools/spritedata/, taking
the pokemon-showdown checkout as an argument instead of hardcoding an
absolute path.

Both reproduce the committed tables byte-identically against
pokemon-showdown f2fe71c87.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MQicnpaee6kQXGG7uVExLS
2026-08-23 00:29:40 -04:00
Marty
6591abd406 Add Smogon Sprite Project's Gen 5-style sprites 2026-08-22 20:03:08 -04:00
Christopher Monsanto
ed199c3768 Move single-use rule sets to their call sites
rules/ exists so several deploys can declare the same rules; a set with
one caller is just indirection. Only gen10Modelslike has two (smogon xy/
and ps ani/), so it stays. The live sets move into assets.build.ts and
smogon.build.ts, and the three deprecated sets move into the commented-out
block that was their only reference, which empties minisprites.ts and
social.ts.

Declarations are copied verbatim, so no rule key moves: a full dry run
reports 7748 up to date, 0 would run, and all three deploy trees come out
byte-identical.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MQicnpaee6kQXGG7uVExLS
2026-08-21 01:13:16 -04:00
Christopher Monsanto
45adc3579f Make rule inputs source paths only
Nothing chains any more, so Input collapses to string and the machinery
that existed to order rules against each other goes with it: the
executor's demand graph and per-decl memo, the DryDirty sentinel, the
whole blocked outcome, the driver's producer closure, and inspect's
transitive-consumer fixpoint. What is left is a flat parallel sweep whose
only coupling is byte-identical declarations sharing one execution.

Every deploy's tree comes out byte-identical and every rule key is
unchanged: a full dry run reports 7748 up to date, 0 would run.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MQicnpaee6kQXGG7uVExLS
2026-08-21 01:13:15 -04:00
Christopher Monsanto
327b2318d0 Fold the spritesheet webp into the sheet rule
The sheet's png fed a separate cwebp rule, the one place a rule output
was another rule's input. A third output plus %o1 says the same thing
in one rule; the published css and webp come out byte-identical.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MQicnpaee6kQXGG7uVExLS
2026-08-21 01:13:15 -04:00
Christopher Monsanto
b698ac882a Publish assets under their served prefix and name whole urls
The prefix a manifest value needs isn't knowable here: it lives in the
receiving home's services.toml, and the upload prepends it. So the two
guessed at each other and every reader carried a copy -- the forum's
SMOGON_XF_SPRITE_URL, the dex's hardcoded /__assets/sprites/.

Write the prefix on this side instead: everything served ships under
sprites/ in the tar, the upload rejects a tree that disagrees, and with
the tar path settled a manifest entry can be the whole url. Keys lose
their extension, since a url has nothing left to disambiguate. __meta/
stays at the tar root -- the upload diverts it out of the served tree.

The css pointer becomes spritesheet-css-url.txt, holding the url rather
than a suffix to paste onto one; the rename is deliberate, so a reader
of the old file fails instead of splicing a url into a prefix.

The minisprites webps go: 9d3e9abe shipped them speculatively in 2023
and nothing has read them or __meta/minisprites-hash.txt since, in any
repo. The dex draws minisprites from the spritesheet, and these are
named by PS id where it uses smogon aliases, so they never could have
lined up. newspritecopy() went with them, its only caller.

xy/ and xyicons/ keep the old shape: they ship by rsync and don't know
their url yet.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-21 01:13:15 -04:00
Christopher Monsanto
e3055ae3f5 Fold stamped copying into Manifest
The ctx is #private now; Manifest.copy(f, dest, name) owns the
hash-stamp-record-queue sequence (absorbing set()'s duplicate-key
check), and the sprite-name resolution in spritecopy/itemspritecopy
stays outside as free functions calling it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-21 01:13:15 -04:00
Christopher Monsanto
40bd3052f5 Make the deploy layer and tool I/O async
DeployCtx read/list/hash return promises (copy and write stay sync:
they only queue ops), the stamped copy helpers and buildfile deploy
blocks await them, ActionQueue materialization and packing use
fs/promises, and the CLI, config loaders, spritesheet writer, and
trim tool follow. Declaration-time globbing and the module-init data
reads stay sync: rules exist by the end of the import.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-21 01:13:15 -04:00
Christopher Monsanto
1d7e52ca29 Make the build core's file I/O async
hash and cas move to node:fs/promises: source reconciliation runs a
small worker pool over the stat cache, the executor's clean check
gathers CAS stats concurrently, and inserts/sweeps await their way
through. The sqlite store stays sync; that is all node:sqlite offers.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-21 01:13:15 -04:00
Christopher Monsanto
b8d7aa50eb Replace non-null assertions with explicit checks
Parsers use charAt or check-and-throw; the executor gets a paired()
helper for its parallel arrays; matchSubsets returns {entry, matched}
pairs so the deploy loop stops indexing two arrays; spawn stdin and
pid get real guards.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-21 01:13:15 -04:00
Christopher Monsanto
dc2bff9738 Convert declarations to the house shapes
Interfaces become type aliases with comma members (extends becomes
intersection), and class privates use #names instead of the private
keyword.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-21 01:13:15 -04:00
Christopher Monsanto
73f5954407 Adopt the house import style
Node builtins come in as node:-prefixed namespace imports;
multi-symbol internal imports use short namespace aliases (artifact,
cas, subst, db, api, executor). Single-symbol pulls, types, and the
buildfile DSL names stay braced, matching the sanctioned exceptions.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-21 01:13:14 -04:00
Christopher Monsanto
3ec1abad12 Write let instead of const
Per the house rule, const survives only where TypeScript hangs
behavior off the spelling: the <const T> type parameter in rule()
and the as const option tables.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-21 01:13:14 -04:00
Christopher Monsanto
ebbb5e6f2e Tighten formatting to the smogon.com house style
Annotation colons are tight (ternaries keep their spaces), plain
double-quoted strings become single-quoted, and .editorconfig comes
over verbatim. Rule cmd values are untouched; the sheet rules reran
once because their tool sources are declared deps, byte-identically.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-21 01:13:14 -04:00
Christopher Monsanto
3e87df43da Replace defineDeploy with free-floating deploy() blocks
deploy(ctx => ...) registers like rule() does, so a buildFile
co-locates each shipping block with the rules it ships instead of
one monolithic finish; the blocks run in registration order after
the build, sharing one output tree per module. The CLI slices the
registry across its sequential imports to know which blocks belong
to which file.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-21 01:13:14 -04:00
Christopher Monsanto
8f58cf3da0 Ship the smogon deploy through deploy.json5
The xy and xyicons rsyncs move from ad-hoc scripts (updatexyicons.sh
and the untracked syncdexani.sh) into dir-mode deploy entries;
deploy smogon replaces both.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-21 01:13:14 -04:00