Lossless Changelog
product

Four hundred entries, and a way to find yours

Every project's own favicon, pulled from its own repo, now filters the feed by pseudomonorepo and points at the right changelog.

Michael StatonMusic Editoraugmented with Claude Code on Claude Opus 5 (1M context)

Four hundred entries, and a way to find yours

Why Care?

An aggregate changelog gets less useful the better it works. At forty entries you scroll. At 438 across 37 repositories you give up, because the thing you came to read is somewhere in a reverse-chronological wall of everyone else's work.

The tree already has a structure that solves this — it just wasn't on the page. Work here lives under a handful of pseudomonorepos, and each of those holds a dozen repos. That's two levels, and each wants a different affordance: you filter at the top level to narrow the feed, and you navigate at the bottom level to read one project's ledger end to end.

Both now exist, and both are wearing the right project's face.

What's New?

Filter the feed by pseudomonorepo. Four chips, plus All:

Entries
All438
AI Labs228
Astro Knots120
Content Farm57
Misc33

Misc is not a dumping ground — it's the standalone repos that never took a parent, and collecting them beats either dropping them or giving each its own chip. Four controls is a filter; eleven is a second navigation.

Every project's own favicon. Not generated monograms — the actual mark, read out of each project's own repo. 20 of 39 streams have one.

Every entry knows where it came from. The origin label on each row links to that project's changelog page, and so does its row in the ledger below. 39 per-repo changelog pages, each with its own strip, its own line-kind split, and its own entries.

How it Works

Getting the marks

There is no favicon convention in this tree, which is the whole difficulty. Repos with a splash page keep the project mark at splash/public/favicon.svg. Plain sites keep it at public/favicon.svg. A couple of nested apps keep it deeper than either. So sync-icons.mjs walks an ordered candidate list and falls back to a scan.

The scan needed a guard, and it needed one immediately. Its first run gave hypernova-site a Water Foundation favicon — TWF trademarks were sitting in its public/trademarks/ directory, left over from other work, and they were the only SVGs present. Wrong brand is worse than no brand.

So an undecorated favicon.svg is taken as the project's own by definition, and a decorated one has to prove it:

JS
// `favicon__LearnStart--Dark.svg` names its project. The TWF files do not
// name hypernova. Tokens under four characters and the word "site" are
// dropped, or every site in the tree would match every other.
const tokens = `${stream.slug} ${stream.title}`
  .toLowerCase().split(/[\s-]+/)
  .filter((t) => t.length >= 4 && t !== "site");

learnstart keeps its mark. hypernova correctly falls through to none.

No JavaScript

The filter is a radio group and :has(). Radio rather than checkbox because these are views of one feed, not additive facets — single-select is what pure-CSS isolation can express honestly, and a multi-select would need real state.

CSS
.feed:has(#g-ai-labs:checked) .rows > :not([data-group="ai-labs"]) { display: none; }

Every entry is already in the DOM, so narrowing is instant, survives a page refresh no worse than any static page, and costs nothing on a cold load.

The feed renders deeper than it shows

Thirty entries is the right unfiltered length. But filtering thirty down to Content Farm leaves three rows, which makes the filter feel broken rather than selective.

So each group contributes its 25 most recent, everything past the first thirty overall is marked data-extra, and those rows are revealed only when a filter is active. One rule, no duplication, no second render pass.

Under the Hood

Marks are designed for their own ground

The first version put a white plate behind every icon — a defensive habit, assuming dark marks on a dark page would vanish. It was exactly wrong. ai-labs and content-farm each ship their own background <rect>, and astro-knots is transparent line-art stroked in indigo, chartreuse and amber. The plate was covering a transparent mark with the one background it was never designed to sit on, and rendering the Astro Knots knot as a white square.

No plate on any of them now. They were already correct.

A group is a tree root, not a taxonomy

groupOf() walks a stream's ancestors to its root and checks it against the three named groups. Nothing is hand-assigned, so a stream re-parented in streams.yaml moves to the right chip on the next build with no second list to keep in sync — which is exactly what happened to lossless-site earlier today when it moved from a standalone root into the Astro Knots subtree.

Chrome you have to fight is chrome you should not use

The filter started as a <fieldset> with a visually-hidden <legend>, which renders a bordered box with an inset label until you override it per browser. It's a <div role="group" aria-label="Filter by project"> now — same semantics for a screen reader, nothing to override.

Files Touched

  • scripts/sync-icons.mjs — collect each project's favicon from its own repo

  • scripts/lib/clones.mjs — repo-to-clone resolution by origin remote, shared with the line sync

  • src/lib/groups.ts — the four buckets, derived from tree roots

  • src/pages/index.astro — the chips and the CSS-only filter

  • src/components/ProjectRow.astro — per-project marks in the ledger

  • public/icons/ — 20 collected favicons

What's Next

  • There is no per-repo filter on the home feed. Repo-level is navigation only — you click through to /stream/<slug>/. Filtering to one repo without leaving the front page is the obvious next rung.

  • 19 of 39 projects have no favicon at all. fullstack-vc ships only favicon.afdesign, an Affinity source file, and needs an SVG export before it can appear anywhere.

  • The share banners exist — ogimage__AI-Labs--Banner.jpg and friends — but at 1200×630 they are the wrong shape for a chip. They would suit a group header or a hover card.

  • Ledger row counts still flip meaning by depth: a root shows its whole subtree, a child shows only itself, so MemoPop AI reads 5 while its own child reads 71.

References

Companion to [[2026-08-18_01]], which covers the line-kind split and the daily commit series shipped alongside this.