Lossless Changelog

One ledger for everything we ship

Thirty-eight changelogs scattered across thirty-one repositories now read as a single record — and the brand gradient stopped being decoration and started telling you when.

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

One ledger for everything we ship

Why Care?

We have been writing changelogs religiously for eighteen months. Every repo at every level of the tree has a changelog/ directory, and the discipline has held — 392 entries and counting.

The problem is that nobody could read them. They live in thirty-one different GitHub repositories, on branches that disagree about their own names, some of them nested three levels deep inside pseudomonorepos that most people never clone. The momentum was real and completely illegible.

The old lossless-site tried to solve this and picked the wrong shape: three hardcoded content collections, each with its own layout, each fed by markdown copied into the site repo. That was right when there were three projects. There are now thirty-eight. Adding one meant adding a collection, a layout, and a sync.

This is the rebuild, and it starts here because the changelog was the worst of it. A changelog stream is now a configuration entry, not a code change.

What's New?

One feed, thirty-eight streams, three levels deep. Every project's own changelog/ is pulled through the GitHub API and merged into one record — fleet-level narratives from astro-knots and ai-labs sitting alongside the component-level entries from the eleven sites and ten plugins underneath them. Each project page carries an include sub-projects toggle, so you can read ai-labs as twelve fleet narratives or as all 223 entries beneath it.

The gradient tells you when. The Lossless mark is a four-stop ramp — teal, purple, crimson, amber. An aggregate changelog is a time series. So the ramp is now sampled by an entry's position between the oldest and newest ship note: teal is January 2025, amber is today. It is the same colour on the strip, on the sparklines, and on the rule beside every entry in the feed, so the vocabulary only has to be learned once and never needs a legend.

A ledger strip you can read at a glance. One hairline per day we shipped, placed by date, sized by how much landed that day. Density is velocity. Gaps are the quiet weeks. It is the first thing on the page because it is the honest answer to "are these people actually building anything."

Incremental by default. A cold sync is 466 API calls. A warm one is 37 — one per stream, everything else served from cursors. Pushing a single entry adds a single entry.

How it works

The manifest at src/config/streams.yaml declares every stream explicitly: repo, ref, path, parent, shape. Nothing is discovered automatically, and that is the whole trick — it is what keeps rolled-up content out.

Because here is the trap. Every parent in the tree is supposed to roll its children's changelogs into its splash page; that is a feature we built on purpose. Naive aggregation would surface the same entry once from the child and again from every ancestor that copied it. There are 868 such copies sitting in committed splash/src/rollup/ directories right now.

They never land in <repo>/changelog/. They land in <repo>/splash/src/rollup/changelog/<child>/. So a manifest that names changelog/ paths excludes all 868 by construction. Two more layers back that up — a path denylist, and a frontmatter guard that drops anything carrying the from: provenance the rollup scripts inject. A pleasing consequence: this site's own output is stamped the same way, so it can never re-ingest itself.

The parent-level changelogs are not duplicates and they do get ingested. "Sweep: LFM 0.3.0, Astro 6.3.1, and OpenPanel layered across the fleet" is original cross-cutting narrative and among the most valuable writing in the corpus. It is a granularity problem, not a duplication one, which is what the altitude model is for.

Under the Hood

Three things the live API taught us that no amount of reading .gitmodules would have:

Default branch is not where the changelog lives. Six repos 404'd on the first probe. lmstud-yo, grab-reference, plunk-it, filestarter, and file-transporter all default to master, which has no changelog/ at all — every entry is on development. augment-it keeps its 95 entries only on rebuild/turbo-rsbuild, a feature branch that happens to be the default. So ref is a required, explicit field, and the manifest records reality rather than the branch-tier model's intent.

A changelog/ can itself be a submodule. dark-matter is the case: GET /repos/lossless-group/matter-site/contents/changelog returns {"type":"submodule"} and zero files. Its 25 entries live in a standalone repo. Any implementation assuming that endpoint returns an array produces an empty stream and no error — so shape is asserted, and a mismatch fails loud.

Cursors key on stream, not repo. memopop-ai, memopop-native, and memopop-site all read one repository at three different paths. A shared cursor would let the first sync mark the others' commits as already seen, and the symptom would be entries mysteriously ceasing to appear.

Rendering leans on @lossless-group/lfm, including remarkHeadingIds from 0.4.0 — every heading gets a stable, deduped anchor and a copy-link button, so you can point someone at a paragraph rather than a page. Parsing is lenient throughout: twelve entries carry no title and fall back to their first heading, one is not date-named, and a malformed frontmatter block renders raw rather than failing the build. The corpus spans three years of drifting convention and the job is to surface what people actually wrote.

The type does the work the changelog convention asks for. Bodoni Moda for display, Figtree for prose, JetBrains Mono for data — a journal of record at the top of a page dissolving into a terminal by the bottom, which is the "marketing up top, engineering as the narrative unfolds" cascade expressed as a typeface change rather than only a tone change.

What's Next

  • Push-triggered ingest. A single org-level webhook covers all thirty-eight streams and any added later, with a cron sync as the self-healing fallback.

  • The rest of the rebuild. This is surface one. The brand layer extracted here — two-tier tokens, dark by decision rather than omission — is the contract every subsequent surface reads.

  • Historical migration. Several hundred legacy changelog--content and changelog--code entries are still parked in the old site, undecided.

References

  • context-v/specs/Aggregate-Changelog-Streams-Across-the-Lossless-Tree-via-the-GitHub-API.md

  • context-v/specs/Extract-the-Lossless-Brand-Layer-from-lossless-site.md

  • lfm/context-v/Maintain-Heading-Anchors-and-Share-Links.md