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>
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>
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-*.
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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
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
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
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
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
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
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
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
deploy <names> -o <dir> materializes each entry's subset under
<dir>/<name>/<entry index>/ instead of running its command (tar
entries included), for eyeballing what would ship.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Nothing in tools/build or tools/deploy imports debug, and the
deploy package never imports sprite-data (its consumers live in
the root package, which declares it).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Unhandled rejections crash Node by default since v15, and node:sqlite
covers the Store: readBigInts replaces defaultSafeIntegers, a manual
BEGIN/COMMIT helper replaces transaction(), busy detection keys on
errcode, and lookupRule normalizes the null-prototype rows. No native
build step remains (allowBuilds gone from the workspace).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The deploy CLI gets a hand-rolled verb dispatch over per-verb
parseArgs option tables and one usage text; trim and
smogdexspritesheet are plain parseArgs calls. tools/build only
listed the dep without importing it.
The smogdex sheet rule depends on its tool's source, so it reran
once; the rebuild was byte-identical (stamps unchanged).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
An entry with dir: true materializes its subset into a temp
directory whose path replaces %d in the cmd, instead of piping a
tar on stdin -- rsync-style transports (the PS ani sync) need real
files. The flag and %d must agree, so a forgotten one is caught at
config load.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A dying consumer destroys the tar pack and its pending entry sinks,
each of which emits the EPIPE as an unhandled streamx error and
crashed the process before the child's exit code was reported. Keep
the pack and its sinks quiet; the command's exit code is the report.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Deploying is opt-in per name; with no names the verb prints each
deploy's buildFile and (subset, cmd) entries instead of shipping
everything.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
An untracked json5 config maps deploy names to a buildFile plus
(subset globs, cmd) entries: the deploy verb builds the named
buildFiles, finishes each, routes the outputs through the subset
globs, and pipes one tar per entry to its command. Every glob must
match an output and every output must be covered by some entry.
The tar no longer carries __key; the upload command names the asset
set itself (smogonctl assets upload sprites).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
They are the buildFiles the upcoming deploy config points at; "deploy"
now belongs to the config entries that ship their outputs.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- 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>
pnpm build/deploy now run the umbrella CLI (build/deploy/run/inspect); the
fixed-name build/ tree, the old planner (rename detection, adopt, tamper
checks), and the vm deploy sandbox are gone. Also lands tools/deploy/api.ts
and the CLI, which earlier commits referenced but an overbroad local
gitignore pattern had silently excluded.
Migration: the store migrates the v1 db in place; build/ is dead and can be
deleted. A full rebuild repopulated .build/cas, and the old and new deploy
trees were diffed: byte-identical except the two multi-file hash pointers
(algorithm change), fb/twitter PNG re-encodes (pixel-identical; old outputs
predated current tool versions), and a stale 2023 orphan sprite the old
system never cleaned up.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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>