Commit Graph
35 Commits
Author SHA1 Message Date
wesandClaude Opus 5 d3d93a982a reading map: seven models, one library — /reading/models
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>
2026-08-16 13:40:11 -04:00
wesandClaude Opus 5 898c942f95 reading map: a second page, arranged to be read rather than to look like weather
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>
2026-08-15 20:42:14 -04:00
wesandClaude Opus 5 78c74ff50b Reading map: make a marked node findable inside a dense cluster
A quote landing in the philosophy knot was effectively invisible. Two causes,
and only the second one was obvious.

Size. `PointsMaterial` has one size for the whole object, so the vertex shader
is patched to take a per-vertex `aScale`. Quoted draws 2.1x, newest 1.5x —
quoted is larger because it is the one a reader goes hunting for, having just
read the passage in the header; the newest is ambient and nobody looks for it.

Draw order, which was the real problem and which no hue or size would have
fixed. Every highlight is one transparent Points object, so vertices draw in
buffer order: a node marked early in the trace sits under the two dozen white
sprites and the green trace lines that come after it, and gets blended away.
Measured on the worst case in the picture — the most crowded Meditations node,
25 neighbours within 0.05 — the mark rendered at 20 saturated pixels, peaking
at (209,114,225) against a target of (232,121,249).

So the two marked nodes are drawn a second time into their own two-point
buffer above the trace. Same position, same sprite, same colour as the tint
underneath: the same mark rendered where nothing can cover it, not a badge
stuck on top. That is 54 pixels peaking at (229,120,245) — essentially the
pure colour, and visible at a glance in the zoom.

An unused slot hides by holding scale 0, which collapses gl_PointSize, rather
than by juggling draw ranges for two points.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-14 12:34:24 -04:00
wesandClaude Opus 5 6849d8ffe5 Reading map: a legend under the figure
Five marks, and until now the only way to learn what any of them meant was to
hover a dot. The last row is the one worth having: it says out loud that the
fuchsia node is where the quote at the top of the page came from, which is
otherwise a connection nobody would guess was being made.

Three things it does not do:

Counts come from the payload at mount, not from the markup — they move every
time a book is indexed, and a hardcoded 8,424 would go stale on the next one.

Swatches take the palette the renderer resolved rather than re-reading the CSS
vars, so a scheme modifier on the container (`--ember` swaps the newest hue)
cannot leave the legend describing a different picture than the one on screen.

The quoted row follows markQuoted's return value, not the attempt. A quote
whose source book was never indexed leaves the map unmarked, and the legend
now stays quiet about it instead of pointing at a colour that is not there.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-14 09:18:07 -04:00
wesandClaude Opus 5 b1a0192dd1 Reading map: tint the nodes themselves instead of ringing them
Wes asked for a hue change on the node and got rings; the rings look like
chrome bolted onto the picture. This does what was asked: `signal` gains a
per-vertex colour attribute, and marking a node is three floats written into
the buffer it already draws. No extra object, no extra draw call, and the
billboarding the ring meshes needed goes away with them.

vertexColors MULTIPLIES material.color, so the material is white now — left at
the signal colour it would filter every tint through that hue.

The reason I had reached for rings was real, and needed handling rather than
avoiding: the trace head parks on the newest node under prefers-reduced-motion
at ~3x a signal point's size, which buried the amber tint completely — caught
by pixel-sampling the render, where amber came back ABSENT in that mode. The
head marks how far the draw has got, and under reduced motion there is no
draw, so it now hides itself. Both marks verified present in both modes.

Colour is doing the whole job now with no form difference to fall back on, so
the measured contrasts move from reassuring to load-bearing; they are recorded
in the CSS. Every pair that matters clears dE 30 worst-case across normal,
protan and deutan.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-14 09:05:48 -04:00
wes 51b4033541 Reading map: measure the marker contrast instead of asserting it
The ember-scheme override was sky blue against the fuchsia quoted marker,
which measures deltaE 0.5 under deuteranopia — the two rings would have been
the same colour to a deuteranope, and telling them apart is the one job these
hues actually have. White is the only candidate clearing all three of that
scheme's marks (worst case 30.4; cyan managed 7.8, lime 8.5).

Also records honestly that amber vs the trace green is 4.8 under protanopia,
accepted on the same grounds the ember scheme already documents, and drops the
border-top when the reading widget is the rail's only child — on a page with
fewer than two headings it was a separator with nothing above it.

Counts in the module docstring were still 262/206; they are 273/215.
2026-08-14 08:54:46 -04:00
wes 3039ff45ed Reading map: correct the measured no-op rate for markQuoted
0.9% (2 of 230), not the ~7% estimated before the endpoint was restricted to
indexed books — that filter, added for the byline fix, removed nearly every
orphan as a side effect.
2026-08-14 08:47:14 -04:00
wes bd6910a5dd Reading map: ring the newest highlight and the one quoted in the header
Two standing markers, drawn as rings rather than brighter dots. The animated
trace head is already a bright point and comes to rest on exactly the newest
node at the end of every draw cycle — under prefers-reduced-motion it parks
there permanently — so a second filled dot at that coordinate would read as a
rendering fault. Outlines say "different kind of thing" without competing.

