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
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
`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>
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>
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 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
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
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>
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>
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>
makeCtx serves finish over the CAS and ActionQueue; cli.ts adds the
build/deploy/run/inspect verbs (unwired until cutover). Copy-mode
materialization chmods CAS-sourced files back to 0644.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Keys resolve after producers finish, so there is no upfront plan; identical
keys share one execution, failures block dependents, and GC (union builds
only) drops undeclared keys then sweeps the CAS.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
helpers.ts is a pure move out of api.ts; artifact.ts introduces
Artifact-returning rule()/forEachRule() with execution-time content keys.
The old api.ts machinery still powers Buildfile.ts until cutover.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Keeps the v1 file_cache on migration; failures are never recorded, so
failed and never-ran are the same state.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The deploy tool grows a deploy subcommand that runs the build, packs the
tar, and streams it to the command .env names. Packing now finalizes the
archive; without the end-of-archive marker, strict stream readers
(Python tarfile, hence smogonctl) die on truncation.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
They are outside the rule key, so a format change (like the one in the
previous commit) silently disabled rename detection for existing records.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Verify rename sources against recorded stats at copy time; colliding
rename destinations/sources fall back to running the rule
- Restrict rename detection to single-input, no-deps rules (sheet-style
rules embed input names in output bytes)
- Include input extensions in the rename template (magick picks output
format from extensions)
- Adopt only rules with no stored record, never known-dirty ones
- Skip dotfiles in glob (tup parity)
- Complete sheet dep closures (data/lib, lib/root)
- Validate --jobs; reject non-numeric values instead of silently no-oping
- Worker pool drains all workers before rethrowing; per-rule internal
errors abort scheduling instead of racing db.close()
- SIGKILL all live process groups on second Ctrl-C
- Transactional loadStoredRules snapshot; dry run without existing state
uses an in-memory db; idempotent DDL
- rule() rejects % placeholders in literal outputs
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>