20 Commits

Author SHA1 Message Date
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
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
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
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
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
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
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
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
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
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
bc39c94851 Fix review findings in the content-addressed core
- casInsert verifies an existing object's bytes before trusting it, fsyncs
  before the rename, and the store records object sizes so a crash-truncated
  object reads as dirty instead of clean forever.
- The driver hashes sources over the producer closure, so partial builds
  (inspect) that pull in producers outside the demanded set work.
- Unknown executor errors are reported and abort the build instead of
  vanishing into allSettled.
- %b/%B expand to nominal names at declaration (never CAS basenames), which
  also puts name-dependence in the identity key; cmds are key-joined with
  NUL so a multi-line command cannot collide with split commands. This
  churns every rule key once.
- Rules must declare at least one output; output names reject shell-hostile
  characters.
- run --link copies read-only (CAS) sources instead of hardlinking 0444
  modes into deploy trees; ctx.list filters dotfiles and directories like
  the build-side glob; inspect resolves targets against the invoking cwd.
- The registry dedupes identical declarations (same cmds/inputs/deps/
  outputs/display), returning the existing artifacts, so shared rule sets
  are plain functions and memo() is gone; ps.deploy.ts no longer declares
  the gen5 gif set it never ships.
- rule() returns a tuple typed by its literal output list (single string
  output returns the Artifact directly), so destructuring needs no
  non-null assertions.
- Manifest is a class carrying the deploy ctx: set() rejects duplicate
  keys, write(dst) emits the sorted JSON, and spritecopy/itemspritecopy/
  stampcopy take just the manifest. The unstamped smogon sets (xyitems,
  fbsprites, twittersprites) are deprecated and commented out.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-21 01:13:12 -04:00
Christopher Monsanto
499651b3f8 Strip PNG date chunks from magick outputs
date:create/date:modify tEXt chunks made rebuilds nondeterministic, which
would churn every content-addressed name.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-21 01:13:12 -04:00
Christopher Monsanto
460da484a1 Port the Buildfile and deploy scripts to co-located deploy modules
rules/ holds the shared sets (memoized) and the publish helpers that were
duplicated across the vm scripts; assets/smogon/ps .deploy.ts each declare
their rules and finish. The smogdex sheet is split into two chained rules,
retiring the undeclared spritesheet.png temp.

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