The newest highlight is the LAST trace entry, never the max of `t`: the
payload is pre-sorted, `t` is date-only, and many highlights share a day.

The quoted one is looked up by text_hash, which /highlights/random now
returns beside the quote. The two fetches race on every page view, so the
hash is published both as a global and as a `garden:quote` event — one covers
each order. About one highlight in fifteen has no node, because its source
book was never indexed; markQuoted returns false and nothing is drawn.

Rings are flat geometry parented to the rotating world, so they are
billboarded each frame or they would vanish edge-on twice a revolution.

Also adds the "Currently reading" rail widget. It says how long ago on
purpose: six of the ten books on that shelf have not been opened in months,
and without a relative date the thing looks broken rather than honest. Past
45 days it mutes itself. No cover — kobo.books.isbn is a sideloader's UUID
for most of the shelf and storygraph_links has no cover URL.

The TOC rail is now always rendered, since it carries the widget too; it was
conditional on having two headings, which would have hidden the widget on the
homepage. Hidden explicitly on wide notebook pages, where a third grid child
would wrap under the sidebar.
2026-08-14 08:45:12 -04:00
wes d0f56e4881 Reading map: the payload is a generated artifact, not a tracked file
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.
2026-08-14 07:59:25 -04:00
wesandClaude Opus 5 4dbf8fe526 Reading map: honour reduced-motion for the orbit; give /knoebels a real nav link
Two follow-ups on yesterday's hero.

The `prefers-reduced-motion` branch only skipped the trace *animation* — the
scene kept orbiting continuously, which is precisely what a reader who asked for
reduced motion is asking not to get, on the homepage no less. The cloud now
holds still; it still renders in full.

/knoebels was reachable only from the homepage crowd strip, and that strip is
hidden whenever the park API is unreachable — so an API outage made the page
unreachable from anywhere on the site. It gets a sidebar entry, the same way
/notebooks does, since neither is a vault note that buildNavTree could carry.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-13 08:51:36 -04:00
wesandClaude Opus 5 b8d7dad3e3 Knoebels: the park map gets its own page
The homepage had two competing visuals once the reading map landed, and the
park heat map was in the header — above the hero. Moved to /knoebels; the
homepage keeps the one-line crowd verdict as a teaser that links there.

Extracted verbatim into KnoebelsMap.astro. Its styles stay `is:global`: every
element inside the <svg> is built with createElementNS and so never carries
Astro's scoping attribute, which would make a scoped `.knb-map text` selector
silently match nothing.

Homepage HTML drops ~4 KB and no longer fetches /knoebels/rides or the Esri
tiles; the strip's own /knoebels/now call is unchanged.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-13 08:43:32 -04:00
wesandClaude Opus 5 9519c46fe2 Reading map: a 3D hero of everything read, traced by what I highlighted
An orbiting point cloud of all 8,424 indexed passages, with the 262 highlighted
ones lit and threaded in the order they were marked. Data comes from library-rag
(chunks + embeddings) projected to 3D with UMAP; the page never touches Postgres.

Projection is `airy` (n_neighbors=50, min_dist=0.8) and the palette is `frost` --
both chosen by eye against the alternatives. Note that airy shows the underlying
book-territory structure LESS than the tight projection the pre-flight measured,
so the 0.805 k-NN purity figure describes that one, not this.

Page weight: the wrapper adds 321 B gzip to the homepage's blocking load. `three`
(127 KB gz) and the payload (56 KB gz) are a dynamic chunk fetched after render.
It fails closed -- no WebGL, no payload, or a dead fetch leaves a plain page.

