The Play button said "about 3.0 MB". That is the gzipped total, and no public
visitor gets it: NPM on the VPS blanks Accept-Encoding for every proxied
request at its own nginx.conf:44, so cyrion's gzip_static is never asked and
answers raw. The real cost is about 9 MB, and a button whose entire purpose is
to be honest about the download was understating it threefold.
It now quotes rawBytes. Overstating is the safe direction, and this goes back
to wireBytes -- a third of the size -- if NPM is ever made to pass the header
through. Falls back to wireBytes for an export made before rawBytes existed.
Also puts a warning at the top of nginx.conf. It is bind-mounted as a single
FILE, so the inode is pinned and `git pull` gives the file a new one the
running container never sees; nothing in auto-build.sh recreates it. The
gzip_static rule added an hour ago was not live until the container was
force-recreated by hand, and the build was green the whole time.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S4wdbGSVzvUDHBWRjSVxfr
A third instance of the pattern notebooks-export/ and the reading payloads
already use: an artifact built somewhere with a toolchain this repo's
node:24-slim container does not have, dropped into a gitignored directory,
staged into public/ by a script, and picked up by auto-build.sh's signature on
the next tick. Nothing here needs a JDK, Go, or an opinion about the game --
cannons_reloaded's deploy/export-cannons.sh hands over a directory that is
already ready to serve.
Three things are load-bearing and easy to get wrong later:
The payload is at /cn/, not /cannons/. With build.format:'file' the page
builds to cannons.html, and try_files would then have a file and a directory
competing for the same URL. That is exactly why the notebook exports live at
/nb/ and not under /notebooks/.
nginx gets gzip_static for /cn/. nginx:alpine has `gzip on` commented out in
its base config, so without it the page ships 9MB instead of 3.3MB and nothing
anywhere reports that. It gets no-cache rather than an expiry, because
classes.js and cannons.wasm are stable filenames whose contents change on
every rebuild -- a long expiry would pair a stale client with a fresh server.
The page loads on click, not `loading="lazy"`. Lazy only defers until the
frame nears the viewport and this frame is above the fold, so a few MB would
download for anyone who opened the page to read the paragraph.
A missing export is a normal state throughout: copy-cannons.mjs stages
nothing, the page says so, and the sidebar link does not appear.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S4wdbGSVzvUDHBWRjSVxfr
The same 9,194 passages arranged by seven embedding models, switchable. Every
model's coordinates are Procrustes-aligned to nomic, without which the toggle
would be seven unrelated arrangements and the motion between them meaningless.
⚠️ The page states the NEIGHBOUR-AGREEMENT number, not the animation, and the
distinction is load-bearing. Every projection of 1024 dimensions into 3 is lossy
in a way that shapes what you see: measured directly, PCA and UMAP disagreed by
10x about whether whole books move together, each reporting its own arithmetic.
So the claim is computed on the raw embeddings where there is no projection to
be an artifact of (models share ~50% of each passage's 15 nearest neighbours),
and the picture illustrates that number rather than being the evidence for it.
The page says so in as many words.
Renderer gains setCoordinates(), driven from the existing render loop rather
than its own rAF so a morph cannot outlive the scene or run while scrolled away.
It writes through the single `xyz` buffer the nebula, signal and trace all read
from, so every layer moves together and nothing drifts out of registration.
`trace` is now optional. The banner and explorer always carry one; a comparison
payload legitimately does not — its subject is where passages MOVE between
models, not the order they were highlighted in.
Payload is split: a 44 KB manifest with everything shared, plus one ~145 KB
coordinate file per model fetched on demand. The per-node book/kind arrays are
identical across models, so carrying them seven times is waste, and eager
loading would be ~1.1 MB before first paint.
auto-build.sh now watches public/reading_*.json as ONE glob. The previous
OR-chain was subtly broken — with `-o` the `-printf` binds only to the last
clause, so earlier matches printed in a different format and the signature was
not stable. The glob also picks up future payloads without an edit.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The banner draws all 9,194 chunks in one flat colour. `nodes.book` was in the
payload but only ever reached the tooltip, so nothing on screen said which book
or which region you were looking at.
This adds /reading: the same corpus at the pre-flight's 15/0.1 instead of the
banner's 50/0.8, coloured by kind of book, with any single title isolatable.
The two projections are NOT interchangeable, and that is why this is a separate
page rather than a mode of the banner. 50/0.8 was chosen *because* it blurs book
territory into something that reads as a cloud; showing that territory is the one
thing this page exists to do.
Renderer notes:
• Explorer mode turns on only when the payload actually carries a `kind`
layer, not when the caller asks for it — a page pointed at the banner's
file degrades to the flat cloud instead of throwing.
• Smaller and denser points (0.032/0.62 vs 0.055/0.3). Three hues piling up
in one volume average toward grey; that pairing is what keeps each one's
hue, and it is not cosmetic.
• Isolation dims to 8.5% rather than hiding. Against an empty frame you learn
where a book's chunks are and lose the only question worth asking, which is
where they sit relative to everything else.
• `nebulaBase` keeps a pristine copy of the vertex colours; dimming from an
already-dimmed buffer compounds and the cloud would fade on every hover.
Kind colours are slots 1-3 of the validated categorical palette, checked
ALL-PAIRS against this file's own #0b0d12 — a point cloud has no series order,
so the adjacent-pairs default that suits stacks and lines is the wrong test.
Worst pair CVD dE 9.4, worst normal-vision dE 20.9.
⚠️ `--map-kind-narrative` (aqua) sits near `--map-trace` (the garden green).
Tolerable only because form separates them absolutely — diffuse points versus
one continuous line — and nothing is encoded by the similarity. If the trace
ever becomes points, re-measure that pair.
The payload is gitignored for the same reason the banner's is: a tracked
generated file dirties the tree and breaks the `git pull` at the top of every
build tick. auto-build.sh's signature now watches both, or a corpus change
would rebuild one page and leave the other on yesterday's cloud.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
library-rag-export.service now regenerates reading_umap.json on cyrion
whenever the corpus or its located highlights move, writing straight into this
checkout. Keeping it tracked would defeat that: a generated file that changes
under git dirties the tree, which both flips auto-build.sh's change signature
and breaks the `git pull` at the top of every build tick — the reason
notebooks-export/ is already ignored, in that file's own words.
So it joins them: ignored, plus a find line in the signature block so the
5-minute tick still notices a new projection.
The cost is that a fresh checkout has no map until the service runs — on
cyrion that is within the hour, anywhere else never. ReadingMap.astro says so
where someone would hit it.
Notebooks from the marimo server can now appear in the garden. A notebook
opts in with an HTML comment in one of its markdown cells — invisible when
rendered, greppable in the .py source:
<!-- garden:publish
title: Dream of Spotification
order: 30
-->
Default-deny on purpose: garden.c0smere.net is public and the export bakes
each notebook's executed output into the page, not just its code.
export-notebooks.py also carries a NEVER_PUBLISH list (genome_*, coursework)
so a marker pasted into one of those refuses loudly instead of publishing.
Pipeline: export-notebooks.py runs each marked notebook in a one-shot
marimo container (same image/env/GPU as the live server, so it hits the real
databases) into notebooks-export/ + index.json; copy-notebooks.mjs stages
those to public/nb/; Astro reads index.json to build the pages and the
sidebar section.
Isolation choices worth keeping:
- Own timer and own lock, separate from the 5-min garden build — executing a
notebook takes minutes and must never hold up a build tick.
- Cached on notebook content; both index.json and .cache.json go through
write_if_changed, since auto-build.sh hashes mtimes under notebooks-export/
and an unconditional rewrite would force a full rebuild every 30 minutes.
- Per-notebook timeout; a failure keeps the previous export and continues.
- Runs against a throwaway copy of the notebook dir, so notebooks that write
scratch files don't dirty the notebooks repo.
- Notebooks are NOT injected into the garden collection — they aren't vault
notes, and doing so would move noteCount and the sitemap.
build.format:'file' makes the listing a file (notebooks.html) beside a
directory of detail pages. Verified against the running nginx: /notebooks and
/notebooks/<slug> resolve through the generic try_files but /notebooks/ does
not, so nginx.conf gets an explicit location for the trailing-slash form.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- footer: 'built <ts> · <hash> · <n> notes', hash linking to Gitea
(GARDEN_COMMIT passed in by build.sh; '+' marks dirty-tree builds)
- homepage header: live strip fed by api.c0smere.net/spotify/summary
(weekly aggregate + monthly top artist/genre), fail-closed like the
odometer and header quote
- /og/<slug>.png satori+resvg cards for every note + the homepage;
og:/twitter: meta in Base
- shiki github-dark-default (#0d1117 sits closer to the page ground)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- deploy/rebuild-hook.py: tiny stdlib HTTP service (:18101) — button
page for the Homepage dashboard iframe, POST /rebuild runs
auto-build.sh --force, /status + /healthz for feedback/monitoring
- auto-build.sh: --force flag + flock so timer and manual builds
serialize
- [...slug].astro: skip the synthetic h1 when the note body already
opens with an identical h1
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Root-owned node_modules/dist/public from the container blocked git pull
(public/fonts could not be created). Chown once, then keep artifacts
user-owned via --user + HOME/npm cache in /tmp.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- deploy/auto-build.sh: git pull + rebuild only when the repo HEAD or any
vault file mtime changed (sha256 signature in .build_state); replaces
the Quartz update_quartz_docker.sh loop
- systemd user units (5-min cadence, matching the old Quartz timer)
- compose service/container renamed garden-next -> garden
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>