Audit item 14.
THE DEPLOY VERIFIED THE ONE URL A PLAYER NEVER LOADS. Every check in deploy.sh appends
?v=$BUST — which is exactly what makes it bypass Cloudflare's edge — so all six could pass
green while every actual player was still being served the previous build from a POP. The
script's answer was a line of prose at the end asking a human to remember to purge. It now
fetches js/boot.js the way a browser does, with no query string, and byte-compares it to what
was just shipped; a stale edge fails the deploy and prints the purge steps. No credentials
needed, and it asks the question the prose was only gesturing at.
This is the same mistake as the `assets` bug and the unwinnable saliva tide: verifying the
mechanism rather than the path the player actually takes through it. Third time, so it is
written into the tool this time rather than into a comment.
QUALITY SWITCH. renderer.setPixelRatio was hardcoded to min(dpr, 2), so a retina laptop drew
four times the pixels of a 1x one with no way for its owner to decline — and this game is
fill-bound, not geometry-bound (fogged tube, additive glows, a full-screen damage layer), so
that is the lever that moves. `performance` caps it at 1. On the title beside difficulty,
saved, same reload path (the pixel ratio is set at boot, before any card exists).
save.js's two one-line setters collapsed into setOpt(key, value) now that there are two options
and a third is obvious.
Note on verification: the chip persists and boot reads it, confirmed live. The resolution
change itself is not observable in the harness — that browser pane runs at devicePixelRatio 1,
where min(1,1) and min(1,2) are the same number.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
I shipped a working tree earlier and had to tell John "live matches no commit in
the repo" — which is exactly the sentence a deploy should make impossible.
deploy.sh now writes the HEAD short-sha to web root as VERSION, and verifies it
came back. `curl https://partly.party/gutsy/VERSION` answers "what is online right
now" without trusting anyone's memory of the last deploy.
The dirty check is scoped to `git status --porcelain -- web/`, not the whole
tree, because only web/ ships: editing docs or this script does not make the
payload unreproducible, and a stamp that cried wolf on every NOTES edit would get
ignored. A dirty payload ships as `<sha>-dirty` and says so, loudly, rather than
refusing — mid-round lanes need to push a look at something without committing
first, and a deploy tool that blocks that will just get bypassed.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
README said "target: partly.party, not wired yet". It's wired.
tools/deploy.sh follows the GAMES doctrine (deploy-map skill): QA gate -> rsync
web/ to VPS staging -> docker cp into forum-nginx -> verify. No build step, so
web/ ships as-is: vanilla ES modules + importmap with three r175 vendored, all
paths relative or resolved from import.meta.url, which is why it runs from a
subdirectory with no base rewrite. 6.6 MB. nginx.conf needed no edit — its
catch-all `root /usr/share/nginx/html` already routes any new directory, which is
how blobbo works. /gutsy (no slash) 301s to /gutsy/.
Excludes web/dev/ (ruling #5: never shipped). Verified live, not assumed: the
lane harnesses 404 through to the arcade index.
THE 200 THAT ISN'T. That same catch-all try_files means ANY missing file returns
the arcade's index.html with HTTP 200. A deploy that lost every module would
still curl 200 on all of them. So every check asserts on CONTENT, never on the
status code — this is the failure deploy-map records as ".bin data URLs serving
arcade HTML". The guard caught itself, too: it originally grepped for
`<title>MonsterRobot`, but the arcade's real title is
`~*~W3LC0M3 2 TH3 M0NST3R R0B0T P4RTY 4RC4D3~*~`, so it could never have fired.
Matched against the live box now.
DURABILITY, and it's John's call. This docker-cp's into the container's own
layer, matching blobbo/glytch: zero downtime, nothing else touched, survives
restarts and reboots (restart: unless-stopped) — but NOT `docker compose up
--force-recreate` or an nginx image bump, which wipe it. Re-running the script is
the fix. The durable alternative is a bind mount in forum/docker-compose.yml like
cratewars/roguelike have, which costs a forum-nginx recreate and brief downtime
across ALL of partly.party, so this script does not take that decision.
ship-check (README says run it before deploying — I ran it after; my miss, and it
came back clean):
1 auth N/A — static files, no endpoint/API/admin surface
2 secrets PASS — nothing secret-shaped in the payload or the diff
3 input/SSRF PASS — the only user string reaching a fetch is ?lvl=, and C
gates it on the CAMPAIGN allowlist BEFORE the load
(levels/index.js:151). Traversal tested live with
`curl --path-as-is` (plain curl normalises ../ away and proves
nothing): /etc/passwd never leaked. DEPOT_BASE in assets.js is
unused — the manifest has zero external URLs.
4 money N/A
5 deploy PASS — verified by content + booted in a real browser:
assets.misses() == [], 15 draws, 69k tris, zero console errors.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>