Two states crossfading: A — the real Great Blue Heron header (the existing site identity, asset static/images/header.webp from origin/main, embedded here) with the Trans Scend Survival title; B — the constellation as a night star-field statement. Dwell 14s, crossfade 1.4s. Pauses on hover/focus; explicit pause/play control (WCAG 2.2.2); under prefers-reduced-motion the oscillation never starts — heron state static, the toggle becomes a discrete no-animation switch. The masthead constellation is decorative (aria-hidden nodes); the accessible deterministic interaction (R6) is the disclosure section at the page bottom. Node model already carries kind: post | pulse — a future git kind slots in without redesign (the eventual git-activity-as-pulse “statement”), claimed nowhere in beta copy. Current hero’s scroll-fade behavior is retained.
slim — keep avatar/name/tagline/equation/bio/badges + TagCloud; reduce GitHub-activity widgets 5 → 2 (stats + streak); full widget set relocates to /about.
+
Post pages
BlogSidebar
unchanged (out of scope this cycle).
+
+
+
+
▼ The mock — proposed “/” top to bottom
+
+
+
+
mock · proposed “/” · fixture data shaped like the live 140-post / 6-pulse snapshot
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Trans Scend Survival
+
Trans: Latin prefix implying “across” or “beyond” · Scend: originally nautical, the rising motion through a wave · Survival: continuing to live, in spite of.
+
Jess Sullivan — a naturalist-technologist’s log, in one system and two registers
+
+
+
+
+
+ deterministic layout from current category & tag metadata — not an embedding
+
Kept verbatim (current truths): the public-domain dedication, RSS/JSON feeds, licenses link, and the built-with-SvelteKit-&-Skeleton + source line. No copyright/org line is added — the blog has none today and needs none.
+
Adopted from greatfallstoolbus.org: the built from {short-sha} · GitHub-verified provenance line (rendered only when a real build sha is baked in — never on local/static builds), footer nav groups as the home for demoted-from-primary items, and the NOTICE/credits.ts discipline for asset provenance (the heron header currently has no recorded provenance — flagged, must be recorded before ship, not fabricated).
+
Deliberately NOT imported: tinyland.dev's brand statement, address block, or copyleft line — different site, different truths. And zero federation language anywhere on the page (R2/R11: no "federated" claims until TIN-2416's proof packet exists).
+
+
+
Shared invariants unchanged from round 1: badges only where data provides tier · pulse producer order preserved · constellation honestly labeled · Planetarium deferred · type/density hierarchy, no cards · promotion gated on TIN-2891 + R8 + operator review.
A naturalist-technologist's log, in one system and two registers
+
+ The feed has three strata of signal, and they split into two registers.
+ Printed & permanent — the blog: noteworthy (deep reverse-engineering and
+ architecture write-ups, promoted to the top) and less-noteworthy (everyday recipes,
+ release notes, field observations). Live & observed — pulse: Cayuga-basin
+ bird sightings, trail runs, and small life updates, fetched from the broker. Same sample content below,
+ rendered three ways — so you compare layout and motion, not palette, and pick one.
+
+
+
+ Printed & permanent — serif on parchment
+ live & observed — mono on ink-teal
+
+
+
+
+
+
Noteworthy
Deep / reverse-engineering / architecture. Hand-curated to the top.
cardinal · printed
+
+
+
+
Less-noteworthy
Recipes, release notes, casual posts. Published, not promoted.
+ Observatory Ledger
+ SSR-friendly · recommended beta default
+ A printed field-guide page beside a live night rail — motion only where it's genuinely live.
+
Reverse-engineering the Canon G-series service protocol
+
+
The service menu never shipped with documentation. What it did ship with was a
+ six-byte frame header and a two-byte trailer that refused to be a plain sum, an XOR, or any of
+ the usual suspects. It was a CRC — the only real question was which one.
+
+
So I pulled 41,900 frames off the serial bus and did the unsubtle thing: brute-force every
+ 16-bit polynomial until one reproduced every captured trailer. On a laptop that is a long
+ afternoon; across the flywheel's remote executors it is a single parallel forall
+ and a coffee. Chapel makes the parallelism disappear into the language — the same source
+ runs on one core or on the whole cluster.
+
+
+
// G-series service frames: 6-byte header, CRC-16 trailer.// decode pipeline: raw -> frame -> crc16 -> verdict// candidate polys: 0x8005 :: uint(16) |> reflect |> (== trailer)use BlockDist, Time;const frames = readFrames("dump.bin"); // 41,900 captured framesconfig const poly = 0x1021: uint(16); // CCITT guessproccrc16(bytes: [] uint(8), poly: uint(16)): uint(16) {var crc: uint(16) = 0xFFFF;for b in bytes { crc ^= (b: uint(16)) << 8;for0..#8do crc = if (crc & 0x8000) != 0then (crc << 1) ^ poly else crc << 1; }return crc;}// brute-force every polynomial across the flywheel, in parallelvar hits: atomic int;forall p in0x0000..0xFFFF {var ok = true;for f in frames doifcrc16(f.body, p: uint(16)) != f.trailer then { ok = false; break; }+if ok then hits.add(1);// [!code ++]-if ok thenwriteln("match: 0x", p: uint(16));// [!code --]if ok && p >= 0x1000 && p <= 0x9000then// [!code highlight]writeln("plausible service poly => 0x%04xu".format(p));}
+ Rendered with shiki (github-light) — the printed register. On the live
+ site the // [!code ++], [!code --] and highlight comments are consumed by shiki's
+ notation transformers and the lines are tinted; here they are shown faded so the mapping is visible.
+ Ligatures (→, ≠, ≥, ≤, ⇒, ::) render fuller with the bundled ligature mono on the real site.
+
+
+
0x8005 fell out in under a minute — the reflected CRC-16/ARC, not the CCITT
+ polynomial I'd started guessing with. Every trailer in the capture verified, the service frames
+ decoded clean, and the whole search was a page of Chapel that ran wherever the flywheel had cores
+ to spare.
+
+
+
+
+
+
+
+ Ticker + River
+ animated · attention-forward
+ Auto-advancing noteworthy carousel, a live pulse marquee, then a plain river of everyday posts.
+
+
+
Noteworthy — carousel
+
+
+ hover to pause · ~8s auto-advance
+
+
+
+
+
+
+
+
+
Pulse
+
+
+
+
Less-noteworthy — river
+
+
+
+
+
+
+ Planetarium
+ wow · heavier
+ A living constellation. Time = radial distance; plumage-hue + brightness encode the tier.
+
+
+
+
+
+
+
Noteworthy — bright cardinal stars
+
Less-noteworthy — dim heron stars
+
Pulse — goldfinch comets
+
+
+
+
hover a star for its title · click to zoom tag lines = shared topics · center = now, edge = older
+
+
+
+
+
+ Static prototype · all data is sample content · pulse "liveness" is simulated (no broker attached).
+ Self-contained single file — system serif + mono, inline SVG/unicode only, no external requests.
+ Reduced-motion: carousel & ticker freeze, the pulse rail pre-fills without animation, the planetarium renders a static star map.
+
diff --git a/src/lib/stream/blend.test.ts b/src/lib/stream/blend.test.ts
new file mode 100644
index 0000000..bbf4c1f
--- /dev/null
+++ b/src/lib/stream/blend.test.ts
@@ -0,0 +1,114 @@
+import { describe, expect, it } from 'vitest';
+import type { Post } from '$lib/types';
+import type { PublicPulseItem } from '@blog/pulse-core/schema';
+import { blendStream, compareStreamItems, type StreamItem } from './blend';
+
+function makePost(overrides: Partial = {}): Post {
+ return {
+ title: 'A Post',
+ slug: 'a-post',
+ date: '2026-03-04',
+ description: 'desc',
+ tags: [],
+ published: true,
+ ...overrides,
+ };
+}
+
+function makePulse(overrides: Partial = {}): PublicPulseItem {
+ return {
+ id: 'pulse_1',
+ kind: 'note',
+ occurredAt: '2026-03-04T12:00:00.000Z',
+ summary: 'a pulse',
+ content: 'a pulse body',
+ tags: [],
+ ...overrides,
+ } as PublicPulseItem;
+}
+
+describe('blendStream', () => {
+ it('returns an empty timeline for empty inputs', () => {
+ expect(blendStream([], [])).toEqual([]);
+ });
+
+ it('flattens posts and pulses into the unified shape', () => {
+ const items = blendStream([makePost()], [makePulse()]);
+ const post = items.find((i) => i.kind === 'post') as StreamItem;
+ const pulse = items.find((i) => i.kind === 'pulse') as StreamItem;
+
+ expect(post.url).toBe('/blog/a-post');
+ expect(post.id).toBe('post:a-post');
+ expect(pulse.url).toBe('/pulse');
+ expect(pulse.id).toBe('pulse:pulse_1');
+ expect(pulse.pulseKind).toBe('note');
+ });
+
+ it('interleaves posts and pulses in reverse-chronological order', () => {
+ const posts = [
+ makePost({ slug: 'old-post', date: '2026-01-01' }),
+ makePost({ slug: 'new-post', date: '2026-06-01' }),
+ ];
+ const pulses = [
+ makePulse({ id: 'mid-pulse', occurredAt: '2026-03-01T00:00:00.000Z' }),
+ makePulse({ id: 'newest-pulse', occurredAt: '2026-06-30T00:00:00.000Z' }),
+ ];
+
+ const order = blendStream(posts, pulses).map((i) => i.id);
+ expect(order).toEqual(['pulse:newest-pulse', 'post:new-post', 'pulse:mid-pulse', 'post:old-post']);
+ });
+
+ it('floats noteworthy first within an equal date (mirrors the #217 comparator)', () => {
+ // All share the same instant so only the tier weight can reorder them.
+ const at = '2026-04-01T00:00:00.000Z';
+ const items = blendStream(
+ [
+ makePost({ slug: 'plain', date: '2026-04-01' }),
+ makePost({ slug: 'featured', date: '2026-04-01', editorial_tier: 'noteworthy' }),
+ ],
+ [makePulse({ id: 'loud', occurredAt: at, salience: 'noteworthy' })],
+ );
+
+ // The two noteworthy entries come before the untiered post.
+ expect(items[items.length - 1].id).toBe('post:plain');
+ const noteworthyIds = items.slice(0, 2).map((i) => i.id);
+ expect(noteworthyIds).toContain('post:featured');
+ expect(noteworthyIds).toContain('pulse:loud');
+ });
+
+ it('treats less-noteworthy and absent tiers as equal (weight 0)', () => {
+ const items = blendStream(
+ [
+ makePost({ slug: 'zzz', date: '2026-04-01', editorial_tier: 'less-noteworthy' }),
+ makePost({ slug: 'aaa', date: '2026-04-01' }),
+ ],
+ [],
+ );
+ // Neither outranks the other on tier, so the deterministic id tie-break wins.
+ expect(items.map((i) => i.id)).toEqual(['post:aaa', 'post:zzz']);
+ });
+
+ it('is deterministic regardless of input ordering', () => {
+ const posts = [
+ makePost({ slug: 'a', date: '2026-05-01' }),
+ makePost({ slug: 'b', date: '2026-05-01', editorial_tier: 'noteworthy' }),
+ ];
+ const pulses = [
+ makePulse({ id: 'p1', occurredAt: '2026-05-01T00:00:00.000Z' }),
+ makePulse({ id: 'p2', occurredAt: '2026-05-02T00:00:00.000Z', salience: 'noteworthy' }),
+ ];
+
+ const forward = blendStream(posts, pulses).map((i) => i.id);
+ const reversedPosts = blendStream([...posts].reverse(), [...pulses].reverse()).map((i) => i.id);
+ expect(reversedPosts).toEqual(forward);
+ });
+});
+
+describe('compareStreamItems', () => {
+ it('imposes a total order (antisymmetric on the tie-break)', () => {
+ const a: StreamItem = { kind: 'post', id: 'post:a', title: 'a', summary: '', date: '2026-01-01', url: '/blog/a' };
+ const b: StreamItem = { kind: 'post', id: 'post:b', title: 'b', summary: '', date: '2026-01-01', url: '/blog/b' };
+ expect(Math.sign(compareStreamItems(a, b))).toBe(-Math.sign(compareStreamItems(b, a)));
+ expect(compareStreamItems(a, a)).toBe(0);
+ });
+});
diff --git a/src/lib/stream/blend.ts b/src/lib/stream/blend.ts
new file mode 100644
index 0000000..7d4058b
--- /dev/null
+++ b/src/lib/stream/blend.ts
@@ -0,0 +1,90 @@
+// Pure blend/merge for the shadow ingestion "stream": one reverse-chronological,
+// tier-weighted timeline that interleaves long-form blog POSTS with public
+// PULSE items. Kept framework-free so the merge+sort is unit-testable without a
+// DOM, and so /stream/+page.ts can call it at prerender time.
+//
+// Tiers here are reader-weight / IA ONLY (docs/blog-editorial-taxonomy-2026-07-03.md):
+// never authorization, privacy, or ActivityPub-delivery signals. `editorial_tier`
+// (posts) and `salience` (pulses) are the same display union.
+
+import type { Post, PostEditorialTier } from '$lib/types';
+import type { PublicPulseItem } from '@blog/pulse-core/schema';
+
+export type StreamItemKind = 'post' | 'pulse';
+
+/** A single blended timeline entry — a post or a pulse flattened to one shape. */
+export interface StreamItem {
+ readonly kind: StreamItemKind;
+ /** Stable, namespaced id (`post:` / `pulse:`) — also the sort tie-break. */
+ readonly id: string;
+ readonly title: string;
+ readonly summary: string;
+ /** ISO date (post `YYYY-MM-DD`) or ISO datetime (pulse `occurredAt`). */
+ readonly date: string;
+ /** Reader-weight tier: post `editorial_tier` or pulse `salience`. Absent = untiered. */
+ readonly tier?: PostEditorialTier;
+ readonly url: string;
+ /** Present only for pulses, so the reader can show the pulse sub-kind icon. */
+ readonly pulseKind?: PublicPulseItem['kind'];
+}
+
+export function postToStreamItem(post: Post): StreamItem {
+ return {
+ kind: 'post',
+ id: `post:${post.slug}`,
+ title: post.title,
+ summary: post.description ?? post.excerpt ?? post.body_excerpt ?? '',
+ date: post.date,
+ tier: post.editorial_tier,
+ url: `/blog/${post.slug}`,
+ };
+}
+
+export function pulseItemToStreamItem(item: PublicPulseItem): StreamItem {
+ const summary = item.kind === 'bird_sighting' ? (item.birdSighting?.placeLabel ?? '') : item.content;
+ return {
+ kind: 'pulse',
+ id: `pulse:${item.id}`,
+ title: item.summary,
+ summary,
+ date: item.occurredAt,
+ tier: item.salience,
+ url: '/pulse',
+ pulseKind: item.kind,
+ };
+}
+
+// `noteworthy` floats gently up WITHIN an equal date; untiered and
+// less-noteworthy keep weight 0. Mirrors the #217 blog-index comparator so the
+// two surfaces rank consistently. This never influences delivery or policy.
+function noteworthyWeight(tier: PostEditorialTier | undefined): number {
+ return tier === 'noteworthy' ? 1 : 0;
+}
+
+/**
+ * Total order over blended items: date desc primary; within an equal date,
+ * noteworthy first; then a stable id tie-break so the result is fully
+ * deterministic regardless of input order (never relies on sort stability).
+ */
+export function compareStreamItems(a: StreamItem, b: StreamItem): number {
+ const ad = new Date(a.date).getTime();
+ const bd = new Date(b.date).getTime();
+ if (ad !== bd) return bd - ad;
+
+ const weightDiff = noteworthyWeight(b.tier) - noteworthyWeight(a.tier);
+ if (weightDiff !== 0) return weightDiff;
+
+ if (a.id < b.id) return -1;
+ if (a.id > b.id) return 1;
+ return 0;
+}
+
+/**
+ * Merge published posts + public pulses into one reverse-chronological,
+ * tier-weighted timeline. Pure and deterministic: same inputs (in any order)
+ * always yield the same ordering. Handles empty inputs and absent tiers.
+ */
+export function blendStream(posts: readonly Post[], pulseItems: readonly PublicPulseItem[]): StreamItem[] {
+ const items: StreamItem[] = [...posts.map(postToStreamItem), ...pulseItems.map(pulseItemToStreamItem)];
+ return items.sort(compareStreamItems);
+}
From b47be05167a853607e6b197408cfb432dc4d9242 Mon Sep 17 00:00:00 2001
From: Jess Sullivan
Date: Wed, 15 Jul 2026 13:21:47 -0400
Subject: [PATCH 03/16] feat(reader): pure tier-partition + constellation-group
model (TIN-2903)
---
src/lib/reader/ledger.test.ts | 141 +++++++++++++++++++++++++++++++
src/lib/reader/ledger.ts | 152 ++++++++++++++++++++++++++++++++++
2 files changed, 293 insertions(+)
create mode 100644 src/lib/reader/ledger.test.ts
create mode 100644 src/lib/reader/ledger.ts
diff --git a/src/lib/reader/ledger.test.ts b/src/lib/reader/ledger.test.ts
new file mode 100644
index 0000000..7a032cb
--- /dev/null
+++ b/src/lib/reader/ledger.test.ts
@@ -0,0 +1,141 @@
+import { describe, expect, it } from 'vitest';
+import type { Post } from '$lib/types';
+import type { PublicPulseItem } from '@blog/pulse-core/schema';
+import { buildConstellationGroups, partitionLedger } from './ledger';
+
+function makePost(overrides: Partial = {}): Post {
+ return {
+ title: 'A Post',
+ slug: 'a-post',
+ date: '2026-03-04',
+ description: 'desc',
+ tags: [],
+ published: true,
+ ...overrides,
+ };
+}
+
+function makePulse(overrides: Partial = {}): PublicPulseItem {
+ return {
+ id: 'pulse_1',
+ kind: 'note',
+ occurredAt: '2026-03-04T12:00:00.000Z',
+ summary: 'a pulse',
+ content: 'a pulse body',
+ tags: [],
+ ...overrides,
+ } as PublicPulseItem;
+}
+
+describe('partitionLedger', () => {
+ it('partitions strictly on the explicit editorial_tier', () => {
+ const { noteworthy, lessNoteworthy, unclassified } = partitionLedger([
+ makePost({ slug: 'n', editorial_tier: 'noteworthy' }),
+ makePost({ slug: 'l', editorial_tier: 'less-noteworthy' }),
+ makePost({ slug: 'u' }),
+ ]);
+ expect(noteworthy.map((p) => p.slug)).toEqual(['n']);
+ expect(lessNoteworthy.map((p) => p.slug)).toEqual(['l']);
+ expect(unclassified.map((p) => p.slug)).toEqual(['u']);
+ });
+
+ it('never infers a tier from featured/category/tags/length/recency', () => {
+ // Every signal that could tempt inference is present, but no tier is set.
+ const { noteworthy, lessNoteworthy, unclassified } = partitionLedger([
+ makePost({
+ slug: 'loud-but-untiered',
+ featured: true,
+ category: 'hardware',
+ tags: ['a', 'b', 'c'],
+ date: '2030-01-01',
+ description: 'x'.repeat(5000),
+ }),
+ ]);
+ expect(noteworthy).toHaveLength(0);
+ expect(lessNoteworthy).toHaveLength(0);
+ expect(unclassified.map((p) => p.slug)).toEqual(['loud-but-untiered']);
+ });
+
+ it('is deterministic under input reordering (date-desc, slug tie-break)', () => {
+ const posts = [
+ makePost({ slug: 'b', date: '2026-05-01', editorial_tier: 'noteworthy' }),
+ makePost({ slug: 'a', date: '2026-05-01', editorial_tier: 'noteworthy' }),
+ makePost({ slug: 'c', date: '2026-06-01', editorial_tier: 'noteworthy' }),
+ ];
+ const forward = partitionLedger(posts).noteworthy.map((p) => p.slug);
+ const reversed = partitionLedger([...posts].reverse()).noteworthy.map((p) => p.slug);
+ // c is newest; a before b on the slug tie-break within the shared 05-01.
+ expect(forward).toEqual(['c', 'a', 'b']);
+ expect(reversed).toEqual(forward);
+ });
+
+ it('does not silently cap: N in -> N out across the three buckets', () => {
+ const posts = Array.from({ length: 137 }, (_, i) =>
+ makePost({
+ slug: `p${i}`,
+ date: '2026-01-01',
+ editorial_tier: i % 3 === 0 ? 'noteworthy' : i % 3 === 1 ? 'less-noteworthy' : undefined,
+ }),
+ );
+ const { noteworthy, lessNoteworthy, unclassified } = partitionLedger(posts);
+ expect(noteworthy.length + lessNoteworthy.length + unclassified.length).toBe(137);
+ });
+});
+
+describe('buildConstellationGroups', () => {
+ it('groups posts by category then tag, honest basis on each group', () => {
+ const groups = buildConstellationGroups(
+ [
+ makePost({ slug: 'hw', category: 'hardware', tags: ['soldering'] }),
+ makePost({ slug: 'tagged', tags: ['Birding'] }),
+ makePost({ slug: 'bare', tags: [] }),
+ ],
+ [],
+ );
+ const byKey = Object.fromEntries(groups.map((g) => [g.key, g]));
+ expect(byKey.hardware.basis).toBe('category');
+ expect(byKey.birding.basis).toBe('tag');
+ expect(byKey.birding.label).toBe('Birding');
+ expect(byKey.uncategorized.basis).toBe('category');
+ });
+
+ it('is deterministic under post reordering', () => {
+ const posts = [
+ makePost({ slug: 'a', category: 'music', date: '2026-01-01' }),
+ makePost({ slug: 'b', category: 'hardware', date: '2026-02-01' }),
+ makePost({ slug: 'c', category: 'hardware', date: '2026-03-01' }),
+ ];
+ const shape = (ps: Post[]) =>
+ buildConstellationGroups(ps, []).map((g) => `${g.key}:${g.nodes.map((n) => n.id).join(',')}`);
+ expect(shape([...posts].reverse())).toEqual(shape(posts));
+ // hardware sorts before music by key; within hardware, c (newer) before b.
+ expect(shape(posts)).toEqual(['hardware:post:c,post:b', 'music:post:a']);
+ });
+
+ it('preserves pulse producer order and appends the pulse group last', () => {
+ const groups = buildConstellationGroups(
+ [makePost({ slug: 'p', category: 'hardware' })],
+ [makePulse({ id: 'first' }), makePulse({ id: 'second' }), makePulse({ id: 'third' })],
+ );
+ const pulseGroup = groups[groups.length - 1];
+ expect(pulseGroup.key).toBe('pulse');
+ expect(pulseGroup.nodes.map((n) => n.id)).toEqual(['pulse:first', 'pulse:second', 'pulse:third']);
+ });
+
+ it('does not silently cap: every post and pulse becomes exactly one node', () => {
+ const posts = Array.from({ length: 40 }, (_, i) =>
+ makePost({ slug: `p${i}`, category: i % 2 ? 'music' : 'hardware' }),
+ );
+ const pulses = Array.from({ length: 6 }, (_, i) => makePulse({ id: `q${i}` }));
+ const groups = buildConstellationGroups(posts, pulses);
+ const postNodes = groups.flatMap((g) => g.nodes.filter((n) => n.kind === 'post'));
+ const pulseNodes = groups.flatMap((g) => g.nodes.filter((n) => n.kind === 'pulse'));
+ expect(postNodes).toHaveLength(40);
+ expect(pulseNodes).toHaveLength(6);
+ });
+
+ it('omits the pulse group entirely when there are no pulse items', () => {
+ const groups = buildConstellationGroups([makePost({ category: 'hardware' })], []);
+ expect(groups.some((g) => g.basis === 'pulse')).toBe(false);
+ });
+});
diff --git a/src/lib/reader/ledger.ts b/src/lib/reader/ledger.ts
new file mode 100644
index 0000000..d4da97f
--- /dev/null
+++ b/src/lib/reader/ledger.ts
@@ -0,0 +1,152 @@
+// Pure, framework-free model for the "/" Observatory Ledger reader (TIN-2903).
+// This is the ONLY place the reader decides what is "noteworthy", and it does so
+// strictly from the explicit `editorial_tier` frontmatter field. Tier is
+// reader-weight / IA ONLY (docs/blog-editorial-taxonomy-2026-07-03.md): it is
+// NEVER inferred from `featured`, category, tags, length, recency, or author,
+// and it is never an authorization, privacy, or ActivityPub-delivery signal.
+// Kept DOM-free so the partition + grouping stay unit-testable and callable at
+// prerender time.
+
+import type { Post, PostEditorialTier } from '$lib/types';
+import type { PublicPulseItem } from '@blog/pulse-core/schema';
+
+// Node provenance in the constellation model. `pulse` items ride alongside blog
+// `post`s; the union is intentionally open so a future stratum (e.g. a
+// git-pulse or signal-boost node) can join without reshaping callers.
+export type LedgerNodeKind = 'post' | 'pulse';
+
+export interface PartitionedLedger {
+ readonly noteworthy: Post[];
+ readonly lessNoteworthy: Post[];
+ readonly unclassified: Post[];
+}
+
+// Date-desc primary, slug asc tie-break: a total order that does not rely on
+// input order or sort stability, so a reordered input yields identical output.
+function byDateDescThenSlug(a: Post, b: Post): number {
+ const ad = new Date(a.date).getTime();
+ const bd = new Date(b.date).getTime();
+ if (ad !== bd) return bd - ad;
+ if (a.slug < b.slug) return -1;
+ if (a.slug > b.slug) return 1;
+ return 0;
+}
+
+/**
+ * Split posts into the two editorial registers plus an `unclassified` bucket,
+ * strictly on the explicit `editorial_tier`. A post that is `featured` (or in
+ * any category, or heavily tagged) but carries no tier lands in `unclassified`
+ * — never promoted by inference. Every input post appears in exactly one
+ * bucket (N in -> N out); each bucket is date-desc with a slug tie-break.
+ */
+export function partitionLedger(posts: readonly Post[]): PartitionedLedger {
+ const noteworthy: Post[] = [];
+ const lessNoteworthy: Post[] = [];
+ const unclassified: Post[] = [];
+
+ for (const post of posts) {
+ if (post.editorial_tier === 'noteworthy') noteworthy.push(post);
+ else if (post.editorial_tier === 'less-noteworthy') lessNoteworthy.push(post);
+ else unclassified.push(post);
+ }
+
+ noteworthy.sort(byDateDescThenSlug);
+ lessNoteworthy.sort(byDateDescThenSlug);
+ unclassified.sort(byDateDescThenSlug);
+
+ return { noteworthy, lessNoteworthy, unclassified };
+}
+
+// A single point in the constellation — a post or a pulse flattened to one
+// shape the masthead render and the accessible disclosure section can share.
+export interface ConstellationNode {
+ readonly kind: LedgerNodeKind;
+ // Stable, namespaced id (`post:` / `pulse:`).
+ readonly id: string;
+ readonly label: string;
+ readonly href: string;
+ // Reader-weight tier (post `editorial_tier` / pulse `salience`). Absent = untiered.
+ readonly tier?: PostEditorialTier;
+}
+
+export interface ConstellationGroup {
+ // Normalized grouping key (lowercased category / tag, or `pulse`).
+ readonly key: string;
+ readonly label: string;
+ // How this group was formed — honest by construction, never an embedding.
+ readonly basis: 'category' | 'tag' | 'pulse';
+ readonly nodes: ConstellationNode[];
+}
+
+function postToNode(post: Post): ConstellationNode {
+ return {
+ kind: 'post',
+ id: `post:${post.slug}`,
+ label: post.title,
+ href: `/blog/${post.slug}`,
+ tier: post.editorial_tier,
+ };
+}
+
+function pulseToNode(item: PublicPulseItem): ConstellationNode {
+ return {
+ kind: 'pulse',
+ id: `pulse:${item.id}`,
+ label: item.summary,
+ href: '/pulse',
+ tier: item.salience,
+ };
+}
+
+// A post's honest home: its typed `category`, else its first tag, else a shared
+// `uncategorized` group. No embedding, no similarity model — just the metadata
+// already on the post.
+function groupingFor(post: Post): { key: string; label: string; basis: 'category' | 'tag' } {
+ if (post.category) return { key: post.category, label: post.category, basis: 'category' };
+ const firstTag = post.tags.find((t) => t.trim().length > 0);
+ if (firstTag) return { key: firstTag.toLowerCase(), label: firstTag, basis: 'tag' };
+ return { key: 'uncategorized', label: 'uncategorized', basis: 'category' };
+}
+
+/**
+ * Group posts by their existing category/tag metadata, plus a single `pulse`
+ * group for the live snapshot. Deterministic and honest by construction: every
+ * post falls into exactly one group (N in -> N out) sorted date-desc/slug, the
+ * post groups are ordered by key so any input reordering yields identical
+ * output, and the pulse group preserves producer order (never re-ranked). The
+ * pulse group is appended last as a distinct live stratum.
+ */
+export function buildConstellationGroups(
+ posts: readonly Post[],
+ pulseItems: readonly PublicPulseItem[],
+): ConstellationGroup[] {
+ const byKey = new Map();
+
+ for (const post of posts) {
+ const { key, label, basis } = groupingFor(post);
+ const bucket = byKey.get(key);
+ if (bucket) bucket.posts.push(post);
+ else byKey.set(key, { label, basis, posts: [post] });
+ }
+
+ const groups: ConstellationGroup[] = [...byKey.entries()]
+ .sort((a, b) => (a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0))
+ .map(([key, bucket]) => ({
+ key,
+ label: bucket.label,
+ basis: bucket.basis,
+ nodes: [...bucket.posts].sort(byDateDescThenSlug).map(postToNode),
+ }));
+
+ if (pulseItems.length > 0) {
+ groups.push({
+ key: 'pulse',
+ label: 'pulse',
+ basis: 'pulse',
+ // Producer order preserved — pulse is never re-ranked.
+ nodes: pulseItems.map(pulseToNode),
+ });
+ }
+
+ return groups;
+}
From 434e3ffcb6db007af634d9d144e6288e331e2e25 Mon Sep 17 00:00:00 2001
From: Jess Sullivan
Date: Wed, 15 Jul 2026 13:23:07 -0400
Subject: [PATCH 04/16] feat(reader): oscillating heron/constellation masthead
(TIN-2903)
State A is the existing heron hero (verbatim identity, assets, alt, scroll-fade);
state B is a decorative aria-hidden constellation whose motion language (golden-angle
plumage stars, time-rings, tag lines, twinkle, goldfinch pulse comets, slow drift) is
carried from the recovered 2026-07-03 study's planetarium canvas. 14s dwell / 1.4s
crossfade; pause on hover/focus; explicit toggle + pause buttons (aria-pressed);
prefers-reduced-motion holds state A static and never starts the oscillation.
---
.../reader/ObservatoryMasthead.svelte | 439 ++++++++++++++++++
1 file changed, 439 insertions(+)
create mode 100644 src/lib/components/reader/ObservatoryMasthead.svelte
diff --git a/src/lib/components/reader/ObservatoryMasthead.svelte b/src/lib/components/reader/ObservatoryMasthead.svelte
new file mode 100644
index 0000000..b2c8110
--- /dev/null
+++ b/src/lib/components/reader/ObservatoryMasthead.svelte
@@ -0,0 +1,439 @@
+
+
+ (hovered = true)}
+ onmouseleave={() => (hovered = false)}
+ onfocusin={() => (hovered = true)}
+ onfocusout={() => (hovered = false)}
+ aria-label="Trans Scend Survival"
+>
+
+
+
+
+
+
+
+
+
+
+
+
+
Trans Scend Survival
+
+
+ Trans: Latin prefix implying “across” or “Beyond”, often used in gender
+ nonconforming situations
+ —
+ Scend: Archaic word describing a strong “surge” or “wave”, originating
+ with 15th century english sailors
+ —
+ Survival: 15th century english compound word describing an existence only worth transcending
+
+
Jess Sullivan
+
+
+
+
+
+
+ deterministic layout from current category & tag metadata — not an embedding
+
+ Constellation · grouped by current category & tag metadata
+
+
+ The accessible, deterministic view behind the masthead — the same nodes, an honest label, keyboard-native
+ disclosures. Not an embedding, not t-SNE.
+
+
+ {#each groups as group (group.key)}
+
+
+ {group.label}
+ · {group.nodes.length} {group.nodes.length === 1 ? 'entry' : 'entries'}
+
+
+ {#each group.nodes as node (node.id)}
+
diff --git a/src/routes/+page.ts b/src/routes/+page.ts
new file mode 100644
index 0000000..cd427ad
--- /dev/null
+++ b/src/routes/+page.ts
@@ -0,0 +1,21 @@
+import { getPosts } from '$lib/posts';
+import { loadPulseSnapshot } from '$lib/pulse/load';
+import type { PublicPulseItem } from '@blog/pulse-core/schema';
+import type { PageLoad } from './$types';
+
+// SSR-first, fail-soft. Posts come from the build-time static index (always at
+// least as fresh as the deploy). The pulse snapshot is a best-effort enrichment:
+// a missing or invalid snapshot degrades to an empty rail — the reader still
+// renders. Prerendering is inherited from the root +layout.ts.
+export const load: PageLoad = async ({ fetch }) => {
+ const posts = await getPosts();
+
+ let pulseItems: PublicPulseItem[] = [];
+ try {
+ pulseItems = (await loadPulseSnapshot(fetch)).items;
+ } catch {
+ pulseItems = [];
+ }
+
+ return { posts, pulseItems };
+};
From 20bb420f8143b5d969dfaecfe6fd989420677ee3 Mon Sep 17 00:00:00 2001
From: Jess Sullivan
Date: Wed, 15 Jul 2026 13:26:52 -0400
Subject: [PATCH 07/16] feat(reader): reduce nav, add footer More group +
conditional build-sha (TIN-2903)
Retire the Blog nav link (wordmark -> /), demote Signal Boosts and surface /aag +
/consultancy in a footer More group. Suppress the layout hero on / (the masthead
carries identity there) and keep glass-nav tracking scroll. Add a build-from-{sha}
footer line rendered only when a real PUBLIC_BUILD_SHA/GITHUB_SHA is present (wired
via a Vite define), plus a TODO(TIN-2903) for the unrecorded heron asset provenance.
Existing footer lines kept verbatim; mobile drawer theme/mode pickers untouched.
---
src/app.d.ts | 4 +
src/routes/+layout.svelte | 151 +++++++++++++++++++++++++-------------
vite.config.ts | 26 ++++---
3 files changed, 119 insertions(+), 62 deletions(-)
diff --git a/src/app.d.ts b/src/app.d.ts
index da08e6d..136ae16 100644
--- a/src/app.d.ts
+++ b/src/app.d.ts
@@ -1,6 +1,10 @@
// See https://svelte.dev/docs/kit/types#app.d.ts
// for information about these interfaces
declare global {
+ // Public build SHA injected by Vite `define` (see vite.config.ts). Empty
+ // string when no real build sha is present.
+ const __BUILD_SHA__: string;
+
namespace App {
// interface Error {}
// interface Locals {}
diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte
index 451e710..520267f 100644
--- a/src/routes/+layout.svelte
+++ b/src/routes/+layout.svelte
@@ -43,25 +43,44 @@
let bannerNaturalHeight = 0;
let scrolledPastBanner = $state(false);
+ // Primary nav (TIN-2903 ratified reduction): the Blog link is retired — "/" is
+ // now the reader home, reached via the wordmark — and Signal Boosts is demoted
+ // to the footer "More" group. Photography/Music/Making/CV/About are unchanged.
const navLinks = [
- { href: '/blog', label: 'Blog' },
{ href: '/photography', label: 'Photography' },
{ href: '/music', label: 'Music' },
{ href: '/making', label: 'Making' },
- { href: '/signal-boosts', label: 'Signal Boosts' },
{ href: '/cv', label: 'CV' },
{ href: '/about', label: 'About' },
];
+ // Footer "More" group: primary-demoted plus previously-unlinked destinations.
+ const footerMoreLinks = [
+ { href: '/blog', label: 'Full archive (/blog)' },
+ { href: '/signal-boosts', label: 'Signal Boosts' },
+ { href: '/aag', label: 'AAG 2019 poster' },
+ { href: '/consultancy', label: 'Consultancy' },
+ ];
+
+ // Public build SHA, injected at build time via Vite `define` (see
+ // vite.config.ts). Empty unless a real published build sets PUBLIC_BUILD_SHA /
+ // GITHUB_SHA — the footer provenance line renders only when it is present.
+ const buildSha = __BUILD_SHA__;
+ const buildShaShort = buildSha.slice(0, 7);
+
+ const isReaderHome = $derived($page.url.pathname === '/');
+
onMount(() => {
theme.init();
// Scroll-fade for hero banner
if (bannerRef) bannerNaturalHeight = bannerRef.offsetHeight;
const onScroll = () => {
+ // Glass-nav tracks scroll on every route, including "/" where the reader
+ // owns its own masthead and no layout hero banner is present.
+ scrolledPastBanner = window.scrollY > 50;
if (!bannerRef || !bannerNaturalHeight) return;
bannerOpacity = Math.max(0, 1 - window.scrollY / bannerNaturalHeight);
- scrolledPastBanner = window.scrollY > 50;
};
window.addEventListener('scroll', onScroll, { passive: true });
@@ -113,7 +132,7 @@
transscendsurvival.org
@@ -122,21 +141,20 @@
+ {/each}
+ GitHub
@@ -249,43 +267,46 @@
-
-
-
-
-
-
-
-
Trans Scend Survival
-
-
- Trans: Latin prefix implying “across” or “Beyond”, often used in gender
- nonconforming situations
- —
- Scend: Archaic word describing a strong “surge” or “wave”, originating
- with 15th century english sailors
- —
- Survival: 15th century english compound word describing an existence only worth transcending
-
-
Jess Sullivan
-
-
+
+ {#if !isReaderHome}
+
+
+
+
+
+
+
Trans Scend Survival
+
+
+ Trans: Latin prefix implying “across” or “Beyond”, often used in gender
+ nonconforming situations
+ —
+ Scend: Archaic word describing a strong “surge” or “wave”, originating
+ with 15th century english sailors
+ —
+ Survival: 15th century english compound word describing an existence only worth transcending
+
From a2f35aaffe219d243ec1027b7d8f436dfa4af804 Mon Sep 17 00:00:00 2001
From: Jess Sullivan
Date: Thu, 16 Jul 2026 23:14:06 -0400
Subject: [PATCH 12/16] =?UTF-8?q?fix(reader):=20semantic=20honesty=20?=
=?UTF-8?q?=E2=80=94=20real=20h1,=20single=20banner=20landmark,=20machine-?=
=?UTF-8?q?readable=20dates=20(TIN-2903)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Adversarial semantic review findings, all branch-introduced:
- the reader home had no h1 (site identity was a p[role=banner]); the
masthead title is now the document h1, and the illegal role=banner
(nested inside main, duplicating the AppBar's header landmark) is gone
- dates render in