The payload is a static snapshot; the 5-minute auto-build rebuilds the site, not
the data. New reading reaches this map only when export.py is re-run.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-13 08:30:45 -04:00
wesandClaude Opus 5 9da9a6cba2 Publish opted-in marimo notebooks at /notebooks
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>
2026-07-29 22:07:14 -04:00
wesandClaude Opus 4.8 8a1e502cc3 Park map: add motor boats to the landmark labels
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-22 11:58:23 -04:00
wesandClaude Opus 4.8 ece2dadc27 Park map: add impulse to the landmark labels
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-22 11:56:08 -04:00
wesandClaude Opus 4.8 f5c37ac5a5 Park map: add phoenix back to the landmark labels
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-22 11:55:20 -04:00
wesandClaude Opus 4.8 5e0529930e Park map: fixed landmark labels (twister/sklooosh/haunted mansion/kiddie whip)
Wayfinding anchors across the park regions instead of the busiest rides;
name-only, since the glow and hover already carry the waits.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-22 11:51:25 -04:00
wesandClaude Fable 5 5d08d6b1bb Strip order: knoebels line last, directly above the park map
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-22 11:20:25 -04:00
wesandClaude Fable 5 df5b58c4fd Park-heat map: Esri World Imagery basemap under the glows
Web-Mercator slippy projection for both tiles and rides (exact
alignment), dark tint to keep the garden tone, label halos for
legibility over imagery, required Esri attribution. Google tiles were
ruled out (ToS requires the Maps JS API + key/billing).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-22 11:19:39 -04:00
wesandClaude Fable 5 16b837a2e7 Wider map label clamp (black diamond was still clipping)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-22 11:17:08 -04:00
wesandClaude Fable 5 8b9b5f2831 Homepage strip: Susquehanna stage + 48h flood forecast line; clamp map edge labels
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-22 11:15:56 -04:00
wesandClaude Fable 5 2c7f467cf9 Homepage: Knoebels park-heat map under the live strip
One glow per ride from api.c0smere.net/knoebels/rides — live waits while
the park is open, typical-weekend-peak means otherwise. Color = wait
class (validated dark palette), glow radius = magnitude, top-5 direct
labels + native tooltips. Fail-closed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-22 11:11:07 -04:00
wesandClaude Fable 5 01d7ce8aa8 Homepage live strip: Knoebels line (live crowd verdict / best-day outlook)
Second row in the live strip fed by api.c0smere.net/knoebels/{now,outlook}
(knb-oracle pipeline): while the park is open, live avg wait + verdict with
a crowd-colored dot; while closed, the best upcoming day. Fail-closed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-22 10:05:32 -04:00
wesandClaude Fable 5 97d302e9af Homepage h1s back to section heads (links row left the vault index); LinkedIn in nav + footer
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-21 22:10:55 -04:00
wesandClaude Fable 5 8f48656e91 Build stamp, live listening strip, OG cards, closer shiki ground
- 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>
2026-07-21 22:08:53 -04:00
wesandClaude Fable 5 3d70e23471 Manual rebuild hook + polish: single title h1, '›' tree marker
- 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>
2026-07-21 21:45:36 -04:00
wesandClaude Fable 5 bb80d1836a Adopt Wes's cyrion-local resume slug fix (easy-bronze-gazelle) in header + footer
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-21 21:40:34 -04:00
wesandClaude Fable 5 601922e607 Design pass: visual alignment with the Ghost blog
Fixed-dark theme matching blog.c0smere.net (Source theme + injections):
bg #0b0d12, white/64%-white text, accent green #218107 (links lifted to
#2fa50f for contrast). Self-hosted JetBrains Mono + Libre Baskerville —
the same woff2 files the blog serves. Site header with logo + nav
mirroring gh-head; serif-italic quotes; blog-style borderless tables;
single dark shiki/mermaid theme; homepage styled as a typographic feed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-21 21:38:04 -04:00
wesandClaude Opus 4.8 0a57aa9db1 Footer resume link -> public Reactive Resume URL (Wes's fix); auto-build detects uncommitted repo edits
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-15 18:44:31 -04:00
wesandClaude Opus 4.8 7fe09f077a Header slot: random reading highlight per page view
Pulled client-side from /highlights/random (max_len=280); attribution
from the API's parsed title/author; hidden when the API is unreachable.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-15 15:42:47 -04:00
wesandClaude Opus 4.8 a05e3f7661 Footer quotes + right-hand table of contents
- two static quotes above the name line in every footer
- Toc component renders h1-h3 from Astro's render() headings as a sticky
  right column on >=88rem screens (hidden below); scroll-position
  highlighting via a small passive listener; shown only when a page has
  2+ headings, so listings and short notes stay clean

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-15 15:34:20 -04:00
wesandClaude Opus 4.8 ec3aa15e66 Drop the api link from the footer caption
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-15 15:30:25 -04:00
wesandClaude Opus 4.8 f9e45f32f8 Move odometer into the shared footer
Widget + script + styles live in Base.astro now, so every page ticks;
index.astro is back to just rendering the vault homepage.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-15 15:21:17 -04:00
wesandClaude Opus 4.8 13b23ace3d Sidebar nav: full garden tree on every page
- buildNavTree: every non-draft page nested by vault folder; folders link
  to their (real or synthetic) index page and take its title when set
- NavTree component: recursive details/summary tree, no JS — folders on
  the current path render open, current page gets aria-current styling
- layout: sticky left sidebar >=72rem, flows after the footer as a site
  index on narrow screens

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-15 14:54:04 -04:00
wesandClaude Opus 4.8 04c9aa2fb2 Astro garden skeleton: Quartz-compatible URLs, Obsidian markdown, odometer homepage
- glob content loader over the vault's Digital_Garden with verbatim-path ids
- remark plugin: wikilinks, image embeds (wiki + relative md), callouts
- URL parity with live Quartz sitemap verified 102/102 (built set is a
  strict superset: +30 synthetic folder listings)
- KaTeX, lazy mermaid, shiki dual themes, quoted-string draft handling
- copy-assets resolves embeds vault-wide (fixes live-site 404s for
  Blog Scratch/assets images) into flat /assets/
- homepage odometer widget: fetches api.c0smere.net baseline, ticks at
  lifetime average rate, fail-closed hidden on error
- deploy/: nginx try_files config + compose (port 18100) + containerized
  build script for cyrion

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-15 14:18:59 -04:00