russ/code code.fugl.dev
Merge PR #32: the site face and the /latest asks: seven cards on one branch (pr/latest-and-site-face)
merged by Russ T. Fugalopened by Claude Opus 5 (1M context)80 files+9,032 −163f0437ac30 merged here in total
description
Seven kanban items, two threads that share a surface: the site face (#17, #7) and the /latest asks (#45, #46, #47, #48), with #35 riding along as a measured routing defect. Built by a team of agents against ~/.claude/plans/plan-out-implementation-of-dynamic-harp.md, then rebased onto main and re-verified.
bun run check is green on the rebased tree: 80 files, 1251 tests, oxlint --type-aware + typecheck + build --all clean.
What each card became
#35 — trailing slash. html_handling moves to drop-trailing-slash rather than the builders moving. Every internal link the site emits was costing a 307; the builders' slashless output is compared against literal paths in a dozen places (pages.ts's manifest, Layout.tsx's end-matched NavLinks, head.tsx's canonicalUrl) and every one stays correct by not being touched. Not yet verified live — the curl matrix below is owed after deploy.
#17 — the social face. OUTBOUND in components/Outbound.tsx, rendered in header and footer. Deliberately not in NAV: NAV entries are end-matched routes asserted to be paths the Worker will not redirect away, and an absolute URL to another host satisfies neither claim. The header set is hidden md:flex — one outbound link there once cost a 28px overflow at 390px — so the footer is where reachability lives, and the test asserts that rather than trusting the comment. The Bluesky mark is inline SVG on currentColor, because the CSP is img-src 'self' with no data:.
#46 — GFM markdown. remark-parse → remark-gfm → remark-rehype (allowDangerousHtml) → rehype-raw → rehype-sanitize → rehype-stringify, at build time. Rendered HTML rides on RenderedMerge, never on MergedPr, so it stays out of servedMerge()'s hand-written allowlist. A new Prose.tsx sibling to Viewer.tsx carries its own trust doc — Viewer's html is trusted because the owner generated it; a PR body is written by anyone with push access to refs/meta/prs. Markdown renders before the try around pierre, so a repo whose diff fails no longer silently loses its body too (kanban 0008).
#7 — generated OG cards. satori + @resvg/resvg-wasm, 1200×630 per repo plus a site default, in the site's own IBM Plex. Deterministic because satori outlines every glyph to <path> at layout time, so the SVG carries no font reference for resvg to resolve. sharp was rejected for the opposite property: its SVG text goes through librsvg's system-font path and reads different bytes on a different machine. SVG-only was rejected because X, Facebook and LinkedIn do not render SVG cards, so the unfurl would stay broken while every test passed.
#47 — contribution grids. LOG_FORMAT gains %cI, so the per-day series costs zero additional git invocations. Day boundaries are America/Denver, DST-aware, with the zone passed in and never read from the environment — the repo's first timezone handling. commitDays is required on RepoEntry, not optional: the ~13 fixture edits are the point, because a default at the seam is how PR #24 reinstated the exact defect it existed to fix.
#48 — private-PR publication. Tier 4, and the plan explicitly did not build it; built here on the user's instruction. Design C (signed publication bundle) with Design A as its strict prefix. See the caveat at the bottom — as shipped this is Design A wearing Design C's machinery.
Measurements
#45 bundle sizes — apps/web/dist/assets/index-<hash>.js, one chunk, all pages:
| variant | raw | gzip |
|---|---|---|
| baseline (before this branch) | 1,103 B | 0.61 kB |
| shipped (adds diffs-interactive) | 2,595 B | 1.14 kB |
+ @pierre/trees/web-components | 41,644 B | 7.68 kB |
+ @pierre/diffs main entry (tree-shaken) | +40 B | — |
The decision taken was "ship pierre's client bundle." All three asks landed without it, and that narrowing is deliberate rather than quiet. @pierre/trees/web-components costs +6.5 kB gzip and delivers zero folder collapse — its entire content is adoptDeclarativeShadowDom + ensureFileTreeStyles + scrollbar-gutter measurement, and ~39 kB of it is the tree's stylesheet already inlined in the SSR'd shadow root. Pierre's expand handler lives on the ./react path. Folder collapse is instead hand-rolled over the data-item-type, aria-expanded and data-item-parent-path attributes the tree already emits. Prerendered documents already have their shadow roots built by the HTML parser, so even the adoption that module does perform is redundant here.
#7 card sizes — default 44,514 B; russ/code 35,845 B; russ/ap-bio 43,276 B; fugl.dev 34,933 B; russ/ts-template 31,118 B. All 1200×630, IHDR parsed from a real emitted file. Installed footprint ~11.7 MB, no per-platform native binaries.
#47 palette — re-validated against both surfaces with the dataviz ordinal checks: light end 2.20:1 on #EFF1F4, dark end 2.42:1 on #1A1C20, monotone lightness, ΔL ≥ 0.06 between adjacent steps, single hue. The zero bucket is --border, outside the ramp, which is what keeps a zero-commit day distinguishable from both the surface and an absent day.
Findings worth reading
for-each-ref matched with wildmatch in pathname mode. refs/meta/publications/* never crosses /, so refs/meta/publications/org/widget — a ref anyone with push access to the publication repo can create — was invisible to the build entirely. Matched by literal prefix now; every ref in the namespace is seen and non-slugs are refused by name. Not looking and refusing produce the same published set, but only one of them says so.
Four guards on this branch were covered only incidentally, and mutation found every one. slugFromRef's namespace check was fully redundant with a later slug-mismatch check. The slug/ref agreement check was pinned in packages/forge but not at the seam where a slug becomes a path in expected. Suppressing the publication repo's own empty card was asserted nowhere. And publicationRepo was tested at both ends — parsing pins that only the literal true counts, the consumer is tested on a hand-built scan — while every onAdmit case ran with the flag absent, so false was the only value ever observed, which is also what a constant at that seam produces. In all four the code was correct and the evidence was decorative. Reading would not have found any of them.
A publication bundle's file list was sorted with localeCompare, and that sort feeds the signed bytes. Two reviewers on differently-configured machines could rebuild the same merge from the same immutable sha and get different bytes, so a signature made over one verifies against the other's rebuild as tampering. Now compareCodeUnits (main's 632f828, same reasoning one step earlier: localeCompare is not a total order — it returns 0 for unequal pairs and Array#sort then keeps arrival order). The pre-existing order-independence test could not catch it, because src/parse.ts and src/parse.test.ts sort identically under both comparators; the new case uses A.ts and a.ts, which ICU orders opposite to their code units.
A \0 written into a source file in this repo arrived as a raw NUL byte three times. The file becomes binary and grep silently refuses to search it — exiting 1 with no matches, which reads exactly like "not present". An empty grep result is not evidence of absence unless you know the file is text; file -b answers that in one call. Where a test genuinely needs a control character, build it with String.fromCharCode rather than an escape.
renderedCount stopped meaning what it said. Once a diff-failed merge still emits body HTML, a counter commented "repos that got a rendered diff" would have counted rendered prose as success — on precisely the run where pierre broke for every repo. Kept meaning diffs.
Contracts rewritten rather than falsified
when.ts said "nothing on this site adds a node after load". The grid appends a <td> per elapsed day, so that sentence no longer describes the bundle. The paragraph now names the three markers (time[data-when], [data-commit-grid], <file-tree-container>), states that a document carrying none wires up nothing, and re-anchors the modulePreload argument to the gating property the sentence was only ever a description of.
routes.ts's paragraph documenting the 307s as current behaviour, and vite.config.ts's modulePreload comment, were both rewritten against the code that shipped. The latter took three passes: it first described a dynamic import() that was never written, then invented a byte count for machinery that does not exist.
Verification by mutation
Every new guard was broken, watched go red, and restored. Recorded in the commits and the test files. The ones most worth re-running: removing rehype-sanitize (10 of 12 hostile-markdown cases red); removing rehype-raw (raw HTML is dropped rather than sanitized — indistinguishable from outside without the case that pins it); rendering a placeholder for a future grid day (zero-vs-absent); ignoring verifyCommit's result; dropping gpg.ssh.allowedSignersFile; and reverting the bundle file sort.
Still owed
- The live
curl -sImatrix for #35, over/about,/about/,/latest,/latest/russ/ap-bio,/latest/russ/ap-bio/and/. This is a serving-layer change no test can observe, and the previous convention went stale precisely because a serving assumption was never re-checked. Expect 200 on the slashless forms and 307 on the slashed ones — the mirror of today. - A manual pass in
vite previewat 390px and 1440px in both colour schemes. The fixture carries an unbreakable long path deliberately, because that width shipped a sideways scroll once.
The caveat on #48
As shipped this is Design A wearing Design C's machinery. The verification is real code and it runs on every build, but git config --get-regexp 'gpg|commit.gpgsign|user.signingkey' returns nothing at repo and global scope, and ssh-add -l reports no identities. Until a signing key exists that an unattended process cannot use, this is detection after the fact, not prevention. Nothing in the docs, the README or the code comments claims otherwise, and nothing should.
Two fidelity losses in the publication adapter are named in the code rather than hidden: a published review renders as a plain comment (buildRecord flattens comment and review events, and fixing it changes the signed schema), and commitDays is deliberately empty for a publication, because drawing a grid from a bundle's single date either implies the source repo did exactly that much that day or requires disclosing the working cadence of a repository nobody agreed to publish. Both belong with the productUrl follow-up.
Co-Authored-By: Claude Opus 5 (1M context) noreply@anthropic.com
80 files changed
This view needs a browser with declarative shadow DOM: Chrome 111, Safari 16.4, or Firefox 123. Read the source instead.
apps/web/index.html
60 unmodified lines6162636465666764656667686970716972737460 unmodified lines <meta property="og:type" content="website" /> <meta property="og:site_name" content="code.fugl.dev" /> <!-- `summary`, not `summary_large_image`: the large card is a wide image box with a caption under it, and with no og:image it renders as an empty rectangle rather than degrading to the small card. Upgrade this line in the same change that adds the image, never before it. `twitter:card` used to live here as `summary`, back when every page unfurled as the same imageless small card. src/head.tsx emits it now, `summary_large_image` per route alongside `og:image` and `og:image:alt` — the card type has to move with the image it describes, and a copy in both files would be a duplicate the first-wins rule of the parser picks the wrong one from. src/meta.test.ts counts `twitter:card` to zero here for the same reason it counts the other per-page tags. --> <meta name="twitter:card" content="summary" /> <!-- No font <link> here on purpose: the faces are self-hosted and imported by packages/ui/src/theme.css, so they arrive fingerprinted through the sameapps/web/src/components/CommitGrid.test.tsx
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319/** * The grid, the repaint, and the one property they both have to preserve. * * A day that happened with zero commits is a `<td>` in the zero bucket. A * day that has not happened yet is nothing at all. The tests below assert * that at every seam the grid could break at: the initial render, the * screen-reader label per cell, the civil day resolved against * `America/Denver` rather than the ambient environment, and the client-side * repaint that appends elapsed days without inventing future ones. Each * property was verified by mutation — see the commit body for the record. */import { renderToStaticMarkup } from "react-dom/server";import { describe, expect, it } from "vitest";import { addDays, BUILD_COLUMNS, commitCellLabel, GRID_SELECTOR, MAX_COLUMNS, repaintCommitGrids, SITE_ZONE, todayInSiteZone,} from "../grid";import { CommitGrid } from "./CommitGrid";
/** 2026-08-04, 10:00 UTC — a Tuesday, Denver noon-ish, mid-week for tests. */const NOW = new Date("2026-08-04T16:00:00Z");
/** Render a component to a host node the way the prerenderer would. */function rendered(markup: string): HTMLElement { const host = document.createElement("div"); host.innerHTML = markup; return host;}
function page(days: readonly { date: string; count: number }[]): HTMLElement { return rendered( renderToStaticMarkup(<CommitGrid days={days} now={NOW} label="test" />), );}
/** Every past date from `start` through `end`, dense with zeroes. */function densePastZeroes(start: string, end: string) { const days: { date: string; count: number }[] = []; for (let d = start; d <= end; d = addDays(d, 1)) { days.push({ date: d, count: 0 }); } return days;}
describe("SITE_ZONE", () => { it("names America/Denver — the zone commitDays bucketed against", () => { // The pin. `packages/forge/src/git-source.ts` exports the same string, // and the two must agree — a drift shifts the grid's civil day off the // day the data was bucketed on, and every cell silently misaligns. // Hardcoded rather than imported, because @code/forge is Bun-only and // this file's module ships to the browser (see grid.ts's file header). expect(SITE_ZONE).toBe("America/Denver"); });});
describe("todayInSiteZone", () => { it("resolves the civil date in Denver, not UTC, at the day boundary", () => { // 05:00 UTC on 2026-08-07 is 23:00 on 2026-08-06 in Denver (MDT, UTC-6). // A Denver reader is still living in the 6th; UTC has already flipped. // The assertion goes red if the implementation uses `timeZone: "UTC"`. const at = new Date("2026-08-07T05:00:00Z"); expect(todayInSiteZone(at)).toBe("2026-08-06"); });
it("crosses to the next Denver day at 06:00 UTC in August", () => { // Denver is UTC-6 (MDT) in August. 05:59 UTC → 23:59 previous day // Denver; 06:00 UTC → midnight Denver. This pins the zone offset — // a mutation that hardcodes Chicago (UTC-5) crosses one hour too late. expect(todayInSiteZone(new Date("2026-08-07T05:59:00Z"))).toBe( "2026-08-06", ); expect(todayInSiteZone(new Date("2026-08-07T06:00:00Z"))).toBe( "2026-08-07", ); });});
describe("commitCellLabel", () => { it("says 'No commits' for the zero bucket rather than '0 commits'", () => { expect(commitCellLabel(0, "2026-08-04")).toBe("No commits on 2026-08-04"); });
it("uses the singular for exactly one commit", () => { expect(commitCellLabel(1, "2026-08-04")).toBe("1 commit on 2026-08-04"); });
it("pluralises for two and more", () => { expect(commitCellLabel(2, "2026-08-04")).toBe("2 commits on 2026-08-04"); expect(commitCellLabel(12, "2026-08-04")).toBe("12 commits on 2026-08-04"); });});
describe("CommitGrid — render", () => { it("is a real <table> with a caption, not role=img", () => { // Pins the accessibility choice made in CommitGrid.tsx's header. Change // the container to `<div role="img">` and per-cell aria-labels become // invisible to assistive technology — an explicit test rather than a // convention held only by the comment above it. const host = page(densePastZeroes("2026-06-15", "2026-08-04")); const grid = host.querySelector(GRID_SELECTOR); expect(grid?.tagName).toBe("TABLE"); const caption = grid?.querySelector("caption"); expect(caption).not.toBeNull(); expect(caption?.getAttribute("class")).toBe("sr-only"); });
it("writes the census marker so entry-client can gate on it", () => { // The marker's presence is the only thing standing between a page that // wires the growth up and one that does nothing. If this attribute // disappears, the client's repaint returns 0 and installs no handlers — // which is right on a document with no grid, and would be a regression // on one that has one. const host = page(densePastZeroes("2026-06-15", "2026-08-04")); expect(host.querySelectorAll(GRID_SELECTOR)).toHaveLength(1); });
it("renders 8 columns at build time", () => { // Row 0 (Sunday) is the spine: every fully-past week contributes exactly // one Sunday cell. 8 columns is the build-time spec (kanban 0047). const host = page(densePastZeroes("2026-06-01", "2026-08-04")); const rows = host.querySelectorAll("tbody tr"); expect(rows).toHaveLength(7); expect(BUILD_COLUMNS).toBe(8); expect(rows[0]?.querySelectorAll("td")).toHaveLength(8); });
it("gives every past day a cell — including genuine zero-days", () => { // 2026-08-04 is a Tuesday. Every day in the fixture window falls on or // before today, and every one carries count: 0. So every cell in rows // 0..2 (Sun/Mon/Tue) of the last column is present — the "future" // absence only kicks in from Wednesday onward. const host = page(densePastZeroes("2026-06-08", "2026-08-04")); const rows = host.querySelectorAll("tbody tr"); // Rows 0..2 all have BUILD_COLUMNS cells because Sun/Mon/Tue have all // happened this week too. expect(rows[0]?.querySelectorAll("td")).toHaveLength(BUILD_COLUMNS); expect(rows[1]?.querySelectorAll("td")).toHaveLength(BUILD_COLUMNS); expect(rows[2]?.querySelectorAll("td")).toHaveLength(BUILD_COLUMNS); });
// The one that matters most. Rendering a placeholder for the current // week's future cells would tie zero-cells and future-cells to the same // DOM shape, and everything downstream breaks. Two grids — one whose last // week trails into the future, one whose last week is all real zeroes on // a hypothetical "everything already happened" — must not have the same // cell count in that last week. it("omits future-day cells but keeps past zero-day cells", () => { // NOW = Tuesday 2026-08-04, so this week's Sun/Mon/Tue are past and // Wed/Thu/Fri/Sat are future. Rows 3..6 should have one fewer cell // (the last column is absent) than rows 0..2. const host = page(densePastZeroes("2026-06-08", "2026-08-04")); const rows = host.querySelectorAll("tbody tr"); const withoutFuture = rows[0]?.querySelectorAll("td").length ?? 0; const withFuture = rows[3]?.querySelectorAll("td").length ?? 0; expect(withoutFuture - withFuture).toBe(1); });
it("carries every cell's date and count on the DOM node", () => { const host = page([{ date: "2026-08-04", count: 3 }]); const cell = host.querySelector('td[data-date="2026-08-04"]'); expect(cell?.getAttribute("data-count")).toBe("3"); });
it("labels each cell with count + date for screen readers", () => { // The aria-label is the whole reason a wordless matrix is usable for a // reader who cannot see the shading. Drop it and this test reddens; a // reader with a screen reader loses the only signal on the page. const host = page([{ date: "2026-08-04", count: 3 }]); const cell = host.querySelector('td[data-date="2026-08-04"]'); expect(cell?.getAttribute("aria-label")).toBe("3 commits on 2026-08-04"); // The same label doubles as a mouse-hover tooltip. expect(cell?.getAttribute("title")).toBe("3 commits on 2026-08-04"); });
it("takes the neutral rule token for the zero bucket, not the ramp", () => { // The distinction between a genuine zero and an absent day lives half // in the DOM (element present/absent) and half in the paint — a zero // must not use a violet-ramp class or it will look like a shade-1 cell. const host = page([{ date: "2026-08-04", count: 0 }]); const cell = host.querySelector('td[data-date="2026-08-04"]'); expect(cell?.className).toContain("bg-border"); expect(cell?.className).not.toMatch(/bg-grid-shade-/); });
it("shades non-zero counts through the four ramp buckets", () => { const host = page([ { date: "2026-07-31", count: 1 }, { date: "2026-08-01", count: 4 }, { date: "2026-08-02", count: 8 }, { date: "2026-08-03", count: 20 }, ]); expect(host.querySelector('[data-date="2026-07-31"]')?.className).toContain( "bg-grid-shade-1", ); expect(host.querySelector('[data-date="2026-08-01"]')?.className).toContain( "bg-grid-shade-2", ); expect(host.querySelector('[data-date="2026-08-02"]')?.className).toContain( "bg-grid-shade-3", ); expect(host.querySelector('[data-date="2026-08-03"]')?.className).toContain( "bg-grid-shade-4", ); });
it("carries the total count in the caption for the screen reader", () => { const host = page([ { date: "2026-08-03", count: 5 }, { date: "2026-08-04", count: 7 }, ]); const caption = host.querySelector("caption"); expect(caption?.textContent).toContain("12 commits"); expect(caption?.textContent).toContain("test"); });});
describe("repaintCommitGrids", () => { it("reports how many grids it found, so a page with none can idle", () => { // The census. entry-client.ts installs its listeners only when this is // above zero, so a page without a grid loads a script that runs once and // then does nothing at all. Match `./when.ts:repaintWhen` on this. expect( repaintCommitGrids(page([{ date: "2026-08-04", count: 1 }]), NOW), ).toBe(1); expect(repaintCommitGrids(rendered("<p>no grid</p>"), NOW)).toBe(0); });
it("is idempotent at a fixed instant", () => { const host = page(densePastZeroes("2026-06-08", "2026-08-04")); repaintCommitGrids(host, NOW); const once = host.innerHTML; repaintCommitGrids(host, NOW); expect(host.innerHTML).toBe(once); });
it("appends a zero cell for each day elapsed since the build", () => { // Build wrote through 2026-08-04. Read at 2026-08-06 (in Denver) — // two days elapsed, so two zero cells appear at rows 3 (Wed) and 4 (Thu). const host = page(densePastZeroes("2026-06-08", "2026-08-04")); const readAt = new Date("2026-08-06T18:00:00Z"); // Denver noon Aug 6 repaintCommitGrids(host, readAt); const rows = host.querySelectorAll("tbody tr"); // Rows 3 and 4 each gained a cell; rows 5 and 6 (Fri/Sat) did not, // because those days have not happened yet at 2026-08-06. expect( host .querySelector('td[data-date="2026-08-05"]') ?.getAttribute("data-count"), ).toBe("0"); expect( host .querySelector('td[data-date="2026-08-06"]') ?.getAttribute("data-count"), ).toBe("0"); expect(host.querySelector('td[data-date="2026-08-07"]')).toBeNull(); // The rowmath: Sunday count stayed the same (no new week yet), Wed/Thu // each gained one. const sundayCells = rows[0]?.querySelectorAll("td").length ?? 0; const wedCells = rows[3]?.querySelectorAll("td").length ?? 0; expect(wedCells).toBe(sundayCells); });
it("appends a whole new column when a new week starts", () => { // Build wrote through 2026-08-04 (Tue). Read on 2026-08-10 (Mon of the // NEXT week). That is 6 elapsed days, spanning a week boundary. Row 0 // (Sunday) should gain one cell: the 2026-08-09 Sunday. const host = page(densePastZeroes("2026-06-08", "2026-08-04")); const before = host .querySelectorAll("tbody tr")[0] ?.querySelectorAll("td").length; const readAt = new Date("2026-08-10T18:00:00Z"); repaintCommitGrids(host, readAt); const after = host .querySelectorAll("tbody tr")[0] ?.querySelectorAll("td").length; expect(after).toBe((before ?? 0) + 1); expect(host.querySelector('td[data-date="2026-08-09"]')).not.toBeNull(); });
it("slides the window once growth would exceed MAX_COLUMNS", () => { // Simulate a very-long-open tab: build wrote 10 full columns // (2026-06-01 was a Monday, so 10 Sundays ending 2026-08-02 = 2026-05-31). // Actually build at 2026-08-04 with plenty of history → 8 columns. Then // fast-forward the reader's clock 21 days — three new Sundays would push // the total to 11, but MAX_COLUMNS = 10 says the oldest must come off. const host = page(densePastZeroes("2026-05-01", "2026-08-04")); // Snapshot the leftmost Sunday date. const firstSundayBefore = host .querySelector("tbody tr:first-child td[data-date]") ?.getAttribute("data-date"); // Read three weeks later. const readAt = new Date("2026-08-25T18:00:00Z"); repaintCommitGrids(host, readAt); const rows = host.querySelectorAll("tbody tr"); const sundayCount = rows[0]?.querySelectorAll("td").length ?? 0; expect(sundayCount).toBeLessThanOrEqual(MAX_COLUMNS); // The former leftmost Sunday should have been dropped as the window slid. expect( host.querySelector(`td[data-date="${firstSundayBefore ?? ""}"]`), ).toBeNull(); });
it("adds nothing when read strictly before the build's last day", () => { // Sanity check that time can only go forward. A clock earlier than the // rightmost cell is a bug on the reader's side (system clock skew), and // the grid should not react by inventing future cells backwards. const host = page(densePastZeroes("2026-06-08", "2026-08-04")); const before = host.innerHTML; const readAt = new Date("2026-07-01T18:00:00Z"); repaintCommitGrids(host, readAt); expect(host.innerHTML).toBe(before); });});apps/web/src/components/CommitGrid.tsx
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123/** * A GitHub-style contribution heatmap — one cell per civil day, shaded by * commit count, laid out oldest → newest with the current partial week on the * right (kanban 0047). * * ## The shape the design turns on * * A day in the past always has a cell. A day in the future has NO cell — no * element, no placeholder, no zero-shaded stand-in. The two must not look * alike, and the DOM difference is what makes that testable: a past-day cell * is a `<td>` and a future position is nothing at all, so a row's cell count * differs between a week of zeros and a week with trailing future days. * * That is the single load-bearing property of the whole component. It breaks * the moment someone renders a placeholder for the current-week's tail — the * mutation test in `CommitGrid.test.tsx` walks the DOM node count for both * shapes and reddens the moment they agree. * * ## Why a `<table>` with a `<caption>`, not `role="img"` * * Two accessible shapes were considered. `role="img"` on the container with * one summary label would let a screen reader user hear "sitewide commit * activity: 342 commits from 2026-06-15 through 2026-08-06" and move on, * which is what most SR users want from a wordless heatmap. But `role="img"` * hides descendants from assistive technology, so the per-cell `aria-label` * kanban 0047 asks for ("3 commits on 2026-08-04") would be dead weight — * present in the markup, invisible to the tool that would read it. * * A `<table>` gives both channels: a `<caption class="sr-only">` carries the * summary the reader hears first, and per-cell `aria-label` lets a reader who * wants the detail actually navigate to it. `role="img"` is rejected because * this repo's design says cells carry a label, and the container-role choice * has to leave that label reachable. * * ## Which "today" * * `now` is passed in for the same reason `formatWhen` in `../forge-view.ts` * takes one — two reads milliseconds apart cannot straddle midnight and * disagree — and the civil day is computed in `SITE_ZONE` (see `../grid.ts`), * never from the ambient environment. A build machine in Tokyo and a browser * in Berlin both call the current day the same day the git server does. * * ## Growth after load * * The build renders {@link BUILD_COLUMNS} columns. The reader's browser * repaints via `../grid.ts:repaintCommitGrids`, gated on the `data-commit-grid` * marker below (which is why the marker is written here, at the one place * responsible for it — see `./When.tsx` for the same pattern). Every past day * that elapses between build and read gains a zero-bucket cell; new weeks * gain a new column; growth past {@link MAX_COLUMNS} columns slides the * window rather than growing forever. */import type { CommitDay } from "@code/shared/forge";import { BUILD_COLUMNS, buildGridMatrix, CELL_CLASS, commitCellLabel, commitGridSummary, shadeClass, todayInSiteZone,} from "../grid";
export interface CommitGridProps { readonly days: readonly CommitDay[]; /** The page's clock, read once by the page. See file header. */ readonly now: Date; /** * The screen-reader summary that opens the caption — the sentence a reader * hears before any cell. Two use sites: "Sitewide commit activity" on Home, * `<display name> commit activity` on Repo. Never rendered visually. */ readonly label: string; readonly className?: string;}
export function CommitGrid({ days, now, label, className }: CommitGridProps) { const today = todayInSiteZone(now); const matrix = buildGridMatrix(days, today, BUILD_COLUMNS); const summary = commitGridSummary(label, matrix);
const wrapperClass = ["inline-block", className ?? ""] .filter((c) => c !== "") .join(" ");
return ( <table // `data-commit-grid` is the marker `../grid.ts:GRID_SELECTOR` looks for. // Written here, one place, so a page carrying the component wires the // growth in and a page without one does nothing (kanban 0047). data-commit-grid="" className={`${wrapperClass} border-separate border-spacing-[3px]`} > {/* The caption is the summary a screen reader reads before the grid. `sr-only` clips it from sighted rendering — the visual affordance is the heatmap itself — so both channels get an honest read. */} <caption className="sr-only">{summary}</caption> <tbody> {matrix.rows.map((row, dow) => ( // Row index is the key: the matrix is a fixed 7-row structure and // rows are never reordered. // oxlint-disable-next-line react/no-array-index-key <tr key={dow}> {row.map((cell) => cell === null ? null : ( <td // The date is a strong per-cell key — every cell in the // grid has a unique civil date and it does not change. key={cell.date} data-date={cell.date} data-count={cell.count} aria-label={commitCellLabel(cell.count, cell.date)} title={commitCellLabel(cell.count, cell.date)} className={`${CELL_CLASS} ${shadeClass(cell.count)}`} /> ), )} </tr> ))} </tbody> </table> );}apps/web/src/components/Discussion.tsx
14 unmodified lines15161718192021222324252627282930313233343536253738394010 unmodified lines51525354555657585912 unmodified lines7273747576777879808182838460616285868788899091929314 unmodified lines */import type { PrComment } from "@code/shared/forge";import { commentLabel } from "../forge-view";import { Prose } from "./Prose";import { When } from "./When";
export interface DiscussionProps { readonly comments: readonly PrComment[]; /** * Sanitized HTML rendered from `comments[i].body`, index-aligned with * {@link comments}. Empty array when there is no render at all — a repo * whose merge had no `RenderedMerge` entry falls back to plain text. * * The pipeline lives in `packages/viewer/src/markdown.ts` and its output * is asserted inert in `packages/viewer/src/markdown.test.ts`; this * component's trust is stated against that pipeline, not against the * string in the prop. */ readonly bodiesHtml: readonly string[]; readonly now: Date;}
export function Discussion({ comments, now }: DiscussionProps) {export function Discussion({ comments, bodiesHtml, now }: DiscussionProps) { if (comments.length === 0) return null;
return (10 unmodified lines // choice. `PrComment` carries no id, and `at`-`actor` is not a // reliable substitute for one, so index is the honest key rather // than a composite that merely looks less like one. // // The same index reads out of `bodiesHtml` — the two arrays are // built together by publish.ts and stay aligned. // oxlint-disable-next-line react/no-array-index-key <li key={index}12 unmodified lines className="text-muted-foreground" /> </p> {/* Rendered from markdown at build time (kanban 0046). Where `bodiesHtml[index]` is missing — a `RenderedMerge` entry the build did not produce for this repo — fall back to the same `<p whitespace-pre-wrap>` shape the whole page shipped as before. That is the state a fixture without a paired render produces, and the fallback is what keeps a broken render from silently emptying the discussion. */} {bodiesHtml[index] === undefined ? ( <p className="mt-1.5 max-w-2xl text-sm text-pretty whitespace-pre-wrap"> {comment.body} </p> <p className="mt-1.5 max-w-2xl text-sm text-pretty whitespace-pre-wrap"> {comment.body} </p> ) : ( <Prose className="mt-1.5 text-sm" html={bodiesHtml[index]} /> )} </li> ))} </ol>apps/web/src/components/Outbound.tsx
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116/** * The links that leave this site for a person rather than for a server. * * `git.fugl.dev` is not in this set. It is already spelled in the shell twice — * bare in the header, and inside the footer's "a static read of git.fugl.dev", * which is the site's claim about itself and not a link in a list. These two * are a different kind of thing: where the work is written up, and who wrote * it. * * They are deliberately **not** in `NAV` (../pages/Layout.tsx). `NAV` is the * set of internal targets, every one `end`-matched against the current location * and asserted to be a path the Worker will not redirect away; an absolute URL * to another host satisfies neither claim, and adding one there would make both * assertions vacuous rather than merely inapplicable. * * The set is exported so Layout.test.tsx can hold both properties at once — * that every one of these is reachable from the footer of every page at every * width, and that none of them has quietly become a nav item. * * ## Why the footer is the one that has to work * * The header set is hidden below `md`. At 360–390px the bar is already full * with the wordmark and three nav items, and a *single* outbound link there * was what pushed the header 28px past the viewport and made the whole page * scroll sideways. The footer wraps and carries the set at every width, so the * reachability guarantee lives there and the header is decoration. */
/** One link off the site. `mark` names the vector art that precedes the label. */export interface OutboundLink { readonly href: string; readonly label: string; readonly mark?: "bluesky";}
/** * The work, then the person. * * `russ.fugl.dev` is an atproto handle rather than a web page — it resolves to * a DID and redirects to the Bluesky profile behind it. Spelled as the handle * rather than as `bsky.app/profile/…` because the handle is the durable name * and the app hosting it is not. */export const OUTBOUND: readonly OutboundLink[] = [ { href: "https://smart-knowledge-systems.com/portfolio?tab=coding", label: "portfolio", }, { href: "https://russ.fugl.dev", label: "russ.fugl.dev", mark: "bluesky" },];
/** * The Bluesky butterfly, inline. * * Inline rather than an `<img>`, and that is a constraint rather than a * preference: the Worker's CSP is `img-src 'self'` with no `data:` * (`CONTENT_SECURITY_POLICY` in worker/index.ts, pinned by worker/index.test.ts), * so a `data:` URI mark would be blocked outright and a file would be a second * request for half a kilobyte. `currentColor` also means it inherits the link's * hover and focus colour for free, which a raster mark could not. * * `aria-hidden`, and never alone: the mark always sits beside the real text * label, so nothing about where the link goes is carried only by the picture. */function BlueskyMark() { return ( <svg viewBox="0 0 568 501" className="inline-block h-[0.95em] w-[0.95em] align-[-0.14em]" fill="currentColor" aria-hidden="true" focusable="false" > <path d="M123.121 33.6637C188.241 82.5526 258.281 181.681 284 234.873C309.719 181.681 379.759 82.5526 444.879 33.6637C491.869 -1.61183 568 -28.9064 568 57.9464C568 75.2916 558.055 203.659 552.222 224.501C531.947 296.954 458.067 315.434 392.347 304.249C507.222 323.8 536.444 388.56 473.333 453.32C353.473 576.312 301.061 422.461 287.631 383.039C285.169 375.812 284.017 372.431 284 375.306C283.983 372.431 282.831 375.812 280.369 383.039C266.939 422.461 214.527 576.312 94.6667 453.32C31.5556 388.56 60.7778 323.8 175.653 304.249C109.933 315.434 36.0535 296.954 15.7778 224.501C9.94525 203.659 0 75.2916 0 57.9464C0 -28.9064 76.1311 -1.61183 123.121 33.6637Z" /> </svg> );}
/** * The set, as a row of links. * * `underline` is asked for by the footer and withheld by the header: the footer * is prose and its links are underlined ink like every other link on the site, * while the header bar distinguishes its links by position, and a row of * underlines across the top of every page is a lot of rule for a shell that is * otherwise one hairline. */export function OutboundLinks({ underline = false, className = "",}: { readonly underline?: boolean; readonly className?: string;}) { return ( <span className={`flex flex-wrap items-center gap-x-4 gap-y-1 font-mono ${className}`} > {OUTBOUND.map((link) => ( <a key={link.href} href={link.href} className={ underline ? "hover:text-foreground inline-flex items-center gap-1.5 underline decoration-1 underline-offset-[0.22em]" : "text-muted-foreground hover:text-foreground inline-flex items-center gap-1.5" } > {link.mark === "bluesky" ? <BlueskyMark /> : null} {link.label} <span aria-hidden="true">↗</span> </a> ))} </span> );}apps/web/src/components/Prose.test.tsx
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950/** * The frame around `renderMarkdown`'s output. The component itself is thin — * an `<div class="prose">` with `dangerouslySetInnerHTML` — so this file * exists for the seam it names, not for the code it exercises. * * Two things it pins: * * 1. The `html` prop reaches the DOM unmodified, so a sanitize regression in * `@code/viewer` is not silently patched here. If a future edit ever wraps, * escapes, or transforms `html` on the way in, this test says so. * 2. The wrapper carries the `prose` class name, which is what * `packages/ui/src/theme.css`'s rules key off. A rename on either side * breaks this test rather than showing up as a page that renders as * unstyled HTML. */import { renderToStaticMarkup } from "react-dom/server";import { describe, expect, it } from "vitest";import { Prose } from "./Prose";
describe("Prose", () => { it("injects `html` verbatim inside a `.prose` wrapper", () => { const markup = renderToStaticMarkup( <Prose html="<h2>Heading</h2><p>Body with <strong>bold</strong>.</p>" />, ); // The wrapper class is the hook every prose style in theme.css binds to. expect(markup).toContain('class="prose'); // The exact HTML lands inside — nothing between `renderMarkdown` and the // page mutates the string. expect(markup).toContain("<h2>Heading</h2>"); expect(markup).toContain("<strong>bold</strong>"); });
it("passes an extra className through, so callers can add spacing utilities", () => { const markup = renderToStaticMarkup( <Prose html="<p>x</p>" className="mt-1.5 text-sm" />, ); expect(markup).toContain("mt-1.5"); expect(markup).toContain("text-sm"); expect(markup).toContain("<p>x</p>"); });
it("renders nothing visible for an empty string, but still emits the wrapper", () => { const markup = renderToStaticMarkup(<Prose html="" />); // The `.prose` wrapper is unconditional; a caller decides visibility by // gating on the source at their level (e.g. Repo.tsx's `hasDescription`). expect(markup).toContain('class="prose'); // Nothing between the div's open and close but attribute markup. expect(markup).toMatch(/<div[^>]*><\/div>/); });});apps/web/src/components/Prose.tsx
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647/** * A prose region — what a person wrote, rendered from markdown at build time. * * Sibling to `./Viewer`. Where `Viewer` frames a large scroll region (a diff * or a tree) whose content pierre owns end-to-end, `Prose` frames a paragraph * of writing whose HTML `packages/viewer`'s `renderMarkdown` produced from PR * text that anyone with push access to `refs/meta/prs` may have written. * * ## Trust * * The `html` prop is trusted **only** in the sense that * `packages/viewer/src/markdown.ts` promises to hand back sanitized output: * no `<script>`, no `<img>`, no `<input>`, no `on*=` attributes, no * `javascript:` / `data:` / `vbscript:` URLs, no `<style>` block that can * reach out. `packages/viewer/src/markdown.test.ts` walks a hostile fixture * through `renderMarkdown` and asserts the output inert with the same * `expectInert` helper `./Viewer`'s content is judged by. The trust is in the * pipeline, not in the string. * * The string itself is visitor-influenced. It must never be built from * anything but `renderMarkdown`'s output — hand-authored HTML, string * concatenation into markup, or a different renderer would bypass the * pipeline this component's trust is stated against. * * ## Styling * * Uses the `prose` class name convention Tailwind Typography follows but the * styles are **not** from that plugin — this repo does not depend on it, and * `packages/ui/src/theme.css` already carries the sans-vs-mono rule that * governs "what a person wrote" (line 207-214 there). The class is a hook a * caller can style; the built-in wrapper here supplies only the container * width and text-wrap behavior that every prose block on this site shares. */export interface ProseProps { /** Sanitized HTML from `packages/viewer`'s `renderMarkdown`. Trusted; see above. */ readonly html: string; readonly className?: string;}
export function Prose({ html, className = "" }: ProseProps) { return ( <div className={`prose max-w-2xl text-pretty ${className}`} dangerouslySetInnerHTML={{ __html: html }} /> );}apps/web/src/diffs-interactive.test.ts
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314/** * The interactive layer for /latest/<repo>, tested against real pierre output. * * The fixtures come from `@code/viewer`'s own renderers rather than from a * hand-built DOM. That is what makes this a test of the seam between the SSR'd * markup and the client handler, not a test of hand-agreement between two * strings — a rename of `data-item-path` upstream would fail here rather than * only where the client uses it. * * The fixture used for the merge is `merge-pr-12.patch` from the viewer * package. It carries both properties per-load-bearing addendum rule 4: * a flattened directory chain (`apps/web/src` collapses three segments into * one row) and rows both above and below the flattening. The file-becomes- * directory case (a dropped-from-tree path) is exercised in its own describe * block below. */import { readFileSync } from "node:fs";import { join } from "node:path";import { renderFileTree, renderPatch, warmHighlighter } from "@code/viewer";import { afterEach, beforeAll, beforeEach, describe, expect, it, vi,} from "vitest";import { installInteractiveDiffs } from "./diffs-interactive";
/** Paths of the merge fixture, in patch order. Kept in sync by the seam test. */const PATHS = [ "apps/web/src/install/platform.ts", "apps/web/src/main.tsx", "apps/web/src/pages/Layout.tsx",] as const;
const patch = (name: string): string => readFileSync( join( import.meta.dirname, "../../../packages/viewer/src/__fixtures__", name, ), "utf8", );
/** * Build the SSR document the interactive layer runs against. * * `document.body.innerHTML = ...` does not activate declarative shadow DOM — * innerHTML never does — so we manually adopt each `<template * shadowrootmode="open">` into a real shadow root on its host, which is what * the initial-parse of a served document already does automatically. Testing * the interactive layer without that setup would be testing it against an * inert `<template>`, which is the whole failure the fallback paragraph * exists to make readable rather than to make testable. */async function mountFixture(fixtureName: string): Promise<{ readonly treeHost: HTMLElement; readonly diffHost: HTMLElement;}> { const { html: diffHtml, files } = await renderPatch(patch(fixtureName)); const paths = files.map((file) => file.path); const { html: treeHtml } = renderFileTree(paths);
document.body.innerHTML = `<main>${treeHtml}${diffHtml}</main>`;
for (const host of document.querySelectorAll<HTMLElement>( "file-tree-container, diffs-container", )) { const template = host.querySelector<HTMLTemplateElement>( 'template[shadowrootmode="open"]', ); if (template === null) continue; const shadow = host.shadowRoot ?? host.attachShadow({ mode: "open" }); shadow.append(template.content.cloneNode(true)); template.remove(); }
const treeHost = document.querySelector<HTMLElement>("file-tree-container"); const diffHost = document.querySelector<HTMLElement>("diffs-container"); if (treeHost === null) throw new Error("no tree host after mount"); if (diffHost === null) throw new Error("no diff host after mount"); return { treeHost, diffHost };}
/** Find a tree button by its exact path. */function treeButton(host: HTMLElement, path: string): HTMLElement | null { const shadow = host.shadowRoot; if (shadow === null) return null; return shadow.querySelector<HTMLElement>(`[data-item-path="${path}"]`);}
/** Find a diff wrapper by its exact path. */function diffWrapper(host: HTMLElement, path: string): HTMLElement | null { const shadow = host.shadowRoot; if (shadow === null) return null; return shadow.querySelector<HTMLElement>(`[data-viewer-file-path="${path}"]`);}
/** Every visible file-row path, in DOM order. */function visibleFilePaths(host: HTMLElement): readonly string[] { const shadow = host.shadowRoot; if (shadow === null) return []; const rows = shadow.querySelectorAll<HTMLElement>( '[data-item-type="file"][data-item-path]', ); return [...rows] .filter((row) => !row.hidden) .map((row) => row.getAttribute("data-item-path") ?? "");}
// Pre-warm Shiki before any renderPatch call. Cold-render takes ~4 seconds// on a first run in a process, longer than vitest's default timeout, and// this is the test file where "the first render costs Shiki setup" is not// the property under test — the viewer package's own suite has that one.beforeAll(async () => { await warmHighlighter();}, 30_000);
/** * The shared scrollIntoView spy. Reassigned each test through * {@link beforeEach} so `toHaveBeenCalled` is asked about the spy for the * current test, and never about `Element.prototype` itself (an unbound * method reference — the compiler flags it, and rightly so). */let scrollSpy: ReturnType<typeof vi.fn<Element["scrollIntoView"]>>;
beforeEach(() => { // scrollIntoView is not implemented in happy-dom. Install a fresh spy // each test so the reveal-file path has something to call, and so a test // can assert it was called with the right options rather than that a // browser API happened to be present. scrollSpy = vi.fn<Element["scrollIntoView"]>(); Element.prototype.scrollIntoView = scrollSpy;});
afterEach(() => { document.body.innerHTML = ""; vi.restoreAllMocks();});
describe("installInteractiveDiffs", () => { it("returns 0 and does nothing when no tree is on the page", () => { document.body.innerHTML = "<main><p>no tree here</p></main>"; expect(installInteractiveDiffs(document)).toBe(0); });
it("wires one tree per document", async () => { await mountFixture("merge-pr-12.patch"); expect(installInteractiveDiffs(document)).toBe(1); }, 30_000);});
describe("clicking a file row", () => { it("opens the file's diff and scrolls to it", async () => { const { treeHost, diffHost } = await mountFixture("merge-pr-12.patch"); installInteractiveDiffs(document);
const path = PATHS[1]; const wrapper = diffWrapper(diffHost, path); if (!(wrapper instanceof HTMLDetailsElement)) { throw new Error(`no <details> wrapper for ${path}`); } // Collapse it first, so the reveal has to open it. wrapper.open = false;
const button = treeButton(treeHost, path); expect(button).not.toBeNull(); button?.click();
expect(wrapper.open).toBe(true); // scrollIntoView is called with {block:"start", behavior:"smooth"} — the // options matter for the reading experience but the presence of the call // is what makes "the click was routed to the right element" a fact. expect(scrollSpy).toHaveBeenCalledTimes(1); expect(scrollSpy).toHaveBeenCalledWith({ block: "start", behavior: "smooth", }); });
it("cannot cross the shadow boundary by querying the light DOM", async () => { // Load-bearing mutation for the shadow-boundary contract: replacing // `diffShadow.querySelector` with `document.querySelector` would look for // `[data-viewer-file-path]` in the light DOM, find nothing (the wrapper // lives inside the diff shadow root), and silently fail to scroll. // // Rather than mutate the module, verify the invariant: nothing with // `data-viewer-file-path` exists at document scope, so a client that // failed to cross the boundary would degrade to a no-op. That is worse // than what this module does, and the reason to test it is that a future // refactor may inadvertently make it worse in this direction. await mountFixture("merge-pr-12.patch"); expect(document.querySelectorAll("[data-viewer-file-path]")).toHaveLength( 0, ); // The wrappers are only reachable through the shadow root. const diffHost = document.querySelector<HTMLElement>("diffs-container"); expect( diffHost?.shadowRoot?.querySelectorAll("[data-viewer-file-path]"), ).toHaveLength(PATHS.length); });});
describe("clicking a folder row", () => { it("collapses the folder, hiding every descendant row", async () => { const { treeHost } = await mountFixture("merge-pr-12.patch"); installInteractiveDiffs(document);
// Every file row visible before the click. Order is pierre's — its tree // sorts directories before files within a level, so `install/` and // `pages/` come before `main.tsx`, and the leaves under those folders // appear inside their parent's subtree in DOM order. expect(visibleFilePaths(treeHost)).toEqual([ "apps/web/src/install/platform.ts", "apps/web/src/pages/Layout.tsx", "apps/web/src/main.tsx", ]);
// Collapse the flattened top-level folder. The row's path is the full // flattened chain — pierre renders `apps / web / src` as one button // with `data-item-path="apps/web/src/"` — which is exactly the shape // our visibility walk expects. const flattenedRoot = treeButton(treeHost, "apps/web/src/"); if (flattenedRoot === null) { throw new Error("no flattened root row"); } expect(flattenedRoot.getAttribute("aria-expanded")).toBe("true"); flattenedRoot.click();
expect(flattenedRoot.getAttribute("aria-expanded")).toBe("false"); expect(visibleFilePaths(treeHost)).toEqual([]);
// Expand again: everything returns. flattenedRoot.click(); expect(flattenedRoot.getAttribute("aria-expanded")).toBe("true"); expect(visibleFilePaths(treeHost)).toEqual([ "apps/web/src/install/platform.ts", "apps/web/src/pages/Layout.tsx", "apps/web/src/main.tsx", ]); });
it("keeps a nested folder collapsed when its parent is reopened", async () => { const { treeHost } = await mountFixture("merge-pr-12.patch"); installInteractiveDiffs(document);
const pages = treeButton(treeHost, "apps/web/src/pages/"); const root = treeButton(treeHost, "apps/web/src/"); if (pages === null || root === null) { throw new Error("no folder rows to click"); }
// Collapse `pages/` (hides Layout.tsx), then collapse and expand the // root. `pages/` should stay collapsed — an ancestor being open does // not re-open every descendant folder, only the ones the reader had // open when they hid it. pages.click(); expect(visibleFilePaths(treeHost)).toEqual([ "apps/web/src/install/platform.ts", "apps/web/src/main.tsx", ]); root.click(); root.click(); expect(pages.getAttribute("aria-expanded")).toBe("false"); expect(visibleFilePaths(treeHost)).toEqual([ "apps/web/src/install/platform.ts", "apps/web/src/main.tsx", ]); });});
describe("a file the tree dropped", () => { // `file-becomes-directory.patch` is git's output for a commit that replaces // an extensionless `docs` file with a directory of the same name. Pierre's // path-store rejects the pair, so `partitionPaths` in // `../../../packages/viewer/src/tree.ts` drops the file and keeps the // directory — the file has a diff wrapper but no tree row. // // Per addendum rule 4: at least one fixture carries the value under test. // This is the fixture for "dropped from tree", and it fails deliberately // in the direction "the diff wrapper is unreachable from the tree" — // which is the honest degrade the module was designed for. it("still ships the diff wrapper — no click target, no dead link", async () => { const { treeHost, diffHost } = await mountFixture( "file-becomes-directory.patch", ); installInteractiveDiffs(document);
// The file's diff wrapper is present in the diff shadow root. const dropped = diffWrapper(diffHost, "docs"); expect(dropped).not.toBeNull(); expect(dropped).toBeInstanceOf(HTMLDetailsElement);
// But the tree carries no button that would click through to it — the // row is filtered by `partitionPaths` before the tree is rendered. const treeShadow = treeHost.shadowRoot; const droppedInTree = treeShadow?.querySelector( '[data-item-path="docs"][data-item-type="file"]', ); expect(droppedInTree ?? null).toBeNull();
// Clicking the directory row does not scroll to `docs`. There is no // path-mismatch bug that would let a click on the directory land on // the file's diff. const directory = treeShadow?.querySelector<HTMLElement>( '[data-item-path="docs/"]', ); directory?.click(); expect(scrollSpy).not.toHaveBeenCalled(); });});apps/web/src/diffs-interactive.ts
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248/** * The interactive layer for the diff panel on /latest/<repo>: click a tree * folder to collapse it, click a file to scroll the patch to its diff, and let * the browser's own `<details>` handle per-file collapse. * * ## Why script, and only here * * `packages/viewer` renders two separate declarative shadow roots — one * `<file-tree-container>` for the tree and one `<diffs-container>` for the * patch — and neither can reach the other. A light-DOM `#fragment` link cannot * name an element inside a shadow tree, page CSS cannot select across the * boundary, and folder-collapse in the tree needs a click handler that pierre * itself ships only on its hydrated path. So the three interactions this * module owns are not achievable without a small amount of JS, and this is * where that JS lives. * * The per-file collapse is native `<details open>` — see `fileOpen` in * `@code/viewer`. Nothing here implements it. What this module adds on top is * opening a collapsed `<details>` when a tree click scrolls to it, so a reader * who closed a file earlier and then clicked its filename does not scroll to * a summary showing nothing. * * ## What this module does NOT ship * * `@pierre/trees/web-components` is 39 KB of duplicate CSS (`style_default` is * the tree's whole stylesheet, already inlined once in the SSR'd shadow root) * plus a `connectedCallback` that only matters when the shadow root was built * via `.innerHTML` — which the initial-parse of a prerendered document does * not do. Pierre's `.` entrypoint pulls in Preact and the controller, and is * heavier still. This module handles the three asks in a few hundred bytes * because the DOM is already there — every row, every folder, every diff — and * the only thing left to do is toggle attributes and set `.open`. * * ## The tree row model * * Every row in the tree is a `<button role="treeitem">` with * `data-item-path="…"` (files) or `data-item-path="…/"` (directories) and, on * anything but a top-level row, `data-item-parent-path="…/"`. Folders also * carry `aria-expanded`. That is the whole surface: this module toggles * `aria-expanded` on click, walks the parent chain to recompute row * visibility, and pairs a file click with its diff by `data-viewer-file-path`. * * Pierre flattens a single-child directory chain into one row — * `apps/web/src/` is one button, not three — and the flattened row's * `data-item-path` is the full path (`apps/web/src/`). That is the same shape * every other row uses, so the parent-chain walk needs no special case. * * A file that `partitionPaths` in `../../../packages/viewer/src/tree.ts` * dropped from the tree — the file half of a name collision, `docs` when * `docs/index.md` also exists — has no row here at all. There is nothing to * click and no way to reach it from the tree, and this is a deliberate degrade * to "no link" rather than a dead one. The diff still has its `<details>`, so * a reader scrolls to it the way they do without a tree. */
/** * A tree row's key attributes, as we walk the DOM. Read once per install call * and cached in a `Map<path, HTMLElement>` so the parent-chain walk in * `updateRowVisibility` is O(depth) rather than O(rows × depth). */interface TreeRow { readonly element: HTMLElement; readonly path: string; readonly parentPath: string | null; readonly kind: "file" | "folder";}
/** * Install the interactive layer, and return how many trees were wired up. * * `entry-client` calls this when a `file-tree-container` is present in the * document. The count matters for the same reason `repaintWhen` returns one: * a page with no tree has nothing to wire, and the caller uses the count to * decide whether the module did any work. */export function installInteractiveDiffs(root: ParentNode): number { const trees = root.querySelectorAll("file-tree-container"); let installed = 0; for (const tree of trees) { if (!(tree instanceof HTMLElement)) continue; const shadow = tree.shadowRoot; if (shadow === null) continue;
// The diff pane is not guaranteed to be a sibling of the tree, and there // is no cross-reference from the tree element to it. Look it up under // `root` — the same document — and let it be null if the render omitted // the diff (a repo pierre could not render still shows a tree if there is // any path list to draw). A tree click still collapses folders in that // case; only the scroll-to-file becomes a no-op. const diff = root.querySelector("diffs-container"); const diffShadow = diff instanceof HTMLElement ? diff.shadowRoot : null;
installTreeHandlers(shadow, diffShadow); installed += 1; } return installed;}
/** * Attach the click handler to a tree's shadow root. * * One listener for both interactions rather than two, because click targets * are disjoint by `data-item-type` and doubling the listeners would double the * `closest()` walks per click. */function installTreeHandlers( treeShadow: ShadowRoot, diffShadow: ShadowRoot | null,): void { const rows = collectRows(treeShadow);
treeShadow.addEventListener("click", (event) => { const target = event.target; if (!(target instanceof Element)) return; // `[data-type="item"]` is pierre's row anchor and it is on the `<button>`. // `closest` because the click can land on an inner icon or label. const button = target.closest<HTMLElement>('[data-type="item"]'); if (button === null) return;
const kind = button.getAttribute("data-item-type"); if (kind === "folder") { toggleFolder(button, rows); } else if (kind === "file" && diffShadow !== null) { revealFile(button, diffShadow); } });}
/** * Walk the tree's shadow root once and index every row by its path. * * The index is stable — the tree does not add or remove rows after render, so * the map built here stays correct for the life of the page. */function collectRows(treeShadow: ShadowRoot): ReadonlyMap<string, TreeRow> { const rows = new Map<string, TreeRow>(); const buttons = treeShadow.querySelectorAll<HTMLElement>( '[data-type="item"][data-item-path]', ); for (const element of buttons) { const path = element.getAttribute("data-item-path"); if (path === null) continue; const kind = element.getAttribute("data-item-type"); if (kind !== "file" && kind !== "folder") continue; rows.set(path, { element, path, parentPath: element.getAttribute("data-item-parent-path"), kind, }); } return rows;}
/** * Flip a folder row's expanded state and hide or reveal its descendants. * * `aria-expanded` on the button is the state — nothing else stores it — so * the visibility walk in `applyVisibility` reads from these attributes rather * than from a parallel Set. The state and the pixels stay in step because * they are one source. */function toggleFolder( button: HTMLElement, rows: ReadonlyMap<string, TreeRow>,): void { const expanded = button.getAttribute("aria-expanded") === "true"; button.setAttribute("aria-expanded", expanded ? "false" : "true"); applyVisibility(rows);}
/** * Recompute the hidden state of every row from its ancestor chain. * * O(rows × depth) with an indexed lookup per hop, and depth is bounded by the * git tree — call it small. A row is visible iff every ancestor folder in its * chain is `aria-expanded="true"`, which is the definition of "the tree is * open above me". Recomputing every row on every click is cheaper to read * than to maintain a delta, and it makes the state trivially consistent: a * folder can only be collapsed if its ancestors are expanded, and re-opening * an ancestor does not automatically re-open the folder inside it. */function applyVisibility(rows: ReadonlyMap<string, TreeRow>): void { for (const row of rows.values()) { row.element.hidden = !isRowVisible(row, rows); }}
function isRowVisible( row: TreeRow, rows: ReadonlyMap<string, TreeRow>,): boolean { let parent = row.parentPath; while (parent !== null && parent !== "") { const parentRow = rows.get(parent); if (parentRow === undefined) return true; // Chain broke; stay visible. if (parentRow.element.getAttribute("aria-expanded") !== "true") { return false; } parent = parentRow.parentPath; } return true;}
/** * Scroll the diff pane to a file, opening it first if it was collapsed. * * The lookup is by `data-viewer-file-path`, which is the exact same raw path * the tree carries on `data-item-path` — both are the value of `path` in * `RenderedFile`, and both are HTML-attribute-encoded once at emit time and * decoded once by the parser. Comparing them in `getAttribute` space (both * decoded, both raw) is what makes the pairing 1:1. * * A file the diff withheld — over the line cap, past the page's byte budget — * has no wrapper in the DOM and returns null here. That case degrades to "no * scroll" rather than "wrong scroll", which is the honest behaviour: the file * is not on the page, and pretending otherwise by scrolling to a neighbour * would tell the reader they were looking at the file they clicked. */function revealFile(button: HTMLElement, diffShadow: ShadowRoot): void { const path = button.getAttribute("data-item-path"); if (path === null) return;
const wrapper = diffShadow.querySelector<HTMLElement>( `[data-viewer-file-path="${cssEscapeAttribute(path)}"]`, ); if (wrapper === null) return;
if (wrapper instanceof HTMLDetailsElement) { wrapper.open = true; } wrapper.scrollIntoView({ block: "start", behavior: "smooth" });}
/** * Escape a value for a `[data-attr="…"]` selector. * * `CSS.escape` handles the general case, but a path with a `"` or a `\` in * it — both legal on this server's filesystem — needs to be escaped as an * attribute value on top of that, because the selector's quoting is * lexically distinct from the CSS-identifier escape. `CSS.escape` covers * both when the value sits inside quotes: it emits the escape sequences * that survive the selector parser, and the browser floor here (Chrome 123) * has had `CSS.escape` for a decade. */function cssEscapeAttribute(value: string): string { return CSS.escape(value);}apps/web/src/entry-client.ts
12234567867891011121337 unmodified lines515253545556575838 unmodified lines979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147/** * Everything this site does in a browser, which is one thing. * Everything this site does in a browser, which is a handful of small things. * * There is no hydration and no client router: `renderToStaticMarkup` wrote * every page and React is not in this bundle at all. What ships is the * timestamp repaint (./when.ts) and the scheduling below — a few hundred bytes * whose absence would leave the pages exactly as correct as they are now, only * with captions aged by however long ago the last push was. * timestamp repaint (./when.ts), the contribution-grid grow (./grid.ts), the * interactive diff panel (./diffs-interactive.ts), and the scheduling below — * a few hundred bytes whose absence would leave the pages exactly as correct * as they are now, only with captions aged by however long ago the last push * was and calendar cells frozen at build day. * * ## Why it is a file rather than an inline script tag *37 unmodified lines * Only `persisted` restores: an ordinary load already repainted above, and * doing it twice is harmless but pointless. */import { installInteractiveDiffs } from "./diffs-interactive";import { repaintCommitGrids } from "./grid";import { repaintWhen } from "./when";
const REPAINT_MS = 30_000;38 unmodified lines if (event.persisted) resume(); });}
// Contribution grids — the marker is `[data-commit-grid]`, written by// `<CommitGrid>` and looked for by `./grid.ts:repaintCommitGrids`. Same census// pattern as the timestamp block above: one feature owns one marker, the DOM// is the state, and the repaint returns a count so a page without a grid does// nothing beyond a single `querySelectorAll` (kanban 0047).//// Day-granularity data does not want the 30 s timer above — a grid's finest// unit is a civil day and no reader watches midnight tick past. The two// handlers below cover the shapes that actually matter: `visibilitychange`// for a tab that was backgrounded across midnight, and `pageshow` for a// bfcache restore that hands the reader yesterday's DOM. Both are added only// when the census is above zero, so the "does nothing" claim holds for /about// and the 404 as strictly as it does above.if (repaintCommitGrids(document, new Date()) > 0) { document.addEventListener("visibilitychange", () => { if (document.visibilityState === "visible") { repaintCommitGrids(document, new Date()); } }); window.addEventListener("pageshow", (event) => { if (event.persisted) repaintCommitGrids(document, new Date()); });}
// Interactive diffs — the marker is `<file-tree-container>`, which// `packages/viewer` emits only on the /latest/<repo> page. The census// pattern extends here: one feature owns one marker, the DOM element is// the state, and the install call returns a count so a page with no tree// does no work beyond a single `querySelector`.//// The original plan (kanban 0045) called for a dynamic `import()` gated on// this marker, so that the interactive module rode along on `/latest/<repo>`// and nowhere else. That was the right shape when the plan assumed// `@pierre/trees/web-components` would ship for folder collapse — a page// that carries pierre's client bundle should not force it on every reader// of /about. Measurement changed the shape: `web-components` is 39 kB of// duplicate CSS with no expand handler at all, so folder collapse is// hand-rolled here in a few hundred bytes and no pierre bundle ships.//// With nothing heavy to gate off, static import is the smaller answer.// `diffs-interactive.ts` is ~500 bytes gzipped, less than the// code-splitting wrapper Vite generates around a lazy `import()`, and it// preserves "one hashed file cached across every document" (the property// this file's header claims). A future feature heavy enough to justify// its own chunk should reach for `import()` and set `modulePreload: {}` in// vite.config.ts; today, the smallest thing that works is the whole thing.installInteractiveDiffs(document);apps/web/src/fixtures/rendered.ts
142 unmodified lines143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178142 unmodified lines return `${DIFF_STYLES}<div class="vw">${blocks.join("")}</div>`;}
/** * A stand-in for `packages/viewer`'s `renderMarkdown`, so the description and * discussion sections render in `vite dev` and the fixture-driven tests * without dragging the unified/remark/rehype chain into this file's * dependency graph. * * `""` in, `""` out — matches the real renderer's contract. * * Everything else: escape the input, split on blank lines, wrap each block in * a `<p>` with the site's `whitespace-pre-wrap` behavior. That is enough shape * to look right on the page and it is honestly *not* markdown — a `**bold**` * source renders as `**bold**` in the fixture, the same as it did before this * fixture learned about the field. The real renderer runs at build time * against `SiteData` from `@code/forge`, so a production page never sees this * stub's output. * * Kept alongside `renderPatchFixture` and `renderTreeFixture` for the same * reason they are here — see this file's header. */export function renderMarkdownFixture(source: string): string { if (source === "") return ""; const paragraphs = source .split(/\n{2,}/) .map((block) => block.trim()) .filter((block) => block !== ""); return paragraphs .map((block) => `<p style="white-space:pre-wrap">${escapeHtml(block)}</p>`) .join("");}
/** The directory a path sits in, or an empty string at the root. */function directoryOf(path: string): string { const slash = path.lastIndexOf("/");apps/web/src/fixtures/site-data.ts
49 unmodified lines5051525353545556575859606118 unmodified lines8081827883848586878889909192939485959697989910010192102103104105300 unmodified lines4064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635648 unmodified lines5735745755765775785798 unmodified lines5885895905915925935948 unmodified lines6036046056066076086099 unmodified lines61962062162262362462549 unmodified lines * `apps/web/src/site-data.ts` is the one import the pages use. Point that at * `@code/forge` and this file becomes test material. */import type { MergedPr, RepoEntry, SiteData } from "@code/shared/forge";import type { CommitDay, MergedPr, RepoEntry, SiteData,} from "@code/shared/forge";
/** `https://git.fugl.dev/<path>.git` — the anonymous clone URL. */function cloneHttps(path: string): string {18 unmodified lines mergedAt: "2026-08-04T09:12:44Z", mergedBy: "Russ T. Fugal", openedBy: "Russ T. Fugal", body: "Turns the ts-template import at 03812b6 into a repo that knows what it is: SPA rather than prerendered, the URL vocabulary this site actually needs, and the Codeberg namespaces carved out of the router before anything can route into them.\n\nThe prerender manifest and the service worker are gone with the render mode. The install offer stays — it costs nothing and this is a page people will open on a phone.", // Written with markdown on purpose. The description panel renders it through // `packages/viewer`'s `renderMarkdown` at build time (kanban 0046), so the // fixture has to carry the shapes the pipeline actually handles — a heading, // bold and inline code, a link, a bulleted list — for the page to show what // it does. Everything here is prose someone might write on a real PR. body: "Turns the ts-template import at `03812b6` into a repo that knows what it is:\n\n- **SPA** rather than prerendered\n- the URL vocabulary this site actually needs\n- the Codeberg namespaces carved out of the router before anything can route into them\n\nThe prerender manifest and the service worker are gone with the render mode. The install offer stays — it costs nothing and this is a page people will open on a phone. See [the build brief](https://git.fugl.dev/russ/code) for the full sequence.", comments: [ { at: "2026-08-04T08:31:02Z", actor: "Russ T. Fugal", kind: "comment", verdict: null, body: "Held the pierre packages in apps/web rather than a package of their own until the viewer proves what it needs from them.", body: "Held the `pierre` packages in `apps/web` rather than a package of their own until the viewer proves what it needs from them.", }, { at: "2026-08-04T08:58:17Z", actor: "review-agent", kind: "review", verdict: "request-changes", body: "routes.ts documents the trailing-slash decision but routes.test.ts does not pin it, so the next person to add a builder can reintroduce one and nothing complains.", body: "`routes.ts` documents the trailing-slash decision but `routes.test.ts` does not pin it, so the next person to add a builder can reintroduce one and nothing complains.\n\nSuggest:\n\n```ts\nexpect(latestPath()).not.toMatch(/\\/$/);\n```", }, { at: "2026-08-04T09:09:51Z",300 unmodified lines ],};
/** * The per-day commit series for `russ/code` — a young, active repo, two * weeks of history ending on the day of PR #1's merge. * * Weekends off, a couple of quiet in-between days, and a heaviest cell on * `2026-08-04` — the merge day — for the top bucket to have somewhere to * fire. Dense from oldest to newest, which is what `@code/forge` produces * for real: a day in the middle with no commits is `count: 0`, not absent. */const CODE_COMMIT_DAYS: readonly CommitDay[] = [ { date: "2026-07-21", count: 3 }, { date: "2026-07-22", count: 5 }, { date: "2026-07-23", count: 2 }, { date: "2026-07-24", count: 4 }, { date: "2026-07-25", count: 0 }, { date: "2026-07-26", count: 0 }, { date: "2026-07-27", count: 7 }, { date: "2026-07-28", count: 6 }, { date: "2026-07-29", count: 3 }, { date: "2026-07-30", count: 8 }, { date: "2026-07-31", count: 4 }, { date: "2026-08-01", count: 0 }, { date: "2026-08-02", count: 2 }, { date: "2026-08-03", count: 5 }, { date: "2026-08-04", count: 12 },];
/** * `russ/ap-bio` — the site's substantial repo, seven weeks of real history * ending on the day of merge #12. * * This is **the** load-bearing fixture for kanban 0047: multi-week, with * genuine zero-days scattered through (weekends, a holiday, mid-week idle * gaps) rather than composed only of non-zero cells. ADDENDUM rule 4: the * grid's whole design turns on distinguishing the zero bucket from an absent * day, and an all-nonzero series cannot exercise that distinction. Keep at * least one zero-day here or the property under test disappears at the seam. */const AP_BIO_COMMIT_DAYS: readonly CommitDay[] = [ { date: "2026-06-15", count: 4 }, { date: "2026-06-16", count: 2 }, { date: "2026-06-17", count: 0 }, { date: "2026-06-18", count: 6 }, { date: "2026-06-19", count: 3 }, { date: "2026-06-20", count: 0 }, { date: "2026-06-21", count: 0 }, { date: "2026-06-22", count: 1 }, { date: "2026-06-23", count: 5 }, { date: "2026-06-24", count: 0 }, { date: "2026-06-25", count: 8 }, { date: "2026-06-26", count: 3 }, { date: "2026-06-27", count: 0 }, { date: "2026-06-28", count: 0 }, { date: "2026-06-29", count: 4 }, { date: "2026-06-30", count: 2 }, { date: "2026-07-01", count: 7 }, { date: "2026-07-02", count: 5 }, { date: "2026-07-03", count: 0 }, { date: "2026-07-04", count: 0 }, { date: "2026-07-05", count: 0 }, { date: "2026-07-06", count: 3 }, { date: "2026-07-07", count: 6 }, { date: "2026-07-08", count: 2 }, { date: "2026-07-09", count: 4 }, { date: "2026-07-10", count: 1 }, { date: "2026-07-11", count: 0 }, { date: "2026-07-12", count: 0 }, { date: "2026-07-13", count: 5 }, { date: "2026-07-14", count: 3 }, { date: "2026-07-15", count: 9 }, { date: "2026-07-16", count: 4 }, { date: "2026-07-17", count: 2 }, { date: "2026-07-18", count: 0 }, { date: "2026-07-19", count: 0 }, { date: "2026-07-20", count: 6 }, { date: "2026-07-21", count: 3 }, { date: "2026-07-22", count: 5 }, { date: "2026-07-23", count: 4 }, { date: "2026-07-24", count: 2 }, { date: "2026-07-25", count: 0 }, { date: "2026-07-26", count: 0 }, { date: "2026-07-27", count: 7 }, { date: "2026-07-28", count: 3 }, { date: "2026-07-29", count: 4 }, { date: "2026-07-30", count: 5 }, { date: "2026-07-31", count: 2 }, { date: "2026-08-01", count: 0 }, { date: "2026-08-02", count: 8 },];
/** * `fugl.dev` — an older repo whose default branch takes bursts rather than * steady work; the shape most repos on the server actually have. */const FUGL_COMMIT_DAYS: readonly CommitDay[] = [ { date: "2026-07-10", count: 2 }, { date: "2026-07-11", count: 0 }, { date: "2026-07-12", count: 0 }, { date: "2026-07-13", count: 3 }, { date: "2026-07-14", count: 1 }, { date: "2026-07-15", count: 0 }, { date: "2026-07-16", count: 0 }, { date: "2026-07-17", count: 4 }, { date: "2026-07-18", count: 0 }, { date: "2026-07-19", count: 0 }, { date: "2026-07-20", count: 2 }, { date: "2026-07-21", count: 0 }, { date: "2026-07-22", count: 0 }, { date: "2026-07-23", count: 0 }, { date: "2026-07-24", count: 5 }, { date: "2026-07-25", count: 0 }, { date: "2026-07-26", count: 0 }, { date: "2026-07-27", count: 1 }, { date: "2026-07-28", count: 2 }, { date: "2026-07-29", count: 0 }, { date: "2026-07-30", count: 3 }, { date: "2026-07-31", count: 1 }, { date: "2026-08-01", count: 6 },];
/** * `russ/ts-template` — the shipped never-merged case, with just enough * activity to exist. Kept sparse so the row for a nearly-empty repo has a * plausible grid rather than an all-zero one that reads as "no data". */const TS_TEMPLATE_COMMIT_DAYS: readonly CommitDay[] = [ { date: "2026-06-20", count: 1 }, { date: "2026-06-21", count: 0 }, { date: "2026-06-22", count: 0 }, { date: "2026-06-23", count: 0 }, { date: "2026-06-24", count: 2 }, { date: "2026-06-25", count: 0 }, { date: "2026-06-26", count: 0 }, { date: "2026-06-27", count: 0 }, { date: "2026-06-28", count: 0 }, { date: "2026-06-29", count: 0 }, { date: "2026-06-30", count: 0 }, { date: "2026-07-01", count: 1 }, { date: "2026-07-02", count: 0 }, { date: "2026-07-03", count: 0 }, { date: "2026-07-04", count: 0 }, { date: "2026-07-05", count: 0 }, { date: "2026-07-06", count: 0 }, { date: "2026-07-07", count: 3 }, { date: "2026-07-08", count: 0 }, { date: "2026-07-09", count: 0 }, { date: "2026-07-10", count: 0 }, { date: "2026-07-11", count: 0 }, { date: "2026-07-12", count: 0 }, { date: "2026-07-13", count: 0 }, { date: "2026-07-14", count: 1 },];
const REPOS: readonly RepoEntry[] = [ { repo: {8 unmodified lines latestMerge: CODE_MERGE, mergedPrCount: 1, lastActivity: "2026-08-04T09:12:44Z", commitDays: CODE_COMMIT_DAYS, }, { repo: {8 unmodified lines latestMerge: AP_BIO_MERGE, mergedPrCount: 12, lastActivity: "2026-08-02T17:41:08Z", commitDays: AP_BIO_COMMIT_DAYS, }, { repo: {8 unmodified lines latestMerge: FUGL_MERGE, mergedPrCount: 2, lastActivity: "2026-08-01T22:05:19Z", commitDays: FUGL_COMMIT_DAYS, }, { repo: {9 unmodified lines latestMerge: null, mergedPrCount: 0, lastActivity: "2026-07-14T11:30:02Z", commitDays: TS_TEMPLATE_COMMIT_DAYS, },];
apps/web/src/fixtures/view.ts
13 unmodified lines14151617171819202121222324252627284 unmodified lines33343532333435363736373840414243394041424344454647484950515253541 unmodified line565758525354555960616213 unmodified lines * module is imported by the browser build, and the real renderer brings ~33 MB * of Shiki grammars with it. */import type { DiffFile } from "@code/shared/forge";import type { DiffFile, MergedPr } from "@code/shared/forge";import type { RenderedMerge } from "@code/shared/site";import { toServedSiteData } from "@code/shared/site";import type { SiteView } from "../site-data";import { renderPatchFixture, renderTreeFixture } from "./rendered";import { renderMarkdownFixture, renderPatchFixture, renderTreeFixture,} from "./rendered";import { FIXTURE_SITE_DATA } from "./site-data";
/**4 unmodified lines * page's "N of M files" line reads `3 of 3` against the fixture, which is what * it should say about a page showing all three. */function fixtureRender( repo: string, mergeSha: string, patch: string, files: readonly DiffFile[],): RenderedMerge {function fixtureRender(repo: string, merge: MergedPr): RenderedMerge { return { repo, mergeSha, tree: renderTreeFixture(files), html: renderPatchFixture(patch), shown: files.map((file) => file.path), mergeSha: merge.mergeSha, tree: renderTreeFixture(merge.files), html: renderPatchFixture(merge.patch), shown: merge.files.map((file: DiffFile) => file.path), withheld: [], // Body and comments as the real publish step lays them alongside the // diff — see `packages/shared/src/site.ts` for the type, and // `renderMarkdownFixture` in `./rendered` for what stands in for the // markdown renderer here. bodyHtml: renderMarkdownFixture(merge.body ?? ""), commentsHtml: merge.comments.map((comment) => renderMarkdownFixture(comment.body), ), };}
1 unmodified linefor (const entry of FIXTURE_SITE_DATA.repos) { const merge = entry.latestMerge; if (merge === null) continue; renders.set( entry.repo.path, fixtureRender(entry.repo.path, merge.mergeSha, merge.patch, merge.files), ); renders.set(entry.repo.path, fixtureRender(entry.repo.path, merge));}
export const FIXTURE_VIEW: SiteView = {apps/web/src/forge-view.test.ts
20 unmodified lines212223242526274 unmodified lines323334353637383940317 unmodified lines35835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543620 unmodified linesimport { changeTotals, commentLabel, commitShade, findRepo, formatCount, formatDate,4 unmodified lines mergeSubject, mirrorList, pathPreview, SHADE_THRESHOLDS, shortSha, siteCommitDays, siteTotals,} from "./forge-view";
317 unmodified lines ); });});
describe("siteCommitDays", () => { it("sums counts across every repo on the shared date", () => { // Every fixture repo carries `2026-07-30` in its own commitDays, so the // sitewide total for that day is the sum of the four cells — the // per-repo grid on Home rolls up into the summation grid the same way. const totals = siteCommitDays(FIXTURE_SITE_DATA); const at = new Map(totals.map((day) => [day.date, day.count])); // 8 (code) + 5 (ap-bio) + 3 (fugl.dev) + 0 (ts-template) expect(at.get("2026-07-30")).toBe(16); });
it("keeps a genuinely-quiet sitewide day as a zero, not as a hole", () => { // Constructed to have a full week between the two ends where every repo // is silent. The sitewide sum must still list every one of those dates // rather than skipping over them — that is the whole reason the grid can // distinguish "quiet week" (zero-bucket cells) from "no cell at all". const totals = siteCommitDays({ repos: [ { ...FIXTURE_SITE_DATA.repos[0]!, commitDays: [ { date: "2026-05-01", count: 3 }, { date: "2026-05-10", count: 2 }, ], }, ], }); expect(totals.map((d) => d.date)).toEqual([ "2026-05-01", "2026-05-02", "2026-05-03", "2026-05-04", "2026-05-05", "2026-05-06", "2026-05-07", "2026-05-08", "2026-05-09", "2026-05-10", ]); // Eight zero-days in the middle; the endpoints have their counts. expect(totals[0]?.count).toBe(3); expect(totals[totals.length - 1]?.count).toBe(2); expect(totals.slice(1, -1).every((d) => d.count === 0)).toBe(true); });
it("is empty for a site whose repos have no commit history yet", () => { expect(siteCommitDays({ repos: [] })).toEqual([]); });});
describe("commitShade", () => { // The distribution the fixture carries lands in these four steps. Change // one boundary and something on screen changes colour — a shade that used // to mean "3-5 commits" now means "3-4", which is worth a pin. it("puts zero in bucket 0 and every non-zero count in 1..4", () => { expect(commitShade(0)).toBe(0); expect(commitShade(-3)).toBe(0); expect(commitShade(1)).toBe(1); expect(commitShade(20)).toBe(4); });
it("splits at 2, 5 and 10 — the boundaries chosen from the fixture", () => { // The pin. `SHADE_THRESHOLDS` is where the numbers live, and this // reasserts them as behaviour so a silent tweak of the export moves the // test with it rather than the other way round. expect(SHADE_THRESHOLDS).toEqual([undefined, 2, 5, 10]); // Just below and just at each boundary. expect(commitShade(2)).toBe(1); expect(commitShade(3)).toBe(2); expect(commitShade(5)).toBe(2); expect(commitShade(6)).toBe(3); expect(commitShade(10)).toBe(3); expect(commitShade(11)).toBe(4); });});apps/web/src/forge-view.ts
15 unmodified lines16171819192021222324252627284 unmodified lines31231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440515 unmodified lines * bundle now (./when.ts), so an `Intl` call here would be one made on the * oldest browser this site claims to support rather than on a build machine. */import type { DiffFile, MirrorLinks, PrComment } from "@code/shared/forge";import type { CommitDay, DiffFile, MirrorLinks, PrComment,} from "@code/shared/forge";import type { RenderedMerge, RepoIndex,284 unmodified lines return { repos: data.repos.length, merges };}
/** * The site-wide contribution series: one row per civil date on which *any* * repo had commits, summed across every repo the site lists. * * Dense from the earliest date any repo carries through the latest, oldest * first — the same shape a single repo's {@link CommitDay} array has. A day * that no repo observed still appears as `count: 0`, so the grid can shade * a genuinely-quiet sitewide day with the zero bucket rather than dropping * out of the series. * * `sortedDates` is what defines "dense": once we have the first and last * dates seen anywhere, every calendar date between them is populated from a * `Map` — an absent one contributes zero. Absence of a date across every * repo is a legitimate answer here, not a gap that needs to be inferred. */export function siteCommitDays(data: RepoIndex): readonly CommitDay[] { const totals = new Map<string, number>(); for (const entry of data.repos) { for (const day of entry.commitDays) { totals.set(day.date, (totals.get(day.date) ?? 0) + day.count); } } if (totals.size === 0) return []; const dates = [...totals.keys()].sort(); const first = dates[0]; const last = dates[dates.length - 1]; if (first === undefined || last === undefined) return []; const dense: CommitDay[] = []; for (let d = first; ; d = nextDate(d)) { dense.push({ date: d, count: totals.get(d) ?? 0 }); if (d === last) break; } return dense;}
/** * Which shade a day's commit count reads as, `0`–`4`. * * Sequential, one-hue, bucketed from the actual distribution of the site's * per-day counts — see {@link SHADE_THRESHOLDS} for the numbers. Zero is * always bucket 0 and reads as the neutral rule token; every non-zero count * takes one of buckets 1–4, which are the four steps of the merge-violet * ramp added to `packages/ui/src/theme.css`. */export function commitShade(count: number): 0 | 1 | 2 | 3 | 4 { if (count <= 0) return 0; if (count <= SHADE_THRESHOLDS[1]) return 1; if (count <= SHADE_THRESHOLDS[2]) return 2; if (count <= SHADE_THRESHOLDS[3]) return 3; return 4;}
/** * The upper-inclusive bounds of shade buckets 1..3. Bucket 4 is `> [3]`. * * These come from the actual distribution the fixture carries, not from a * theoretical curve: across the four fixture repos and their sitewide sum, * daily counts land roughly log-spaced. Buckets 1..4 aim to spread the * observed range so every step of the violet ramp fires on at least a * handful of cells rather than being a colour that never appears. * * Boundary values are pinned by test: change one of these and the pin has * to move in the same commit. That is deliberate — the bucketing decides * how the grid *reads*, and a silent adjustment would move colours around * on a graph nobody realised it was about to. */export const SHADE_THRESHOLDS: readonly [never, number, number, number] = [ // Index 0 is unused — a count of zero has its own bucket above; kept // reserved so callers cannot confuse "shade 0" with "no threshold". undefined as never, 2, 5, 10,];
/** Increment a `YYYY-MM-DD` civil-date string by one day. */function nextDate(date: string): string { const [rawYear, rawMonth, rawDay] = date.split("-"); const year = Number(rawYear); const month = Number(rawMonth); const day = Number(rawDay); const at = new Date(Date.UTC(year, month - 1, day) + 86_400_000); const yyyy = String(at.getUTCFullYear()).padStart(4, "0"); const mm = String(at.getUTCMonth() + 1).padStart(2, "0"); const dd = String(at.getUTCDate()).padStart(2, "0"); return `${yyyy}-${mm}-${dd}`;}
/** * How much of a merge the page is showing, and why the rest is missing. *apps/web/src/grid.ts
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402/** * The contribution grid, extracted from the component that renders it. * * Two consumers: `./components/CommitGrid.tsx` for the build-time render, and * `./entry-client.ts` for the read-time repaint. Same shape from both, because * the shape is where the property under test lives — a day that has not * happened yet has NO cell in the DOM, and a day that happened with zero * commits has a cell in the zero bucket. Two files rendering that from two * copies of the rules would eventually disagree about it (kanban 0047). * * ## The zone * * `SITE_ZONE` is hardcoded here rather than imported from `@code/forge`, * because that package is Bun-only (it spawns `git` and pulls in Node * modules) and this one ships to the browser. The pin against * `@code/forge`'s value is a test in `grid.test.ts` — a drift between the * two shifts the grid's civil day off the day the {@link CommitDay} data was * bucketed on, which would silently misalign every cell. * * ## The shape at a glance * * - 7 rows, one per day-of-week, Sunday (row 0) → Saturday (row 6). * - N columns, one per week, oldest → newest. Build renders 8 columns; the * client-side repaint grows the grid up to {@link MAX_COLUMNS} as new days * elapse, then slides the window rather than growing further. * - The current (rightmost) column is partial: cells for days after "today" * in {@link SITE_ZONE} are absent, not zero-shaded. */
/** * The zone every civil date in this app is bucketed in. * * Same string as `@code/forge`'s `SITE_ZONE`; pinned by test rather than * imported so this file stays browser-safe. */export const SITE_ZONE = "America/Denver";
/** * The number of columns the build renders. Enough for at least 7 full weeks * of history plus the current partial week — see kanban 0047's spec. */export const BUILD_COLUMNS = 8;
/** * The upper bound on grid width. Once the client-side repaint has grown the * grid past this many columns, the leftmost full column is dropped so the * grid reads as a sliding window rather than a monotonically-growing timeline. */export const MAX_COLUMNS = 10;
/** * The marker `./entry-client.ts` gates its grid repaint on. * * Written by {@link CommitGrid} and looked for by * {@link repaintCommitGrids} — a page with no grid on it wires up nothing * (see the doc on {@link repaintCommitGrids} and on `./when.ts:WHEN_SELECTOR` * for the same census pattern). */export const GRID_SELECTOR = "[data-commit-grid]";
/** * Today's civil date in {@link SITE_ZONE}, as a `YYYY-MM-DD` string. * * The `now` instant is passed in rather than read here for the same reason * `formatWhen` in `./forge-view.ts` takes one: two calls milliseconds apart * cannot straddle a midnight and disagree if both see the same instant. * * `Intl.DateTimeFormat` with an explicit `timeZone` reads its zone from the * option, not from `process.env.TZ`, so this stays right on a build machine * or a browser in any zone. `en-CA` is the locale whose short-date format is * already `YYYY-MM-DD`, so no reordering is needed after `formatToParts`. */export function todayInSiteZone(now: Date): string { const parts = new Intl.DateTimeFormat("en-CA", { timeZone: SITE_ZONE, year: "numeric", month: "2-digit", day: "2-digit", }).formatToParts(now); const year = parts.find((p) => p.type === "year")?.value ?? ""; const month = parts.find((p) => p.type === "month")?.value ?? ""; const day = parts.find((p) => p.type === "day")?.value ?? ""; return `${year}-${month}-${day}`;}
/** * Day of week for a `YYYY-MM-DD` civil date, 0 (Sunday) through 6 (Saturday). * * A civil date's day-of-week is timezone-independent — 2026-08-04 is a * Tuesday everywhere — so we can reconstruct it by treating the date as a * UTC midnight and reading `getUTCDay`. That is deliberate: an ambient * `getDay` here would depend on `process.env.TZ` and give the grid a * different first-row-of-week on a build machine set to Tokyo. */export function dayOfWeek(date: string): number { const [y, m, d] = splitDate(date); return new Date(Date.UTC(y, m - 1, d)).getUTCDay();}
/** Add `days` days to a `YYYY-MM-DD` civil date. Negative works too. */export function addDays(date: string, days: number): string { const [y, m, d] = splitDate(date); const at = new Date(Date.UTC(y, m - 1, d) + days * 86_400_000); return isoCivilDate(at);}
/** The next civil date after `date`. Convenience wrapper over {@link addDays}. */export function nextDate(date: string): string { return addDays(date, 1);}
/** Split a `YYYY-MM-DD` into `[year, month, day]` numbers. Strict. */function splitDate(date: string): readonly [number, number, number] { const [rawY, rawM, rawD] = date.split("-"); return [Number(rawY), Number(rawM), Number(rawD)];}
/** Format a UTC-anchored `Date` back to `YYYY-MM-DD`. */function isoCivilDate(at: Date): string { const yyyy = String(at.getUTCFullYear()).padStart(4, "0"); const mm = String(at.getUTCMonth() + 1).padStart(2, "0"); const dd = String(at.getUTCDate()).padStart(2, "0"); return `${yyyy}-${mm}-${dd}`;}
/** * The Tailwind class the cell at `count` reads as. * * Zero takes `bg-border` — the `--rule` neutral — deliberately outside the * violet ramp, so a genuinely-quiet day reads as a hairline neutral distinct * from every "day happened, something merged" reading. Buckets 1..4 walk the * `bg-grid-shade-1..4` tokens landed in `packages/ui/src/theme.css`. * * `commitShade` lives in `./forge-view.ts` and is imported by the component * that renders these cells; this function returns the class name so the * grow-a-cell path in {@link repaintCommitGrids} stays in one file. */export function shadeClass(count: number): string { if (count <= 0) return "bg-border"; if (count <= 2) return "bg-grid-shade-1"; if (count <= 5) return "bg-grid-shade-2"; if (count <= 10) return "bg-grid-shade-3"; return "bg-grid-shade-4";}
/** * The screen-reader label for one cell. * * Singular/plural on `commit`: "1 commit" not "1 commits", and "No commits" * for the zero bucket rather than "0 commits" — that is how the sentence * would be read aloud by any human describing the day, and the cell reads * out one at a time to a screen reader user navigating the grid. */export function commitCellLabel(count: number, date: string): string { if (count === 0) return `No commits on ${date}`; if (count === 1) return `1 commit on ${date}`; return `${count} commits on ${date}`;}
/** * One cell in the rendered grid, or `null` for an absent (future) position. * * `null` rather than a placeholder object because absence is a real state on * the design side — a cell that "has not happened yet" is not drawn at all, * so the shape here mirrors that: `null` means don't render a `<td>` in this * row/column position, and a defined cell means render one with its count. */export interface GridCell { readonly date: string; readonly count: number;}
/** A grid, as a matrix of `[row=DOW][column=week]` cells. */export interface GridMatrix { /** Seven rows, one per DOW from Sunday (row 0) through Saturday (row 6). */ readonly rows: readonly (readonly (GridCell | null)[])[]; /** How many columns wide the matrix is. */ readonly columns: number; /** The rightmost date the grid covers (inclusive), aka "today" in Denver. */ readonly today: string; /** Sum of counts across every rendered cell. */ readonly total: number;}
/** * Build the display matrix from a dense `CommitDay` series and the reader's * clock, over `columns` weeks ending in `today`'s week. * * `days` is expected dense from oldest to newest (which `@code/forge` * produces, {@link CommitDay}'s doc pins it). Any date not in `days` that * falls in the visible window renders as a zero-bucket cell — a repo whose * observed history starts inside the window shows zeroes for the leading * cells rather than absent ones, matching the "past day always has a cell" * half of the design. * * The `today` argument comes in as a civil date so callers can share their * page-level `now` clock — see {@link CommitGrid} and {@link repaintCommitGrids}. */export function buildGridMatrix( days: readonly { readonly date: string; readonly count: number }[], today: string, columns: number,): GridMatrix { const counts = new Map<string, number>(); for (const day of days) counts.set(day.date, day.count);
const todayDow = dayOfWeek(today); // Sunday of today's week is `today` shifted back by `todayDow` days. const sundayOfLastWeek = addDays(today, -todayDow); const sundayOfFirstWeek = addDays(sundayOfLastWeek, -(columns - 1) * 7);
const rows: (GridCell | null)[][] = []; for (let r = 0; r < 7; r++) rows.push([]);
let total = 0; for (let c = 0; c < columns; c++) { const weekStart = addDays(sundayOfFirstWeek, c * 7); for (let r = 0; r < 7; r++) { const date = addDays(weekStart, r); if (date > today) { rows[r]!.push(null); } else { const count = counts.get(date) ?? 0; rows[r]!.push({ date, count }); total += count; } } }
return { rows, columns, today, total };}
/** * The one-line summary a screen reader reads in place of the grid. * * The `<caption>` element inside the table carries this string, visually * hidden. Same information density as the visual read: what the grid is * about, how much activity it shows, and the range it covers. */export function commitGridSummary(label: string, matrix: GridMatrix): string { const first = firstDate(matrix); const totalWord = matrix.total === 1 ? "commit" : "commits"; const rangeClause = first === null ? "with no history yet" : `from ${first} through ${matrix.today}`; return `${label}: ${matrix.total} ${totalWord} ${rangeClause}.`;}
/** The earliest cell date in the matrix, or `null` for an empty grid. */function firstDate(matrix: GridMatrix): string | null { // Row 0 (Sunday) always carries the first cell of every column, because // the matrix is anchored to Sunday-of-first-week. Read that row directly // rather than scanning every row. for (const cell of matrix.rows[0] ?? []) { if (cell !== null) return cell.date; } return null;}
/** * Repaint every marked grid under `root`, and report how many there were. * * "Repaint" here means: fill in cells for days that have elapsed since the * build wrote the grid, and slide the window when growth would exceed * {@link MAX_COLUMNS} columns. New cells are zero-bucket, because the browser * has no source for real commit counts — the point is to distinguish a real * zero from an absent cell as time passes, not to invent activity data. * * The count is not decoration: `./entry-client.ts` uses it to skip its * visibility handlers entirely on a page with no grid, so those documents * cost nothing more than a single `querySelectorAll` per feature census. * A page without a grid on it must still return `0` here — that is the * property the outer gate leans on (see the docstring on `./when.ts`). */export function repaintCommitGrids(root: ParentNode, now: Date): number { let painted = 0; const today = todayInSiteZone(now); for (const grid of root.querySelectorAll(GRID_SELECTOR)) { if (!(grid instanceof HTMLElement)) continue; growGrid(grid, today); painted += 1; } return painted;}
/** * Extend one grid's cells up to `today`, then slide the window if needed. * * Reads the current build's latest rendered date from the DOM — the largest * `data-date` across every `<td>` — and appends one zero-bucket cell per * elapsed day, into the appropriate day-of-week row. That is the "grow" * half. The "slide" half runs afterwards: if the Sunday row (row 0) now * carries more cells than {@link MAX_COLUMNS}, the leftmost full column * comes off. * * Idempotent: called twice against the same `today`, the second call finds * nothing to add and nothing to trim. */function growGrid(grid: HTMLElement, today: string): void { const rows = grid.querySelectorAll<HTMLTableRowElement>("tbody tr"); if (rows.length !== 7) return;
const latestDate = latestRenderedDate(rows); if (latestDate === "" || latestDate >= today) { trimToMaxColumns(grid, rows); return; }
for (let date = nextDate(latestDate); date <= today; date = nextDate(date)) { appendZeroCell(rows, date); } trimToMaxColumns(grid, rows);}
/** Largest `data-date` across every cell, or `""` for an empty grid. */function latestRenderedDate(rows: NodeListOf<HTMLTableRowElement>): string { let latest = ""; for (const row of rows) { for (const cell of row.querySelectorAll<HTMLElement>("td[data-date]")) { const date = cell.getAttribute("data-date") ?? ""; if (date > latest) latest = date; } } return latest;}
/** * Append one zero-bucket cell for `date` to its day-of-week row. * * `document.createElement` rather than `innerHTML`, because a `title` or a * date substituted into markup is not this file's job to escape and neither * is any other browser attribute. The count is always zero — the browser has * no source for real activity, only the build does — and the class matches * what {@link shadeClass} returns for zero so the visual matches the build's * choice for the same input. */function appendZeroCell( rows: NodeListOf<HTMLTableRowElement>, date: string,): void { const row = rows[dayOfWeek(date)]; if (row === undefined) return; const td = row.ownerDocument.createElement("td"); td.setAttribute("data-date", date); td.setAttribute("data-count", "0"); const label = commitCellLabel(0, date); td.setAttribute("aria-label", label); td.setAttribute("title", label); td.className = `${CELL_CLASS} ${shadeClass(0)}`; row.appendChild(td);}
/** * The Tailwind classes every cell wears, without the shade class. * * Kept as one string here rather than duplicated at every append site — a * cell whose base class drifted between the SSR component and the client * grow path would look right in some columns and wrong in the ones added * after load. Read by {@link appendZeroCell} and re-exported for the * component to concatenate with `shadeClass(count)`. */export const CELL_CLASS = "h-3 w-3 rounded-[2px]";
/** * Drop leftmost columns until the Sunday row has {@link MAX_COLUMNS} or fewer * cells. * * Cutoff by date rather than by cell index. Row 0 (Sunday) is the ordered * spine of the grid — every fully-past week contributes one cell to it — so * its leftmost cells name the weeks that come off first. The seven dates in * each doomed week go into a `Set` and every row is walked once to remove * matching cells. */function trimToMaxColumns( grid: HTMLElement, rows: NodeListOf<HTMLTableRowElement>,): void { const sundays = rows[0]?.querySelectorAll<HTMLElement>("td[data-date]"); if (sundays === undefined) return; const overflow = sundays.length - MAX_COLUMNS; if (overflow <= 0) return;
const cutoff = new Set<string>(); for (let i = 0; i < overflow; i++) { const sundayDate = sundays[i]?.getAttribute("data-date") ?? ""; if (sundayDate === "") continue; for (let d = 0; d < 7; d++) cutoff.add(addDays(sundayDate, d)); } for (const row of rows) { for (const cell of Array.from( row.querySelectorAll<HTMLElement>("td[data-date]"), )) { if (cutoff.has(cell.getAttribute("data-date") ?? "")) cell.remove(); } } // `grid` is here for the same reason `data-commit-grid` is: the marker is // the state and everything else is derived from the DOM. Left as a parameter // to keep the signature symmetric with `growGrid`; a future census that // needs a grid-level attribute update will already have it. void grid;}apps/web/src/head.test.tsx
68 unmodified lines6970717272737475767778798081821 unmodified line8485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716868 unmodified lines */ it("differs between routes", () => { const about = headFor( { title: "Colophon", description: "How it is built." }, { title: "Colophon", description: "How it is built.", image: { path: "/og/default.png", alt: "code.fugl.dev — colophon", }, }, "/about", ); expect(about).not.toBe(html);1 unmodified line });});
/** * The card the page unfurls as, checked as markup. The rule the card exists * for is that a link to any URL on this site unfurls as something specific to * that URL — a repo page has to carry a card that names its own repo, not the * site default; the site default is only what the pages a repo does not own * carry. * * Same-origin is mandatory: the Worker's response CSP `img-src 'self'` drops * any cross-origin card, so an `og:image` on a URL not built from * `SITE_ORIGIN` is a card that fails to render for every visitor. Absolute * because a relative `og:image` is dropped rather than resolved by the * readers of these tags. */describe("the card the page unfurls as", () => { it("carries og:image as an absolute, same-origin URL", () => { const html = headFor(homeMeta(), "/"); const home = homeMeta(); const expected = `${SITE_ORIGIN}${home.image.path.replace(/^\//, "")}`; expect(html).toContain(`<meta property="og:image" content="${expected}"`); // Not a check of `startsWith` on the whole document — the actual card URL // may not be at the start of the tag — but a pattern that a cross-origin // one could not match. expect(expected.startsWith(SITE_ORIGIN)).toBe(true); });
it("carries a non-empty og:image:alt", () => { const html = headFor(homeMeta(), "/"); const match = /<meta property="og:image:alt" content="([^"]*)"/.exec(html); expect(match).not.toBeNull(); const alt = match?.[1] ?? ""; // A screen reader that sees an empty alt is a card that lost the alt // altogether. A single non-space character clears the practical minimum // and a real one clears the useful one. expect(alt.trim().length).toBeGreaterThan(0); });
it("emits twitter:card=summary_large_image on every page", () => { for (const route of ["/", "/about", "/latest/russ/ap-bio"]) { const html = headFor(homeMeta(), route); expect( html, `${route} did not carry twitter:card=summary_large_image`, ).toContain('<meta name="twitter:card" content="summary_large_image"'); } });
/** * The bug this rules out is a card whose image URL is the same on every * repo — a possible mistake if someone hardcoded the site default instead * of using each page's own. Reading the two repo pages of the fixture and * asserting that their cards differ pins that the image is per-page rather * than site-wide, and does so without a hand-copy of the URLs. */ it("names a different card on each repo", () => { const first = FIXTURE_VIEW.site.repos[0]; const second = FIXTURE_VIEW.site.repos[1]; if (first === undefined || second === undefined) { throw new Error("the fixture needs at least two repos"); } const htmlA = headFor(repoMeta(first), `/latest/${first.repo.path}`); const htmlB = headFor(repoMeta(second), `/latest/${second.repo.path}`); const grab = (html: string): string => { const match = /<meta property="og:image" content="([^"]*)"/.exec(html); if (match === null) throw new Error("no og:image in head"); return match[1]!; }; const a = grab(htmlA); const b = grab(htmlB); expect(a).not.toBe(b); expect(a).toContain(first.repo.path); expect(b).toContain(second.repo.path); });
it("names the site-default card on non-repo pages", () => { const html = headFor(homeMeta(), "/"); expect(html).toContain('content="https://code.fugl.dev/og/default.png"'); });});
/** * The Worker's static-assets binding answers a miss with `404.html` at a real * 404 status (`not_found_handling: "404-page"` in apps/web/wrangler.jsonc).apps/web/src/head.tsx
17 unmodified lines18192021222324212223242526272814 unmodified lines434445464748495051525354555657585960616219 unmodified lines82838485868788899091929394959697989917 unmodified lines * rather than a second, hand-written escaper that would have to be as good. * * What stays in `index.html` is what is true of every page: `og:type`, * `og:site_name`, `twitter:card`, the icons and the theme colours. Splitting * on "does this vary by route" rather than on "is this a card tag" is what * stops a tag existing in both places, which is the failure `meta.test.ts` * counts for — a second `description` later in the head is read by every * `og:site_name`, the icons and the theme colours. `twitter:card` used to * live there too, when every page unfurled as the same imageless small card * — it moves here now that a card exists per page, because the card type * has to move with the image it describes. `meta.test.ts` counts both to * zero in `index.html` for the same reason: a second copy is read by every * crawler and by nobody debugging it. */import type { PageMeta } from "./meta";14 unmodified lines return SITE_ORIGIN + route.replace(/^\//, "");}
/** * The absolute URL of an asset under `/…` on this origin. Same-origin is * mandatory: the Worker sends `Content-Security-Policy: img-src 'self'` * (apps/web/worker/index.ts), and a card on a policy that is not `'self'` * fails to render for every visitor. * * Same shape as {@link canonicalUrl} — `SITE_ORIGIN` already ends in a slash * and the path already begins with one, so the leading `/` comes off rather * than the two being joined. */export function assetUrl(path: string): string { return SITE_ORIGIN + path.replace(/^\//, "");}
/** * Rendered into the `<!--app-head-->` placeholder, once per route. *19 unmodified lines <meta property="og:url" content={canonicalUrl(route)} /> <meta property="og:title" content={meta.title} /> <meta property="og:description" content={meta.description} /> <meta property="og:image" content={assetUrl(meta.image.path)} /> <meta property="og:image:alt" content={meta.image.alt} /> {/* * `summary_large_image` on every page: every page has a card. The * previous rule — `summary` when there was no image so an empty wide * box did not ship — no longer has a case to fire in. If a future * page ever ships without a card, its meta cannot set `image` at all * (the type is required), so this tag has to change with it. */} <meta name="twitter:card" content="summary_large_image" /> <meta name="twitter:image" content={assetUrl(meta.image.path)} /> <meta name="twitter:image:alt" content={meta.image.alt} /> </> );}apps/web/src/meta.test.ts
214 unmodified lines21521621721821821922022122222322422522622722822923023123223323423523623723823924024124224330 unmodified lines274275276255256257258259260261262263264265266267268269270271272273274277278279280281282283284285286287288289290291214 unmodified lines * comes back is by someone re-adding a line here that looks harmless. */describe("index.html leaves the per-page tags to head.tsx", () => { for (const key of ["description", "og:url", "og:title", "og:description"]) { // `twitter:card` is here for the first time — see the file header. It used // to live in `index.html` as `summary`, unpaired with any image, because // the site had no image to pair it with. Now every page ships its own OG // card and the card type moves with the image (`summary_large_image`), // both emitted by `head.tsx`. Adding it here is the assertion that stops // the static tag creeping back in as a well-intentioned "fallback" — // the first `twitter:card` a parser sees wins, and a static `summary` // ahead of a per-page `summary_large_image` reinstates the empty-box // failure the design deliberately does not have. // // `og:image` and `og:image:alt` are counted for the same reason: a static // copy of either, paired with the per-page ones, is a duplicate the // first-wins rule picks the wrong copy of. for (const key of [ "description", "og:url", "og:title", "og:description", "twitter:card", "og:image", "og:image:alt", "twitter:image", ]) { it(`has no static ${key}`, () => { expect(metaCount(key)).toBe(0); });30 unmodified lines });});
describe("the twitter card matches what the page can actually fill", () => { /** * `summary_large_image` reserves a wide image box and does not fall back to * the small card when there is nothing to put in it — it renders an empty * rectangle. So the two tags move together or not at all, and this is the * assertion that stops one of them moving alone. */ it("stays on the small card for exactly as long as there is no image", () => { const card = metaContent("twitter:card"); const image = metaContent("og:image");
if (image === null) { expect(card, "no og:image, so the large card would be empty").toBe( "summary", ); } else { expect(card).toBe("summary_large_image"); } });});/** * The Twitter-card / OG-image pairing used to live here — `summary` when * there was no image, `summary_large_image` when there was — because the tag * was static in `index.html` and had to match whichever state the site was * in. Both tags moved to `src/head.tsx` when the site grew a card per page * (kanban 0007), which makes them per-route rather than static and takes * this file out of the business of asserting on their pairing: `head.test.tsx` * now pins that the image URL is same-origin, that the alt text is not * empty, and that the card type is `summary_large_image` on every page. * * What remains here is the ban on either tag being static, above. */
describe("the manifest describes the same site", () => { it("uses the home description rather than the template's", () => {apps/web/src/meta.ts
18 unmodified lines192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566679 unmodified lines77787980818283848586878889909192932 unmodified lines969798991001011025 unmodified lines1081091101111121131141151161171181191209 unmodified lines1301311321331341351362 unmodified lines1391401411421431441451 unmodified line14714814915015115215318 unmodified linesimport type { ServedRepo } from "@code/shared/site";import { SITE_NAME } from "./site";
/** * URL path of the site-default OG card, hand-mirrored from * `infra/scripts/site/og-card.ts`. * * Duplicated on purpose: the browser build cannot import from an infra * script (the module tree only crosses `packages/*` boundaries), and this * string is one of the two — with the per-repo pattern below — that pin * where the built card lands. If either moves, both files move together. */const OG_DEFAULT_PATH = "/og/default.png";
/** URL path of a repo's OG card. Encoded per-segment, so `russ/ap-bio` → `/og/russ/ap-bio.png`. */function repoImagePath(repoPath: string): string { return `/og/${repoPath.split("/").map(encodeURIComponent).join("/")}.png`;}
/** * The card a page unfurls as. Absolute-URL'd through `SITE_ORIGIN` in * `head.tsx`, because an `og:image` a reader can resolve is one that renders * — a relative one is dropped rather than resolved. */export interface PageImage { /** Path — the "/og/…" part. Origin is added by the head render. */ readonly path: string; /** * What a screen reader gets instead of the card. Never blank — an empty * `og:image:alt` is a bug the crawlers will happily ship, so `head.test.tsx` * asserts non-emptiness. */ readonly alt: string;}
export interface PageMeta { title: string; description: string; /** * The card. Every page has one, so that a link to any URL on this site * unfurls as something — a blank unfurl is a card whose reader concludes * the site has nothing to say. `head.tsx` decides `summary_large_image` vs * `summary` from the presence of this field, so the Twitter card type stays * paired with the image the way `meta.test.ts` names. */ image: PageImage; /** * Keep this page out of search indexes. Set on the two "no such thing" * pages and nowhere else.9 unmodified lines noindex?: boolean;}
const DEFAULT_IMAGE: PageImage = { path: OG_DEFAULT_PATH, alt: `${SITE_NAME} — recent merged work on a self-hosted git server`,};
export function homeMeta(): PageMeta { return { title: `${SITE_NAME} — recent merged work`, description: "Public repositories on a self-hosted git server, and the most recent pull request merged into each.", image: DEFAULT_IMAGE, };}
2 unmodified lines title: `Latest merges — ${SITE_NAME}`, description: "Every public repository on git.fugl.dev and its most recent merge.", image: DEFAULT_IMAGE, };}
5 unmodified lines merge === null ? `${entry.repo.path} has no merged pull requests yet.` : `PR #${merge.number}: ${merge.title} — the latest merge on ${entry.repo.path}.`, image: { path: repoImagePath(entry.repo.path), alt: merge === null ? `${entry.repo.path} on ${SITE_NAME} — no merges yet` : `${entry.repo.path} on ${SITE_NAME} — PR #${String(merge.number)}: ${merge.title}`, }, };}
9 unmodified lines path === "" ? "That URL names no repository." : `No public repository at ${path}.`, image: DEFAULT_IMAGE, noindex: true, };}2 unmodified lines return { title: `Colophon — ${SITE_NAME}`, description: `How ${SITE_NAME} is built, and why there is no forge behind it.`, image: DEFAULT_IMAGE, };}
1 unmodified line return { title: `Page not found — ${SITE_NAME}`, description: "That page does not exist.", image: DEFAULT_IMAGE, noindex: true, };}apps/web/src/pages/Home.tsx
13 unmodified lines14151617181919202122231 unmodified line2526272829303114 unmodified lines464748495051525354555657585960616263646566676869707172737 unmodified lines8182838485868788899091929394959697989910010110210310413 unmodified lines * is argued once, on `formatWhen` in ../forge-view. */import { Link } from "react-router";import { CommitGrid } from "../components/CommitGrid";import { Fact } from "../components/facts";import { RepoRow } from "../components/RepoRow";import { formatCount, siteTotals } from "../forge-view";import { formatCount, siteCommitDays, siteTotals } from "../forge-view";import { aboutPath } from "../routes";import { useSiteView } from "../site-data";
1 unmodified line const now = new Date(); const { site } = useSiteView(); const totals = siteTotals(site); const siteDays = siteCommitDays(site);
return ( <div className="space-y-16">14 unmodified lines on every public repository, read when the site was built: the last thing that landed, and the diff exactly as it was merged. </p> {/* The one sentence on this site that is about a person rather than a machine, and the reason it is one sentence: everything else here earns its place by being evidence, and a paragraph of self-description in the hero would be the only claim on the page with nothing under it.
Set in the closing prose's register rather than the lead paragraph's — smaller, muted, underlined ink — so it reads as a signature under the thesis instead of as a second thesis. The link text is "portfolio" rather than "here", because a screen reader's list of links is the link text with none of the sentence around it. */} <p className="text-muted-foreground max-w-2xl text-sm text-pretty"> Built in public by Russ Fugal. Selected work is in the{" "} <a href="https://smart-knowledge-systems.com/portfolio?tab=coding" className="text-foreground underline decoration-1 underline-offset-[0.22em] hover:decoration-2" > portfolio </a> . </p> </section>
<section className="space-y-2">7 unmodified lines </p> </div>
{/* The sitewide contribution grid: the last 7-plus weeks of commits summed across every repo the site lists. Sits above the row list because it answers the same question at a different altitude — the rows are which repos, this is when. Rendered only when there is at least one observed day; a site whose data has not been collected yet skips it rather than painting an all-zero grid. */} {siteDays.length > 0 && ( <div className="pt-2 pb-1"> <CommitGrid days={siteDays} now={now} label="Sitewide commit activity" /> </div> )}
{/* Every row is the same row. The data is ordered by last activity and the heading above says so, which is the whole of what "most recent"apps/web/src/pages/Layout.test.tsx
28 unmodified lines293031323334355 unmodified lines41424344454647484950515253545556575859606124 unmodified lines868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917028 unmodified linesimport { StaticRouter } from "react-router";import { describe, expect, it } from "vitest";import { App } from "../App";import { OUTBOUND } from "../components/Outbound";import { CODEBERG_NAMESPACES } from "../routes";import { NAV } from "./Layout";
5 unmodified lines );}
/** * The markup between `<footer` and `</footer>`, or "" if there is none. * * A substring rather than a parse, because what the assertions below need is * exactly a region of the string — whether an href appears inside the footer as * opposed to only in the header, which is the difference between "reachable at * 390px" and "reachable at 1440px". */function footerOf(markup: string): string { const start = markup.indexOf("<footer"); if (start === -1) return ""; const end = markup.indexOf("</footer>", start); return end === -1 ? "" : markup.slice(start, end);}
describe("the shell renders at any URL the fallback can deliver", () => { it("renders every nav target", () => { for (const { to } of NAV) {24 unmodified lines });});
describe("the links that leave the site", () => { it("reaches every outbound link from the footer of every page", () => { for (const path of [...NAV.map((item) => item.to), "/no/such/page"]) { const footer = footerOf(renderAt(path)); for (const link of OUTBOUND) { expect(footer, `${link.href} on ${path}`).toContain( `href="${link.href}"`, ); } } });
it("hides no part of the footer behind a breakpoint", () => { // The header's copy of this set is `hidden md:flex`, because a *single* // outbound link up there once pushed the header 28px past a 390px viewport // and made the whole page scroll sideways. That is only affordable because // the footer carries the set unconditionally — so a `hidden` utility // anywhere in the footer silently deletes the one path to these links that // works at every width, while every test that merely greps the whole // document for the href stays green. // Split rather than `matchAll`: BROWSER_FLOOR's es-x rule forbids // String.prototype.matchAll workspace-wide, and a test file earning a // suppression comment for a one-line convenience is the wrong trade. const attributes = footerOf(renderAt("/")) .split('class="') .slice(1) .map((rest) => rest.slice(0, rest.indexOf('"')));
for (const value of attributes) { expect(value.split(/\s+/), `footer class "${value}"`).not.toContain( "hidden", ); } });
it("never lets the vector mark stand in for the link text", () => { // A mark with no label beside it is a link whose destination is carried // only by a picture. The svg is aria-hidden precisely so the label is what // is announced, which only works if there is one. expect( OUTBOUND.some((link) => link.mark !== undefined), "no link carries a mark, so this case proves nothing", ).toBe(true);
const footer = footerOf(renderAt("/")); for (const link of OUTBOUND) { if (link.mark === undefined) continue; const attribute = footer.indexOf(`href="${link.href}"`); const anchor = footer.slice(attribute, footer.indexOf("</a>", attribute)); expect(anchor, link.href).toContain("<svg"); expect(anchor, link.href).toContain('aria-hidden="true"');
// The label has to be found in the anchor's *content*, past the end of // the opening tag. Searching the whole anchor finds `russ.fugl.dev` // inside its own href and passes with no label rendered at all — which // is what this case exists to catch, so it must not be what makes it // pass. const content = anchor.slice(anchor.indexOf(">") + 1); expect(content, `${link.href} renders no text label`).toContain( link.label, ); } });
it("keeps them out of NAV, which may only hold internal paths", () => { // NAV entries are `end`-matched routes and are asserted to be paths the // Worker will not redirect away. An absolute URL to another host satisfies // neither claim, so putting one there would not merely be untidy — it // would make both of the assertions above it vacuous. const outbound = new Set(OUTBOUND.map((link) => link.href)); for (const { to } of NAV) { expect(outbound.has(to), `${to} is an outbound URL`).toBe(false); expect(to.startsWith("/"), `${to} is not a site-relative path`).toBe( true, ); } });});
describe("the shell links nowhere the Worker would redirect instead of serving", () => { it("emits no href under a Codeberg redirect namespace", () => { // `codebergRedirect` (worker/index.ts) 301s those prefixes toapps/web/src/pages/Layout.tsx
6 unmodified lines7891011121362 unmodified lines76777878798081798081828384858683848586878889909192888990919293939495969798991001011021034 unmodified lines1081091101041111121131143 unmodified lines1181191201211221231241251261271281291306 unmodified lines * src/styles.css. */import { NavLink, Outlet } from "react-router";import { OutboundLinks } from "../components/Outbound";import { formatDate } from "../forge-view";import { aboutPath, homePath, latestPath } from "../routes";import { SITE_NAME } from "../site";62 unmodified lines </nav>
{/* Outside NAV on purpose: NAV is the set of *internal* targets, and Layout.test.tsx renders every one of them and asserts none of them is a path the Worker would redirect away. An absolute URL to another host belongs to neither claim. Outside NAV on purpose — see the doc on OUTBOUND in ../components/Outbound.tsx for why an absolute URL to another host can never be a NAV entry.
Hidden below sm: at 360–390px the four nav items already fill the bar, and this link is what pushed the header 28px past the viewport and made the whole page scroll sideways. The footer carries the same link at every width, so nothing becomes unreachable. Hidden below md: at 360–390px the three nav items already fill the bar, and a *single* outbound link here was what pushed the header 28px past the viewport and made the whole page scroll sideways. Three of them clear 640px only barely, so the whole outbound group appears at 768px rather than at 640px. The footer carries the personal links at every width, so nothing becomes unreachable — that is the property, and Layout.test.tsx asserts it rather than leaving it to this comment. */} <span className="text-label ml-auto hidden items-center gap-4 md:flex"> <a href="https://git.fugl.dev" className="text-muted-foreground hover:text-foreground text-label ml-auto hidden font-mono sm:inline" > git.fugl.dev<span aria-hidden="true"> ↗</span> </a> <a href="https://git.fugl.dev" className="text-muted-foreground hover:text-foreground font-mono" > git.fugl.dev<span aria-hidden="true"> ↗</span> </a> <OutboundLinks /> </span> </div> </header>
4 unmodified lines </main>
<footer className="app-safe-x app-safe-bottom border-t"> <div className="text-muted-foreground text-label mx-auto flex w-full max-w-6xl flex-wrap justify-between gap-x-6 gap-y-2 px-6 py-6 font-mono"> <div className="text-muted-foreground text-label mx-auto flex w-full max-w-6xl flex-wrap items-center justify-between gap-x-6 gap-y-2 px-6 py-6 font-mono"> <p> a static read of{" "} <a3 unmodified lines git.fugl.dev </a> </p> {/* The one part of the shell that is about a person rather than a server, and the only place these two are reachable at every width — the header's copy is hidden below md. Never gated on a breakpoint here; Layout.test.tsx fails if it ever is. */} <OutboundLinks underline /> {/* When, absolutely rather than relatively. Every page under this shell is a view of one build's data, so the date belongs to theapps/web/src/pages/Repo.test.tsx
49 unmodified lines505152535455565758596061621 unmodified line6465666768697071727349 unmodified lines
// The render's own count: two shown, one withheld — three files total,// disagreeing with the five above on purpose.//// `bodyHtml` and `commentsHtml` are not what this file tests — kanban 0022 is// about the file-count label, not the description — so they carry the render// the fixture would produce, kept consistent with `fixtureRender` in// `../fixtures/view.ts`. Setting them to `""` and `[]` would flip the// discussion off in this test's markup, which is not a state kanban 0022 is// asserting anything about.const trimmedRender: RenderedMerge = { repo: REPO_PATH, mergeSha: fixtureMerge.mergeSha,1 unmodified line html: "<div>diff</div>", shown: ["file-1.ts", "file-2.ts"], withheld: [{ path: "file-3.ts", reason: "over-page-budget" }], bodyHtml: `<p>${fixtureMerge.body ?? ""}</p>`, commentsHtml: fixtureMerge.comments.map( (comment) => `<p>${comment.body}</p>`, ),};
const view = {apps/web/src/pages/Repo.tsx
25 unmodified lines2627282930313233343536373880 unmodified lines11912012112212312412512612712812913013113213313413513613713813914014112412512612712812913013114214314414514614714814915015115215315413315515615715815916016116216316416516616716816917017113613713813914017217317417517617717817918018118214218318418518625 unmodified lines */import type { RenderedMerge, ServedMerge } from "@code/shared/site";import { Link, useParams } from "react-router";import { CommitGrid } from "../components/CommitGrid";import { Discussion } from "../components/Discussion";import { Fact, StatRail } from "../components/facts";import { MergeSubject } from "../components/MergeSubject";import { CloneBlock, MirrorRow } from "../components/RepoAccess";import { RepoIdentity } from "../components/RepoIdentity";import { Prose } from "../components/Prose";import { RuleLabel } from "../components/RuleLabel";import { Viewer } from "../components/Viewer";import { When } from "../components/When";80 unmodified lines /> </> )} {/* The per-repo contribution grid: this repo's own commit history on the default branch, oldest → newest, ending on today (Denver). Sits inside the header rather than as its own section because it is the quantitative half of the identity above it — the display name and the merge tell you what the repo is, the grid tells you at a glance whether it is moving. Skipped for a repo with no observed history, same guard as Home's summation grid. */} {entry.commitDays.length > 0 && ( <CommitGrid days={entry.commitDays} now={now} label={`${entry.repo.displayName} commit activity`} /> )} </header>
{merge !== null && ( <> {hasDescription(merge) && ( <section className="space-y-4"> <RuleLabel>description</RuleLabel> <p className="max-w-2xl text-pretty whitespace-pre-wrap"> {merge.body} </p> </section> )} {hasDescription(merge) && render !== null && render.bodyHtml !== "" && ( <section className="space-y-4"> <RuleLabel>description</RuleLabel> {/* The body ships as HTML rendered from markdown at build time — see `packages/viewer/src/markdown.ts` and kanban 0046. Text used to render inside a `<p whitespace-pre-wrap>`; the same source markdown now becomes real prose (bold, code, lists, tables, autolinks) with the same trust properties, because the pipeline sanitizes on the way out.
<Discussion comments={merge.comments} now={now} /> `hasDescription` still guards the label from appearing above an empty region — a body that is only whitespace renders to `""` from the pipeline as well as `false` from `hasDescription`, and the belt-and-braces check keeps both facts pinned here rather than making one imply the other. */} <Prose html={render.bodyHtml} /> </section> )}
<Discussion comments={merge.comments} bodiesHtml={render?.commentsHtml ?? []} now={now} />
{/* No render is a real state, not an error state: a merge `pierre` could not draw omits the diff and keeps the repo (kanban 0008), so the page loses this section and nothing else. The file count above it goes too — it is the label on a panel that is not there, and on its own it reads as a diff that failed to load. No diff render is a real state, not an error state: a merge `pierre` could not draw omits the diff and keeps the repo (kanban 0008), so the page loses this section and nothing else. The file count above it goes too — it is the label on a panel that is not there, and on its own it reads as a diff that failed to load.
After kanban 0046, a diff failure still produces a `RenderedMerge` entry (so the description and discussion above survive), but its `html` is `""`. Testing `render.html !== ""` distinguishes "diff was rendered" from "render entry exists but the diff step failed". */} {render !== null && ( {render !== null && render.html !== "" && ( <DiffSection merge={merge} render={render}apps/web/src/routes.ts
5 unmodified lines6789101112131415169101112131415161819202122232425262717181920212223242526272829305 unmodified lines * - `ROUTE_PATTERNS` are react-router *match patterns* (`/latest/*`). * - The `*Path()` builders return *canonical hrefs*. * * Unlike the template this came from, hrefs here carry **no trailing slash**. * That convention existed to match Cloudflare's `auto-trailing-slash` asset * handling, which mapped a prerendered `about/index.html` onto `/about/` and * 301'd `/about` to it. This site is prerendered too — `about/index.html` is a * real file, written by ./pages.ts — and at the time this reasoning was * written the redirect hop was avoided because the site was served by Caddy, * whose `try_files {path}/index.html {path}` resolves the slashless URL to * that file directly. * Unlike the template this came from, hrefs here carry **no trailing slash**, * and the serving layer is configured to agree with them rather than the other * way around. `apps/web/wrangler.jsonc` sets * `html_handling: "drop-trailing-slash"`, so the Worker's asset binding serves * the prerendered `about/index.html` at `/about` directly and redirects * `/about/` to it. Every href these builders emit is therefore a 200 on the * first hop. * * Caddy is no longer what serves `code.fugl.dev` (kanban 0027): the live path * is the Worker in apps/web/worker/index.ts, whose `apps/web/wrangler.jsonc` * sets exactly the `html_handling: "auto-trailing-slash"` asset behaviour this * paragraph originally described avoiding via Caddy. Measured against the * deployed Worker: `GET /about` answers 307 to `/about/`, and * `GET /latest/russ/ap-bio` answers 307 to `/latest/russ/ap-bio/`. So a * slashless href costs exactly the redirect hop this convention was written * to avoid, on every internal link the site emits — the Caddy `try_files` * pairing was the only thing that ever made it free. Kanban 0035 tracks * fixing the convention; this file's hrefs are unchanged until that lands. * It was not always so. The convention was written when Caddy served * `code.fugl.dev` and its `try_files {path}/index.html {path}` resolved the * slashless URL to the file directly — free, but only incidentally. When the * site moved to the Worker (kanban 0027) the binding defaulted to * `auto-trailing-slash`, which does the opposite: measured against the * deployed Worker, `GET /about` answered 307 to `/about/` and * `GET /latest/russ/ap-bio` answered 307 to `/latest/russ/ap-bio/`. Every * internal link cost exactly the redirect hop the convention existed to avoid. * Kanban 0035 chose to move the setting rather than the builders, because the * builders' output is compared against literal slashless paths in a dozen * places and the setting is one line. * * ## The namespaces this app may not use *apps/web/src/when.ts
43 unmodified lines444546474847484951525354555051525354555657585960616263646566676869707143 unmodified lines * The count is not decoration: it is what lets the caller skip its timer * entirely on a page that has no relative captions on it — /about and the 404 * are both such pages — so those documents load a script that runs once and * then does nothing at all. The count cannot go up later; nothing on this site * adds a node after load. * then does nothing at all. * * "Nothing at all" is a claim about the whole bundle rather than about this * function, and it was not true for as long as Vite's modulepreload polyfill * rode along with it: that installs a permanent document-wide MutationObserver * on every page, /about included. It is switched off in ../vite.config.ts, and * anything else that ships a listener or a timer on every document costs this * sentence its meaning. * function, and holding it means every ship-a-listener block in the bundle has * to be gated on its own census. `./entry-client.ts` today has three of them: * the timer above this repaint (marker `time[data-when]`), the * contribution-grid grow (marker `[data-commit-grid]`, added by * `./grid.ts:repaintCommitGrids` — kanban 0047), and the interactive-diffs * install (marker `<file-tree-container>`, added by `./diffs-interactive.ts`). * A document that carries none of the three markers wires up nothing. Each * new feature that ships a listener adds a marker and gates on its own count; * a feature that gates on a shared truthy would silently wire up on every * page the moment any one marker landed there. * * The grid's grow does add a `<td>` per elapsed day, so the earlier version * of this paragraph — which claimed "nothing on this site adds a node after * load" — no longer described the bundle. What still holds is that every * appended node is grown by a feature whose census gate found something, and * that grid cells never appear on a page whose build did not put a grid * there. Vite's modulepreload polyfill would violate the gating property * (it installs a permanent document-wide MutationObserver on every page) and * is switched off in `../vite.config.ts` for exactly that reason. * * A marked element without a usable instant is skipped rather than blanked. A * cell whose caption was replaced by an empty string is a worse failure thanapps/web/vite.config.ts
18 unmodified lines1920212223242522232425262728291 unmodified line313233333435363435363738394041424344454647484950515253545518 unmodified lines plugins: [devRender(import.meta.dirname)], // Vite prepends a modulepreload polyfill to the entry by default, for // browsers that support `type="module"` but not `<link // rel="modulepreload">`. It was 670 of this bundle's 1,600 bytes — 42% of // the client payload — to support a feature no document here uses: there // are no dynamic imports, so Vite emits no modulepreload links at all, and // `grep modulepreload` across the eight built documents finds nothing. // rel="modulepreload">`. When it was first switched off, it was 670 of // this bundle's 1,600 bytes — 42% of the client payload — to support a // feature no document here uses: no source file in this app calls // `import()`, so Vite emits no modulepreload links at all, and // `grep modulepreload` across the built documents still finds nothing. // // Turn it off and the cost is not only bytes. The polyfill installs a // document-wide `MutationObserver({ childList: true, subtree: true })`1 unmodified line // and the 404, where the entry otherwise finds no captions and stops. It // is what made "a page with no relative captions idles" untrue. // // Revisit this the day something here imports dynamically: the polyfill // exists so a preload link is honoured on Safari 15 and below, and the // browser floor still includes those. build: { modulePreload: { polyfill: false } }, // `modulePreload: false` rather than `{ polyfill: false }`: with zero // dynamic imports the two are observably identical today, because Vite's // preload machinery has nothing to preload either way. The stronger // setting is a promise about tomorrow — if someone adds an `import()`, // `false` keeps it a bare `import()` in the emitted bundle instead of // quietly re-attaching the preload wrapper that the polyfill line of // reasoning above already ruled out. // // Kanban 0045 (interactive diffs) considered a dynamic import gated on // `<file-tree-container>` — the plan's shape when it still expected to // ship pierre's client bundle for folder collapse — and did not take it. // Measurement rewrote the shape: `@pierre/trees/web-components` is 39 kB // of duplicate CSS with no expand handler at all, so folder collapse is // hand-rolled in `src/diffs-interactive.ts` and no pierre bundle ships. // With nothing heavy to defer, the static import that reached the entry // chunk is the smaller answer and keeps "one hashed file cached across // every document" — the property index.html leans on — intact. Revisit // this the day something here is heavy enough to justify its own chunk, // and flip back to `{}` in the same commit that adds the import. build: { modulePreload: false }, },});apps/web/worker/wrangler-config.test.ts
20 unmodified lines212223242425262720 unmodified lines expect(parsed.assets).toEqual({ directory: "./dist", binding: "ASSETS", html_handling: "auto-trailing-slash", html_handling: "drop-trailing-slash", not_found_handling: "404-page", run_worker_first: true, });apps/web/wrangler.jsonc
5 unmodified lines678991011125 unmodified lines "assets": { "directory": "./dist", "binding": "ASSETS", "html_handling": "auto-trailing-slash", "html_handling": "drop-trailing-slash", "not_found_handling": "404-page", "run_worker_first": true, },bun.lock
7 unmodified lines89101112131415161718192092 unmodified lines113114115116117118119120121122123124125503 unmodified lines62963063163263363463563648 unmodified lines68568668768868969069169248 unmodified lines7417427437447457467477486 unmodified lines75575675775875976076176294 unmodified lines85785885986086186286386486586686786820 unmodified lines8898908918928938948958962 unmodified lines8999009019029039049059068 unmodified lines91591691791891992092192218 unmodified lines9419429439449459469479489499509519529539549559562 unmodified lines95996096196296396496596630 unmodified lines9979989991000100110021003100464 unmodified lines106910701071107210731074107510768 unmodified lines1085108610871088108910901091109248 unmodified lines114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164102 unmodified lines126712681269127012711272127312742 unmodified lines1277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713182 unmodified lines132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137862 unmodified lines14411442144314441445144614471448144914501451145214531454145514561457145818 unmodified lines1477147814791480148114821483148442 unmodified lines1527152815291530153115321533153415351536153715381539154015411542154315441545154612 unmodified lines1559156015611562156315641565156644 unmodified lines1611161216131614161516161617161814 unmodified lines1633163416351636163716381639164012 unmodified lines1653165416551656165716581659166014 unmodified lines16751676167716781679168016811682168316841685168624 unmodified lines171117121713171417151716171717181719172017211722172317241725172620 unmodified lines1747174817491750175117521753175468 unmodified lines182318241825182618271828182918304 unmodified lines183518361837183818391840184118427 unmodified lines "@code/forge": "workspace:*", "@code/shared": "workspace:*", "@code/viewer": "workspace:*", "@resvg/resvg-wasm": "^2.6.2", "@types/bun": "^1.3.14", "@types/node": "^26.1.1", "oxfmt": "^0.59.0", "oxlint": "^1.74.0", "oxlint-tsgolint": "^0.25.0", "satori": "^0.19.3", "typescript": "catalog:", "vitest": "catalog:", "wrangler": "4.118.0",92 unmodified lines "dependencies": { "@pierre/diffs": "^1.3.0", "@pierre/trees": "^1.0.0-beta.6", "rehype-raw": "^7.0.0", "rehype-sanitize": "^6.0.0", "rehype-stringify": "^10.0.1", "remark-gfm": "^4.0.1", "remark-parse": "^11.0.0", "remark-rehype": "^11.1.2", "unified": "^11.0.5", }, "devDependencies": { "@code/config": "workspace:*",503 unmodified lines "@radix-ui/rect": ["@radix-ui/rect@1.1.3", "", {}, "sha512-JtyZR+mqgBibTo8xea3B6ZRmzZiM/YeVBtUkas6zMuXjAlfIFIW2FgqeM9eLyvEaYX66vr6DJMK+4U6LV0KhNw=="], "@resvg/resvg-wasm": ["@resvg/resvg-wasm@2.6.2", "", {}, "sha512-FqALmHI8D4o6lk/LRWDnhw95z5eO+eAa6ORjVg09YRR7BkcM6oPHU9uyC0gtQG5vpFLvgpeU4+zEAz2H8APHNw=="], "@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.2.1", "", { "os": "android", "cpu": "arm64" }, "sha512-02hOeOSryYxVrOIphmLAsqnCJWxwlzFk+pEt/N/i6OgT3lShHO7xGCU5cpgchRDHboAEbSjzgGh+O/u1GswQmA=="], "@rolldown/binding-darwin-arm64": ["@rolldown/binding-darwin-arm64@1.2.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-fMsTOnN0OjFm3CyppWPitKnc8UlliVARUULW6cfU6AIqjdtgmSFWSk9vecHzZduv/yMWIHDlRhM1e8Iff9uAfA=="],48 unmodified lines "@shikijs/vscode-textmate": ["@shikijs/vscode-textmate@10.0.2", "", {}, "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg=="], "@shuding/opentype.js": ["@shuding/opentype.js@1.4.0-beta.0", "", { "dependencies": { "fflate": "^0.7.3", "string.prototype.codepointat": "^0.2.1" }, "bin": { "ot": "bin/ot" } }, "sha512-3NgmNyH3l/Hv6EvsWJbsvpcpUba6R8IREQ83nH83cyakCw7uM1arZKNfHwv1Wz6jgqrF/j4x5ELvR6PnK9nTcA=="], "@sindresorhus/is": ["@sindresorhus/is@7.2.0", "", {}, "sha512-P1Cz1dWaFfR4IR+U13mqqiGsLFf1KbayybWwdd2vfctdV6hDpUkgCY0nKOLLTMSoRd/jJNjtbqzf13K8DCCXQw=="], "@sindresorhus/merge-streams": ["@sindresorhus/merge-streams@4.0.0", "", {}, "sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ=="],48 unmodified lines "@types/chai": ["@types/chai@5.2.3", "", { "dependencies": { "@types/deep-eql": "*", "assertion-error": "^2.0.1" } }, "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA=="], "@types/debug": ["@types/debug@4.1.13", "", { "dependencies": { "@types/ms": "*" } }, "sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw=="], "@types/deep-eql": ["@types/deep-eql@4.0.2", "", {}, "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw=="], "@types/esrecurse": ["@types/esrecurse@4.3.1", "", {}, "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw=="],6 unmodified lines "@types/mdast": ["@types/mdast@4.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA=="], "@types/ms": ["@types/ms@2.1.0", "", {}, "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA=="], "@types/node": ["@types/node@26.1.2", "", { "dependencies": { "undici-types": "~8.3.0" } }, "sha512-Vu4a5UFA9rIIFJ7rB/Vaafh9lrCQszopTCx6KjFboXTGQbPNasehVR5TEiithSDGyd1DEiUByggTZsg8jukeIg=="], "@types/react": ["@types/react@19.2.17", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw=="],94 unmodified lines "babel-plugin-react-compiler": ["babel-plugin-react-compiler@1.0.0", "", { "dependencies": { "@babel/types": "^7.26.0" } }, "sha512-Ixm8tFfoKKIPYdCCKYTsqv+Fd4IJ0DQqMyEimo+pxUOMUR9cVPlwTrFt9Avu+3cb6Zp3mAzl+t1MrG2fxxKsxw=="], "bail": ["bail@2.0.2", "", {}, "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw=="], "balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="], "base64-js": ["base64-js@0.0.8", "", {}, "sha512-3XSA2cR/h/73EzlXXdU6YNycmYI7+kicTxks4eJg2g39biHR84slg2+des+p7iHYhbRg/udIS4TD53WabcOUkw=="], "baseline-browser-mapping": ["baseline-browser-mapping@2.11.8", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-zAgkquC2WYF0PIc6XbNYkA2uuxxFavzgmX61R+dHDUa558V8Ejf8ozTZFR6QzM24RWu4kBcRkhJ5kpz77j9fnQ=="], "blake3-wasm": ["blake3-wasm@2.1.5", "", {}, "sha512-F1+K8EbfOZE49dtoPtmxUQrpXaBIl3ICvasLh+nJta0xkz+9kF/7uet9fLnwKqhDrmj6g+6K3Tw9yQPUg2ka5g=="],20 unmodified lines "callsites": ["callsites@3.1.0", "", {}, "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="], "camelize": ["camelize@1.0.1", "", {}, "sha512-dU+Tx2fsypxTgtLoE36npi3UqcjSSMNYfkqgmoEhtZrraP5VWq0K7FkWVTYa8eMPtnU/G2txVsfdCJTn9uzpuQ=="], "caniuse-lite": ["caniuse-lite@1.0.30001806", "", {}, "sha512-72Cuvd95zbSYPKq6Fhg8eDJRlzgWDf7/mtoZv6Qe/DYNCEBdNxoA3+rZAU2ZhGCpZlns3EssFavaZomckT5Uuw=="], "ccount": ["ccount@2.0.1", "", {}, "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg=="],2 unmodified lines "chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="], "character-entities": ["character-entities@2.0.2", "", {}, "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ=="], "character-entities-html4": ["character-entities-html4@2.1.0", "", {}, "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA=="], "character-entities-legacy": ["character-entities-legacy@3.0.0", "", {}, "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ=="],8 unmodified lines "code-block-writer": ["code-block-writer@13.0.3", "", {}, "sha512-Oofo0pq3IKnsFtuHqSF7TqBfr71aeyZDVJ0HpmqB7FBM2qEigL0iPONSCZSO9pE9dZTAxANe5XHG9Uy0YMv8cg=="], "color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], "comma-separated-tokens": ["comma-separated-tokens@2.0.3", "", {}, "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg=="], "commander": ["commander@14.0.3", "", {}, "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw=="],18 unmodified lines "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], "css-background-parser": ["css-background-parser@0.1.0", "", {}, "sha512-2EZLisiZQ+7m4wwur/qiYJRniHX4K5Tc9w93MT3AS0WS1u5kaZ4FKXlOTBhOjc+CgEgPiGY+fX1yWD8UwpEqUA=="], "css-box-shadow": ["css-box-shadow@1.0.0-3", "", {}, "sha512-9jaqR6e7Ohds+aWwmhe6wILJ99xYQbfmK9QQB9CcMjDbTxPZjwEmUQpU91OG05Xgm8BahT5fW+svbsQGjS/zPg=="], "css-color-keywords": ["css-color-keywords@1.0.0", "", {}, "sha512-FyyrDHZKEjXDpNJYvVsV960FiqQyXc/LlYmsxl2BcdMb2WPx0OGRVgTg55rPSyLSNMqP52R9r8geSp7apN3Ofg=="], "css-gradient-parser": ["css-gradient-parser@0.0.17", "", {}, "sha512-w2Xy9UMMwlKtou0vlRnXvWglPAceXCTtcmVSo8ZBUvqCV5aXEFP/PC6d+I464810I9FT++UACwTD5511bmGPUg=="], "css-to-react-native": ["css-to-react-native@3.2.0", "", { "dependencies": { "camelize": "^1.0.0", "css-color-keywords": "^1.0.0", "postcss-value-parser": "^4.0.2" } }, "sha512-e8RKaLXMOFii+02mOlqwjbD00KSEKqblnpO9e++1aXS1fPQOpS1YoqdVHBqPjHNoxeF2mimzVqawm2KCbEdtHQ=="], "cssesc": ["cssesc@3.0.0", "", { "bin": { "cssesc": "bin/cssesc" } }, "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg=="], "csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="],2 unmodified lines "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" }, "peerDependencies": { "supports-color": "*" }, "optionalPeers": ["supports-color"] }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], "decode-named-character-reference": ["decode-named-character-reference@1.3.0", "", { "dependencies": { "character-entities": "^2.0.0" } }, "sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q=="], "dedent": ["dedent@1.7.2", "", { "peerDependencies": { "babel-plugin-macros": "^3.1.0" }, "optionalPeers": ["babel-plugin-macros"] }, "sha512-WzMx3mW98SN+zn3hgemf4OzdmyNhhhKz5Ay0pUfQiMQ3e1g+xmTJWp/pKdwKVXhdSkAEGIIzqeuWrL3mV/AXbA=="], "deep-is": ["deep-is@0.1.4", "", {}, "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="],30 unmodified lines "emoji-regex": ["emoji-regex@10.6.0", "", {}, "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A=="], "emoji-regex-xs": ["emoji-regex-xs@2.0.1", "", {}, "sha512-1QFuh8l7LqUcKe24LsPUNzjrzJQ7pgRwp1QMcZ5MX6mFplk2zQ08NVCM84++1cveaUUYtcCYHmeFEuNg16sU4g=="], "encodeurl": ["encodeurl@2.0.0", "", {}, "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="], "enhanced-resolve": ["enhanced-resolve@5.24.5", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.3.3" } }, "sha512-L1l8TNvomm6UVW5B253AGxQagSQr+vGwhMlrrfRS2qmhx46AMpMVJKQYLvWYbysTMY8VoicOvzHzoHMbyzB+4A=="],64 unmodified lines "express-rate-limit": ["express-rate-limit@8.6.1", "", { "dependencies": { "debug": "^4.4.3", "ip-address": "^10.2.0" }, "peerDependencies": { "express": ">= 4.11" } }, "sha512-0D493aP61w0TJ2A0wy27riRsO7FMQ7FK+KUHOKCSfPvYo0R55aiC6emCVgFUeShH0fq0ICPVzNcgoS+BsbXQCA=="], "extend": ["extend@3.0.2", "", {}, "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="], "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], "fast-glob": ["fast-glob@3.3.3", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.8" } }, "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg=="],8 unmodified lines "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], "fflate": ["fflate@0.7.5", "", {}, "sha512-QieYf//cis6ywHNi5qW1+PXPQ4bC+XVJAtS4AXIML8P76GroEiOxm/oQtn1f02UkJY1+KsXMJcC+R2v/Eg4G3g=="], "figures": ["figures@6.1.0", "", { "dependencies": { "is-unicode-supported": "^2.0.0" } }, "sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg=="], "file-entry-cache": ["file-entry-cache@8.0.0", "", { "dependencies": { "flat-cache": "^4.0.0" } }, "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ=="],48 unmodified lines "hasown": ["hasown@2.0.4", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A=="], "hast-util-from-parse5": ["hast-util-from-parse5@8.0.3", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "devlop": "^1.0.0", "hastscript": "^9.0.0", "property-information": "^7.0.0", "vfile": "^6.0.0", "vfile-location": "^5.0.0", "web-namespaces": "^2.0.0" } }, "sha512-3kxEVkEKt0zvcZ3hCRYI8rqrgwtlIOFMWkbclACvjlDw8Li9S2hk/d51OI0nr/gIpdMHNepwgOKqZ/sy0Clpyg=="], "hast-util-parse-selector": ["hast-util-parse-selector@4.0.0", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A=="], "hast-util-raw": ["hast-util-raw@9.1.0", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "@ungap/structured-clone": "^1.0.0", "hast-util-from-parse5": "^8.0.0", "hast-util-to-parse5": "^8.0.0", "html-void-elements": "^3.0.0", "mdast-util-to-hast": "^13.0.0", "parse5": "^7.0.0", "unist-util-position": "^5.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0", "web-namespaces": "^2.0.0", "zwitch": "^2.0.0" } }, "sha512-Y8/SBAHkZGoNkpzqqfCldijcuUKh7/su31kEBp67cFY09Wy0mTRgtsLYsiIxMJxlu0f6AA5SUTbDR8K0rxnbUw=="], "hast-util-sanitize": ["hast-util-sanitize@5.0.2", "", { "dependencies": { "@types/hast": "^3.0.0", "@ungap/structured-clone": "^1.0.0", "unist-util-position": "^5.0.0" } }, "sha512-3yTWghByc50aGS7JlGhk61SPenfE/p1oaFeNwkOOyrscaOkMGrcW9+Cy/QAIOBpZxP1yqDIzFMR0+Np0i0+usg=="], "hast-util-to-html": ["hast-util-to-html@9.0.5", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "ccount": "^2.0.0", "comma-separated-tokens": "^2.0.0", "hast-util-whitespace": "^3.0.0", "html-void-elements": "^3.0.0", "mdast-util-to-hast": "^13.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "stringify-entities": "^4.0.0", "zwitch": "^2.0.4" } }, "sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw=="], "hast-util-to-parse5": ["hast-util-to-parse5@8.0.1", "", { "dependencies": { "@types/hast": "^3.0.0", "comma-separated-tokens": "^2.0.0", "devlop": "^1.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "web-namespaces": "^2.0.0", "zwitch": "^2.0.0" } }, "sha512-MlWT6Pjt4CG9lFCjiz4BH7l9wmrMkfkJYCxFwKQic8+RTZgWPuWxwAfjJElsXkex7DJjfSJsQIt931ilUgmwdA=="], "hast-util-whitespace": ["hast-util-whitespace@3.0.0", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw=="], "hastscript": ["hastscript@9.0.1", "", { "dependencies": { "@types/hast": "^3.0.0", "comma-separated-tokens": "^2.0.0", "hast-util-parse-selector": "^4.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0" } }, "sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w=="], "hex-rgb": ["hex-rgb@4.3.0", "", {}, "sha512-Ox1pJVrDCyGHMG9CFg1tmrRUMRPRsAWYc/PinY0XzJU4K7y7vjNoLKIQ7BR5UJMCxNN8EM1MNDmHWA/B3aZUuw=="], "hono": ["hono@4.12.32", "", {}, "sha512-XcuyW9qE2kJn07PkecMOBd5Vq/hMy7mmGw+idz1yblbg9N17ijJODrvPkn7/dwL3Kulj8LcRJ69DLOWf91dRUg=="], "html-void-elements": ["html-void-elements@3.0.0", "", {}, "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg=="],102 unmodified lines "lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.33.0", "", { "os": "win32", "cpu": "x64" }, "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA=="], "linebreak": ["linebreak@1.1.0", "", { "dependencies": { "base64-js": "0.0.8", "unicode-trie": "^2.0.0" } }, "sha512-MHp03UImeVhB7XZtjd0E4n6+3xr5Dq/9xI/5FptGk5FrbDR3zagPa2DS6U8ks/3HjbKWG9Q1M2ufOzxV2qLYSQ=="], "lines-and-columns": ["lines-and-columns@1.2.4", "", {}, "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="], "locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="],2 unmodified lines "log-symbols": ["log-symbols@6.0.0", "", { "dependencies": { "chalk": "^5.3.0", "is-unicode-supported": "^1.3.0" } }, "sha512-i24m8rpwhmPIS4zscNzK6MSEhk0DUWa/8iYQWxhffV8jkI4Phvs3F+quL5xvS0gdQR0FyTCMMH33Y78dDTzzIw=="], "longest-streak": ["longest-streak@3.1.0", "", {}, "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g=="], "lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="], "lru_map": ["lru_map@0.4.1", "", {}, "sha512-I+lBvqMMFfqaV8CJCISjI3wbjmwVu/VyOoU7+qtu9d7ioW5klMgsTTiUOUp+DJvfTTzKXoPbyC6YfgkNcyPSOg=="], "magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="], "markdown-table": ["markdown-table@3.0.4", "", {}, "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw=="], "math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="], "mdast-util-find-and-replace": ["mdast-util-find-and-replace@3.0.2", "", { "dependencies": { "@types/mdast": "^4.0.0", "escape-string-regexp": "^5.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg=="], "mdast-util-from-markdown": ["mdast-util-from-markdown@2.0.3", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "mdast-util-to-string": "^4.0.0", "micromark": "^4.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-decode-string": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-W4mAWTvSlKvf8L6J+VN9yLSqQ9AOAAvHuoDAmPkz4dHf553m5gVj2ejadHJhoJmcmxEnOv6Pa8XJhpxE93kb8Q=="], "mdast-util-gfm": ["mdast-util-gfm@3.1.0", "", { "dependencies": { "mdast-util-from-markdown": "^2.0.0", "mdast-util-gfm-autolink-literal": "^2.0.0", "mdast-util-gfm-footnote": "^2.0.0", "mdast-util-gfm-strikethrough": "^2.0.0", "mdast-util-gfm-table": "^2.0.0", "mdast-util-gfm-task-list-item": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ=="], "mdast-util-gfm-autolink-literal": ["mdast-util-gfm-autolink-literal@2.0.1", "", { "dependencies": { "@types/mdast": "^4.0.0", "ccount": "^2.0.0", "devlop": "^1.0.0", "mdast-util-find-and-replace": "^3.0.0", "micromark-util-character": "^2.0.0" } }, "sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ=="], "mdast-util-gfm-footnote": ["mdast-util-gfm-footnote@2.1.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "devlop": "^1.1.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0" } }, "sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ=="], "mdast-util-gfm-strikethrough": ["mdast-util-gfm-strikethrough@2.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg=="], "mdast-util-gfm-table": ["mdast-util-gfm-table@2.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "markdown-table": "^3.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg=="], "mdast-util-gfm-task-list-item": ["mdast-util-gfm-task-list-item@2.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ=="], "mdast-util-phrasing": ["mdast-util-phrasing@4.1.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "unist-util-is": "^6.0.0" } }, "sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w=="], "mdast-util-to-hast": ["mdast-util-to-hast@13.2.1", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "@ungap/structured-clone": "^1.0.0", "devlop": "^1.0.0", "micromark-util-sanitize-uri": "^2.0.0", "trim-lines": "^3.0.0", "unist-util-position": "^5.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" } }, "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA=="], "mdast-util-to-markdown": ["mdast-util-to-markdown@2.1.2", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "longest-streak": "^3.0.0", "mdast-util-phrasing": "^4.0.0", "mdast-util-to-string": "^4.0.0", "micromark-util-classify-character": "^2.0.0", "micromark-util-decode-string": "^2.0.0", "unist-util-visit": "^5.0.0", "zwitch": "^2.0.0" } }, "sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA=="], "mdast-util-to-string": ["mdast-util-to-string@4.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0" } }, "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg=="], "media-typer": ["media-typer@1.1.1", "", {}, "sha512-yz3xRaG20c6/BOzvYoDaGtPmGscs7YivItZEEqe6GbwNfHuxu9YNmvnEkMzKldAGY4/80pRcQRZSEnhquk9XuQ=="], "merge-descriptors": ["merge-descriptors@2.0.0", "", {}, "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g=="],2 unmodified lines "merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="], "micromark": ["micromark@4.0.2", "", { "dependencies": { "@types/debug": "^4.0.0", "debug": "^4.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "micromark-core-commonmark": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-combine-extensions": "^2.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-encode": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-sanitize-uri": "^2.0.0", "micromark-util-subtokenize": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA=="], "micromark-core-commonmark": ["micromark-core-commonmark@2.0.3", "", { "dependencies": { "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "micromark-factory-destination": "^2.0.0", "micromark-factory-label": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-factory-title": "^2.0.0", "micromark-factory-whitespace": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-classify-character": "^2.0.0", "micromark-util-html-tag-name": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-subtokenize": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg=="], "micromark-extension-gfm": ["micromark-extension-gfm@3.0.0", "", { "dependencies": { "micromark-extension-gfm-autolink-literal": "^2.0.0", "micromark-extension-gfm-footnote": "^2.0.0", "micromark-extension-gfm-strikethrough": "^2.0.0", "micromark-extension-gfm-table": "^2.0.0", "micromark-extension-gfm-tagfilter": "^2.0.0", "micromark-extension-gfm-task-list-item": "^2.0.0", "micromark-util-combine-extensions": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w=="], "micromark-extension-gfm-autolink-literal": ["micromark-extension-gfm-autolink-literal@2.1.0", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-sanitize-uri": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw=="], "micromark-extension-gfm-footnote": ["micromark-extension-gfm-footnote@2.1.0", "", { "dependencies": { "devlop": "^1.0.0", "micromark-core-commonmark": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-sanitize-uri": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw=="], "micromark-extension-gfm-strikethrough": ["micromark-extension-gfm-strikethrough@2.1.0", "", { "dependencies": { "devlop": "^1.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-classify-character": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw=="], "micromark-extension-gfm-table": ["micromark-extension-gfm-table@2.1.1", "", { "dependencies": { "devlop": "^1.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg=="], "micromark-extension-gfm-tagfilter": ["micromark-extension-gfm-tagfilter@2.0.0", "", { "dependencies": { "micromark-util-types": "^2.0.0" } }, "sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg=="], "micromark-extension-gfm-task-list-item": ["micromark-extension-gfm-task-list-item@2.1.0", "", { "dependencies": { "devlop": "^1.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw=="], "micromark-factory-destination": ["micromark-factory-destination@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA=="], "micromark-factory-label": ["micromark-factory-label@2.0.1", "", { "dependencies": { "devlop": "^1.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg=="], "micromark-factory-space": ["micromark-factory-space@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg=="], "micromark-factory-title": ["micromark-factory-title@2.0.1", "", { "dependencies": { "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw=="], "micromark-factory-whitespace": ["micromark-factory-whitespace@2.0.1", "", { "dependencies": { "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ=="], "micromark-util-character": ["micromark-util-character@2.1.1", "", { "dependencies": { "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q=="], "micromark-util-chunked": ["micromark-util-chunked@2.0.1", "", { "dependencies": { "micromark-util-symbol": "^2.0.0" } }, "sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA=="], "micromark-util-classify-character": ["micromark-util-classify-character@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q=="], "micromark-util-combine-extensions": ["micromark-util-combine-extensions@2.0.1", "", { "dependencies": { "micromark-util-chunked": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg=="], "micromark-util-decode-numeric-character-reference": ["micromark-util-decode-numeric-character-reference@2.0.2", "", { "dependencies": { "micromark-util-symbol": "^2.0.0" } }, "sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw=="], "micromark-util-decode-string": ["micromark-util-decode-string@2.0.1", "", { "dependencies": { "decode-named-character-reference": "^1.0.0", "micromark-util-character": "^2.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-symbol": "^2.0.0" } }, "sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ=="], "micromark-util-encode": ["micromark-util-encode@2.0.1", "", {}, "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw=="], "micromark-util-html-tag-name": ["micromark-util-html-tag-name@2.0.1", "", {}, "sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA=="], "micromark-util-normalize-identifier": ["micromark-util-normalize-identifier@2.0.1", "", { "dependencies": { "micromark-util-symbol": "^2.0.0" } }, "sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q=="], "micromark-util-resolve-all": ["micromark-util-resolve-all@2.0.1", "", { "dependencies": { "micromark-util-types": "^2.0.0" } }, "sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg=="], "micromark-util-sanitize-uri": ["micromark-util-sanitize-uri@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-encode": "^2.0.0", "micromark-util-symbol": "^2.0.0" } }, "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ=="], "micromark-util-subtokenize": ["micromark-util-subtokenize@2.1.0", "", { "dependencies": { "devlop": "^1.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA=="], "micromark-util-symbol": ["micromark-util-symbol@2.0.1", "", {}, "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q=="], "micromark-util-types": ["micromark-util-types@2.0.2", "", {}, "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA=="],62 unmodified lines "p-try": ["p-try@2.2.0", "", {}, "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ=="], "pako": ["pako@0.2.9", "", {}, "sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA=="], "parent-module": ["parent-module@1.0.1", "", { "dependencies": { "callsites": "^3.0.0" } }, "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g=="], "parse-css-color": ["parse-css-color@0.2.1", "", { "dependencies": { "color-name": "^1.1.4", "hex-rgb": "^4.1.0" } }, "sha512-bwS/GGIFV3b6KS4uwpzCFj4w297Yl3uqnSgIPsoQkx7GMLROXfMnWvxfNkL0oh8HVhZA4hvJoEoEIqonfJ3BWg=="], "parse-json": ["parse-json@5.2.0", "", { "dependencies": { "@babel/code-frame": "^7.0.0", "error-ex": "^1.3.1", "json-parse-even-better-errors": "^2.3.0", "lines-and-columns": "^1.1.6" } }, "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg=="], "parse-ms": ["parse-ms@4.0.0", "", {}, "sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw=="], "parse5": ["parse5@7.3.0", "", { "dependencies": { "entities": "^6.0.0" } }, "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw=="], "parseurl": ["parseurl@1.3.3", "", {}, "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="], "path-browserify": ["path-browserify@1.0.1", "", {}, "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g=="],18 unmodified lines "postcss-selector-parser": ["postcss-selector-parser@7.1.4", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-HeP7D2wyhkR+XaK6v4W8oRF62Dsz4flyuczALJp61GckGm42u1saSSJ/0auvcBqxs3jMRFEcPK34At/0JBKdOg=="], "postcss-value-parser": ["postcss-value-parser@4.2.0", "", {}, "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ=="], "powershell-utils": ["powershell-utils@0.1.0", "", {}, "sha512-dM0jVuXJPsDN6DvRpea484tCUaMiXWjuCn++HGTqUWzGDjv5tZkEZldAJ/UMlqRYGFrD/etByo4/xOuC/snX2A=="], "preact": ["preact@11.0.0-beta.0", "", {}, "sha512-IcODoASASYwJ9kxz7+MJeiJhvLriwSb4y4mHIyxdgaRZp6kPUud7xytrk/6GZw8U3y6EFJaRb5wi9SrEK+8+lg=="],42 unmodified lines "regex-utilities": ["regex-utilities@2.3.0", "", {}, "sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng=="], "rehype-raw": ["rehype-raw@7.0.0", "", { "dependencies": { "@types/hast": "^3.0.0", "hast-util-raw": "^9.0.0", "vfile": "^6.0.0" } }, "sha512-/aE8hCfKlQeA8LmyeyQvQF3eBiLRGNlfBJEvWH7ivp9sBqs7TNqBL5X3v157rM4IFETqDnIOO+z5M/biZbo9Ww=="], "rehype-sanitize": ["rehype-sanitize@6.0.0", "", { "dependencies": { "@types/hast": "^3.0.0", "hast-util-sanitize": "^5.0.0" } }, "sha512-CsnhKNsyI8Tub6L4sm5ZFsme4puGfc6pYylvXo1AeqaGbjOYyzNv3qZPwvs0oMJ39eryyeOdmxwUIo94IpEhqg=="], "rehype-stringify": ["rehype-stringify@10.0.1", "", { "dependencies": { "@types/hast": "^3.0.0", "hast-util-to-html": "^9.0.0", "unified": "^11.0.0" } }, "sha512-k9ecfXHmIPuFVI61B9DeLPN0qFHfawM6RsuX48hoqlaKSF61RskNjSm1lI8PhBEM0MRdLxVVm4WmTqJQccH9mA=="], "remark-gfm": ["remark-gfm@4.0.1", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-gfm": "^3.0.0", "micromark-extension-gfm": "^3.0.0", "remark-parse": "^11.0.0", "remark-stringify": "^11.0.0", "unified": "^11.0.0" } }, "sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg=="], "remark-parse": ["remark-parse@11.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-from-markdown": "^2.0.0", "micromark-util-types": "^2.0.0", "unified": "^11.0.0" } }, "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA=="], "remark-rehype": ["remark-rehype@11.1.2", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "mdast-util-to-hast": "^13.0.0", "unified": "^11.0.0", "vfile": "^6.0.0" } }, "sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw=="], "remark-stringify": ["remark-stringify@11.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-to-markdown": "^2.0.0", "unified": "^11.0.0" } }, "sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw=="], "require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="], "resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="],12 unmodified lines "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="], "satori": ["satori@0.19.3", "", { "dependencies": { "@shuding/opentype.js": "1.4.0-beta.0", "css-background-parser": "^0.1.0", "css-box-shadow": "1.0.0-3", "css-gradient-parser": "^0.0.17", "css-to-react-native": "^3.0.0", "emoji-regex-xs": "^2.0.1", "escape-html": "^1.0.3", "linebreak": "^1.1.0", "parse-css-color": "^0.2.1", "postcss-value-parser": "^4.2.0", "yoga-layout": "^3.2.1" } }, "sha512-dKr8TNYSyceWqBoTHWntjy25xaiWMw5GF+f8QOqFsov9OpTswLs7xdbvZudGRp9jkzbhv/4mVjVZYFtpruGKiA=="], "scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="], "semver": ["semver@7.8.5", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA=="],44 unmodified lines "string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="], "string.prototype.codepointat": ["string.prototype.codepointat@0.2.1", "", {}, "sha512-2cBVCj6I4IOvEnjgO/hWqXjqBGsY+zwPmHl12Srk9IXSZ56Jwwmy+66XO5Iut/oQVR7t5ihYdLB0GMa4alEUcg=="], "stringify-entities": ["stringify-entities@4.0.4", "", { "dependencies": { "character-entities-html4": "^2.0.0", "character-entities-legacy": "^3.0.0" } }, "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg=="], "stringify-object": ["stringify-object@5.0.0", "", { "dependencies": { "get-own-enumerable-keys": "^1.0.0", "is-obj": "^3.0.0", "is-regexp": "^3.1.0" } }, "sha512-zaJYxz2FtcMb4f+g60KsRNFOpVMUyuJgA51Zi5Z1DOTC3S59+OQiVOzE9GZt0x72uBGWKsQIuBKeF9iusmKFsg=="],14 unmodified lines "tapable": ["tapable@2.3.3", "", {}, "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A=="], "tiny-inflate": ["tiny-inflate@1.0.3", "", {}, "sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw=="], "tiny-invariant": ["tiny-invariant@1.3.3", "", {}, "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg=="], "tinybench": ["tinybench@2.9.0", "", {}, "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg=="],12 unmodified lines "trim-lines": ["trim-lines@3.0.1", "", {}, "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg=="], "trough": ["trough@2.2.0", "", {}, "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw=="], "ts-morph": ["ts-morph@26.0.0", "", { "dependencies": { "@ts-morph/common": "~0.27.0", "code-block-writer": "^13.0.3" } }, "sha512-ztMO++owQnz8c/gIENcM9XfCEzgoGphTv+nKpYNM1bgsdOVC/jRZuEBf6N+mLLDNg68Kl+GgUZfOySaRiG1/Ug=="], "tsconfig-paths": ["tsconfig-paths@4.2.0", "", { "dependencies": { "json5": "^2.2.2", "minimist": "^1.2.6", "strip-bom": "^3.0.0" } }, "sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg=="],14 unmodified lines "unenv": ["unenv@2.0.0-rc.24", "", { "dependencies": { "pathe": "^2.0.3" } }, "sha512-i7qRCmY42zmCwnYlh9H2SvLEypEFGye5iRmEMKjcGi7zk9UquigRjFtTLz0TYqr0ZGLZhaMHl/foy1bZR+Cwlw=="], "unicode-trie": ["unicode-trie@2.0.0", "", { "dependencies": { "pako": "^0.2.5", "tiny-inflate": "^1.0.0" } }, "sha512-x7bc76x0bm4prf1VLg79uhAzKw8DVboClSN5VxJuQ+LKDOVEW9CdH+VY7SP+vX7xCYQqzzgQpFqz15zeLvAtZQ=="], "unicorn-magic": ["unicorn-magic@0.3.0", "", {}, "sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA=="], "unified": ["unified@11.0.5", "", { "dependencies": { "@types/unist": "^3.0.0", "bail": "^2.0.0", "devlop": "^1.0.0", "extend": "^3.0.0", "is-plain-obj": "^4.0.0", "trough": "^2.0.0", "vfile": "^6.0.0" } }, "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA=="], "unist-util-is": ["unist-util-is@6.0.1", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g=="], "unist-util-position": ["unist-util-position@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA=="],24 unmodified lines "vfile": ["vfile@6.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "vfile-message": "^4.0.0" } }, "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q=="], "vfile-location": ["vfile-location@5.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "vfile": "^6.0.0" } }, "sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg=="], "vfile-message": ["vfile-message@4.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw=="], "vite": ["vite@8.2.0", "", { "dependencies": { "lightningcss": "^1.33.0", "picomatch": "^4.0.5", "postcss": "^8.5.23", "rolldown": "~1.2.0", "tinyglobby": "^0.2.17" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "@vitejs/devtools": "^0.4.0", "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "@vitejs/devtools", "esbuild", "jiti", "less", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-pn+CFpM0lwDeKwmOq1ZaBK/9sjorZcgqxki6MbY/jPEVd9vichIlmlD4HmQ5wdP5EgqQCFRaACBxMC7uEGc6lQ=="], "vitest": ["vitest@4.1.10", "", { "dependencies": { "@vitest/expect": "4.1.10", "@vitest/mocker": "4.1.10", "@vitest/pretty-format": "4.1.10", "@vitest/runner": "4.1.10", "@vitest/snapshot": "4.1.10", "@vitest/spy": "4.1.10", "@vitest/utils": "4.1.10", "es-module-lexer": "^2.0.0", "expect-type": "^1.3.0", "magic-string": "^0.30.21", "obug": "^2.1.1", "pathe": "^2.0.3", "picomatch": "^4.0.3", "std-env": "^4.0.0-rc.1", "tinybench": "^2.9.0", "tinyexec": "^1.0.2", "tinyglobby": "^0.2.15", "tinyrainbow": "^3.1.0", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", "why-is-node-running": "^2.3.0" }, "peerDependencies": { "@edge-runtime/vm": "*", "@opentelemetry/api": "^1.9.0", "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", "@vitest/browser-playwright": "4.1.10", "@vitest/browser-preview": "4.1.10", "@vitest/browser-webdriverio": "4.1.10", "@vitest/coverage-istanbul": "4.1.10", "@vitest/coverage-v8": "4.1.10", "@vitest/ui": "4.1.10", "happy-dom": "*", "jsdom": "*" }, "optionalPeers": ["@edge-runtime/vm", "@opentelemetry/api", "@types/node", "@vitest/browser-playwright", "@vitest/browser-preview", "@vitest/browser-webdriverio", "@vitest/coverage-istanbul", "@vitest/coverage-v8", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "./vitest.mjs" } }, "sha512-R9jUTe5S4Qb0HCd4TNqpC7oGcrMssMRGXLW80ubjWsW9VH5GF8y1Y0SFLY9AbqSk6nt0PnOx4H4WNJYZ13GUPw=="], "web-namespaces": ["web-namespaces@2.0.1", "", {}, "sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ=="], "whatwg-mimetype": ["whatwg-mimetype@3.0.0", "", {}, "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q=="], "which": ["which@4.0.0", "", { "dependencies": { "isexe": "^3.1.1" }, "bin": { "node-which": "bin/which.js" } }, "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg=="],20 unmodified lines "yoctocolors": ["yoctocolors@2.2.0", "", {}, "sha512-xYqdZFUK/VYazNl/oCDYN+3WloWQwMfZxBoiNt6qNyk+xfOdi598muWE42rNZFp1kNOiqW936q5RhUdnpqElSg=="], "yoga-layout": ["yoga-layout@3.2.1", "", {}, "sha512-0LPOt3AxKqMdFBZA3HBAt/t/8vIKq7VaQYbuA8WxCgung+p9TVyKRYdpvCb80HcdTN2NkbIKbhNwKUfm3tQywQ=="], "youch": ["youch@4.1.0-beta.10", "", { "dependencies": { "@poppinss/colors": "^4.1.5", "@poppinss/dumper": "^0.6.4", "@speed-highlight/core": "^1.2.7", "cookie": "^1.0.2", "youch-core": "^0.3.3" } }, "sha512-rLfVLB4FgQneDr0dv1oddCVZmKjcJ6yX6mS4pU82Mq/Dt9a3cLZQ62pDBL4AUO+uVrCvtWz3ZFUL2HFAFJ/BXQ=="], "youch-core": ["youch-core@0.3.3", "", { "dependencies": { "@poppinss/exception": "^1.2.2", "error-stack-parser-es": "^1.0.5" } }, "sha512-ho7XuGjLaJ2hWHoK8yFnsUGy2Y5uDpqSTq1FkHLK4/oqKtyUU1AFbOOxY4IpC9f0fTLjwYbslUz0Po5BpD1wrA=="],68 unmodified lines "log-symbols/is-unicode-supported": ["is-unicode-supported@1.3.0", "", {}, "sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ=="], "mdast-util-find-and-replace/escape-string-regexp": ["escape-string-regexp@5.0.0", "", {}, "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw=="], "micromatch/picomatch": ["picomatch@2.3.2", "", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="], "miniflare/undici": ["undici@7.28.0", "", {}, "sha512-cRZYrTDwWznlnRiPjggAGxZXanty6M8RV1ff8Wm4LWXBp7/IG8v5DnOm74DtUBp9OONpK75YlPnIjQqX0dBDtA=="],4 unmodified lines "onetime/mimic-fn": ["mimic-fn@2.1.0", "", {}, "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg=="], "parse5/entities": ["entities@6.0.1", "", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="], "pkg-up/find-up": ["find-up@3.0.0", "", { "dependencies": { "locate-path": "^3.0.0" } }, "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg=="], "prompts/kleur": ["kleur@3.0.3", "", {}, "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w=="],infra/prs/README.md
22 unmodified lines2324252627282934 unmodified lines64656667686970717273747576777879808182838485868788899091929394959697989910010122 unmodified linesbun run pr edit <n> [--title t] [--body b | --body-file p] [--base branch] [--reason why] [--actor a] # any status, merged includedbun run pr close <n> [--body t | --body-file p] | reopen <n> [--body t | --body-file p]bun run pr merge <n> [--force] # from a base checkout; --force bypasses gates and says so in the event logbun run pr publish <n> --slug _<name> --signing-key <path> [--display-name n] [--product-url u] [--acknowledge-leak k]bun run pr sync [--json] # reconcile meta refs with the remote (exit 1 until it reports clean)bun run pr snapshot # regenerate prs.snapshot.jsonbun run pr serve [--port n] # live dashboard on 127.0.0.1 (also: bun run prs)34 unmodified lines
The body stays editable after a PR closes or merges — a public body worth correcting does not stop being public — and it does reach the site even after merge, since `packages/forge` re-reads `refs/meta/prs/<n>` at build time rather than freezing the body at merge. The title is editable too, but never reaches the site once a PR has merged: `packages/forge` takes a merged PR's title from the immutable merge commit subject, not from `pr.json`, so an edited title changes what `pr view` reports without ever showing up on a rebuilt page. The base does not stay editable at all: on a merged PR it is no longer a plan, it is the record of where the work landed; on a closed PR nothing landed, so the base is frozen there for a different reason — there is nothing to retarget until it is reopened. Neither edit touches `prs.snapshot.json`, which lags exactly as `pr open` and `pr close` do, and neither adds anything to the discussion: `packages/forge` renders `comment` and `review` events and ignores the rest.
## `pr publish` — a signed bundle from a repo the site cannot read
Kanban #48. `packages/forge` is structurally incapable of reading a private repo — `anonymous.ts` aborts if the connection authenticated at all, and `ANONYMOUS_ENV` neutralises eight inherited variables so a credential cannot arrive by any channel — so work in such a repo cannot reach the site by any extension of the normal path. `pr publish` is the other route: it runs in a clone of the repo the work came from, and it produces a **signed publication bundle** on a local ref, which a human then pushes to a separate, genuinely public publication repo. The site reads that repo anonymously like any other, and verifies the signature before believing a word of it.
Nothing about the source repo is in the bundle. There is no clone URL and no `sourceRepo` field, and adding one would defeat the design rather than extend it: a private repo's _name_ is the leak this exists to prevent, and a name leak fires at `git push`, before any gate runs, and cannot be undone. What identifies a publication publicly is a slug the publisher chooses.
### The slug namespace cannot collide with a repository
A slug is one segment beginning with `_` — `_widget`. That is not a convention, it is a disjointness proof: a server repo path must match `REPO_PATH` in `packages/forge/src/anonymous.ts`, whose every segment starts with an alphanumeric, while a publishable path segment must match `SEGMENT` in `packages/shared/src/site.ts`, which also allows a leading `_` or `-`. The difference between those two character classes is the namespace, and the two tests that assert it drive the real `parseRepoList` and the real `repoPathProblem` rather than restating either regex. This matters because `infra/scripts/site/publish.ts` resolves a path collision by silently dropping the _second_ arrival, ordered by repo activity — so a colliding slug would kill a page, and which page would depend on which repo was pushed to last. There is no collision to resolve.
### Every refusal is a refusal to publish
The command is built against an unattended agent running it with the wrong number, not against an attacker. So:
- The bundle is built from the PR's **immutable merge sha**. Nothing is read from a branch that could have moved.- `scanForLeaks` runs over the whole bundle **including the patch**. This is the only point in the system where a diff is scannable at all — `toServedSiteData` strips `patch` field by field and `verify.ts` fails the build if it comes back, so the build-time gate structurally cannot look at one. A hit **blocks**. It is cleared by `--acknowledge-leak "<path>::<pattern>"` naming that one hit, which is then recorded inside the signed bytes; there is deliberately no blanket flag, and an acknowledgement matching no hit is an error rather than a no-op, because a stale flag means the content moved since it was reviewed.- The full diff and prose are printed and the reviewer must **type the PR title back**. Never a bare `y`: the one failure a signature cannot catch is a human confirming the wrong bundle, and a keystroke that means yes to any prompt is the reflex that produces it. Without a TTY there is no human, so the prompt cannot be satisfied at all — an agent, a CI job, and a piped heredoc are all refused there.- The commit is signed with a key named by `--signing-key`. There is **no fallback** to `user.signingkey` or to any other ambient configuration, because a key found by fallback is a key nobody decided to use. If the key cannot be used, `commit-tree` fails and nothing is written.- **Nothing is pushed.** The ref is local and the command says so; a human pushes it.
An agent can run every step of this and publish nothing.
### What the guarantee actually rests on
All of it reduces to one deployment fact: _the signing key is not usable by an unattended process_. If the key is an unencrypted file on disk, or is loaded in a running `ssh-agent`, or is the same key that pushes, then an agent that can push can also sign, and this degrades to a legible audit trail after the fact rather than prevention.
Nothing in this repository configures signing today — `git config --get-regexp 'gpg|commit.gpgsign|user.signingkey'` returns nothing and `ssh-add -l` reports no identities. So the machinery is real and the property it depends on is currently unestablished. Do not describe `pr publish` as preventing anything before answering that.
### Revocation
Two mechanisms, and the build treats them identically. A **signed tombstone** on the publication ref is a positive statement that a stale clone's push cannot undo; **deleting the ref** is the other. Neither depends on the other having worked. Neither recovers caches, crawlers, or anyone who already cloned, and nothing here promises otherwise.
## Merge gates (prs.toml)
`pr merge` is where policy is enforced, not suggested. Gates are configured in [prs.toml](prs.toml) (committed; per-machine overrides in `prs.local.toml`, gitignored):infra/prs/src/help.ts
128 unmodified lines129130131132133134135136137138139140141142143144145146147148149150151152153154155156128 unmodified lines ACTOR, ], }, publish: { usage: "pr publish <n> --slug _<name> [--display-name n] [--product-url u] --signing-key p [--acknowledge-leak id]", summary: "sign a merged PR into a publication bundle, from a clone of the source repo", flags: [ [ "--slug _n", "publication slug; must start with `_` so it cannot collide with a server path", ], ["--display-name n", "name the site shows (default: the PR title)"], ["--product-url u", "where the published work lives, if anywhere"], [ "--signing-key p", "ssh key to sign the bundle with — without it nothing is published", ], [ "--acknowledge-leak id", "override one recorded leak-scan hit; repeatable, and recorded in the bundle", ], ], }, sync: { usage: "pr sync [--json]", summary:infra/prs/src/index.ts
10 unmodified lines11121314151617189 unmodified lines2829303132333450 unmodified lines8586878889909192939495969798991001253 unmodified lines13541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152146 unmodified lines1568156915701571157215731574157510 unmodified lines * bun run pr edit <n> [--title t] [--body b | --body-file p] [--base br] [--reason w] [--actor a] * bun run pr close <n> [--body t] [--actor a] | reopen <n> [--body t] [--actor a] * bun run pr merge <n> [--force] [--actor a] * bun run pr publish <n> --slug _<name> --signing-key <path> * signed publication bundle (kanban #48) * bun run pr sync [--json] reconcile meta refs with the remote * bun run pr snapshot regenerate prs.snapshot.json * bun run pr serve [--port n] live dashboard (127.0.0.1)9 unmodified lines */import { readFileSync } from "node:fs";import { parseArgs } from "node:util";import { publicationRef } from "@code/shared/publication";import { enabledGateNames, gatesDisabledLocally,50 unmodified lines PR_STATUSES, reviewState,} from "./model";import { buildRecord, confirmationMatches, leakVerdict, type MergeMaterial, type PublicationIdentity, refusals, scanBundle, signBundle,} from "./publish";import { snapshotPath, writeSnapshot } from "./snapshot";import { runSync } from "./sync";import { serve } from "./serve";1253 unmodified lines runSync(loadConfig(), { json: values.json });}
/* -------------------------------------------------------------- publish */
/** * `pr publish <n>` — export a merged PR as a signed publication bundle. * * See `publish.ts` for the design and for what its guarantee does and does not * rest on. This function is the I/O around those decisions: it reads the * merge, prints everything, demands the title back from a human, and refuses * in every direction that is not "a person looked at this and signed it". * * Nothing is pushed. The ref is local, and a human pushes it to the * publication repo once they are satisfied — which is also what keeps this * command runnable on a machine with no network at all. */function cmdPublish(argv: string[]): void { const { values, positionals } = parseArgs({ args: argv, allowPositionals: true, options: { slug: { type: "string" }, "display-name": { type: "string" }, "product-url": { type: "string" }, "signing-key": { type: "string" }, "acknowledge-leak": { type: "string", multiple: true, default: [] }, }, });
const number = Number.parseInt(positionals[0] ?? "", 10); if (!Number.isInteger(number)) { throw new Error( "usage: pr publish <n> --slug _<name> --display-name <name> --signing-key <path>", ); } const pr = readPr(number);
const identity: PublicationIdentity = { slug: values.slug ?? "", displayName: values["display-name"] ?? pr.title, productUrl: values["product-url"] ?? null, }; const signingKey = values["signing-key"] ?? null;
// Read the merge at its immutable sha. Nothing here consults a branch. const mergeSha = pr.mergeSha; const merge: MergeMaterial = mergeSha === null ? { mergeSha: "", mergedAt: "", mergedBy: "", patch: "", numstat: "", nameStatus: "", } : readMergeMaterial(mergeSha);
const provisional = buildRecord(pr, merge, identity, []); const scan = scanBundle(provisional); const verdict = leakVerdict(scan.hits, values["acknowledge-leak"] ?? []);
const problems = refusals(identity, pr, verdict, signingKey); if (problems.length > 0) { throw new Error( `refusing to publish pr #${number}:\n ${problems.join("\n ")}\n\n` + "Nothing was written and nothing was pushed.", ); } // Narrowing for the compiler; `refusals` already refused both of these. if (signingKey === null || mergeSha === null) return;
const record = buildRecord(pr, merge, identity, verdict.acknowledged);
console.log(`\n${"=".repeat(72)}`); console.log(`PR #${pr.number}: ${pr.title}`); console.log( `branch ${pr.branch} → merged ${merge.mergedAt} by ${merge.mergedBy}`, ); console.log(`publishing as ${record.slug} ("${record.displayName}")`); console.log(`${"-".repeat(72)}\n${pr.body}`); for (const comment of record.comments) { console.log( `${"-".repeat(72)}\n[${comment.at}] ${comment.actor}:\n${comment.body}`, ); } console.log(`${"-".repeat(72)}\n${record.patch}`); console.log(`${"=".repeat(72)}`); console.log( `leak scan: ${scan.fields} fields, ${scan.chars} characters ` + `(prose ${scan.prose.fields}/${scan.prose.chars}, patch ${scan.patch.fields}/${scan.patch.chars}), ` + `${scan.hits.length} hit(s), ${record.acknowledged.length} acknowledged`, ); console.log( "\nEverything above becomes a public page, permanently, and cannot be recalled.", );
confirmByTitle(pr.title);
const commit = signBundle( { kind: "publication", record }, { repoRoot, slug: record.slug, signingKey, parent: tryGit([ "rev-parse", "--verify", "--quiet", `${publicationRef(record.slug)}^{commit}`, ]), }, ); console.log( `\nsigned ${commit.slice(0, 8)} at ${publicationRef(record.slug)} — LOCAL ONLY.\n` + "Nothing has been published yet. Push it to the publication repo when you are ready:\n" + ` git push <publication-remote> ${publicationRef(record.slug)}`, );}
/** The merge, read at a sha that cannot move. */function readMergeMaterial(sha: string): MergeMaterial { const [mergedAt, mergedBy] = git([ "log", "-1", "--format=%cI%x00%an", sha, ]).split("\0"); const range = [`${sha}~1`, sha]; return { mergeSha: sha, mergedAt: mergedAt ?? "", mergedBy: mergedBy ?? "", patch: git(["diff", "-M", ...range]), numstat: git(["diff", "-M", "--numstat", ...range]), nameStatus: git(["diff", "-M", "--name-status", ...range]), };}
/** * Demand the PR title, typed, from a terminal. * * Two refusals, and the first is the one that matters most here. Without a TTY * there is no human, and the honest answer to "is a person confirming this" is * no — so an unattended agent, a CI job, or a piped heredoc cannot satisfy * this prompt at all. That is the fail-closed property stated as code rather * than as a comment. */function confirmByTitle(title: string): void { if (!process.stdin.isTTY) { throw new Error( "refusing to publish: stdin is not a terminal, so nothing here is a " + "human confirming what is about to become public. Nothing was written.", ); } process.stdout.write(`\nType the PR title exactly to publish:\n> `); const typed = readFileSync("/dev/stdin", "utf8").split("\n")[0] ?? ""; if (!confirmationMatches(typed, title)) { throw new Error( "refusing to publish: what was typed is not the PR title. Nothing was written.", ); }}
/* ----------------------------------------------------------------- main */
// Built from the same table the subcommands document themselves out of, so a46 unmodified lines return cmdCloseReopen(rest, true); case "merge": return cmdMerge(rest); case "publish": return cmdPublish(rest); case "sync": return cmdSync(rest); case "snapshot":infra/prs/src/publish.test.ts
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374import { execFileSync } from "node:child_process";import { mkdtempSync, rmSync } from "node:fs";import { tmpdir } from "node:os";import { join } from "node:path";import { PUBLICATION_SCHEMA } from "@code/shared/publication";import { describe, expect, it } from "vitest";import type { Pr } from "./model";import { buildRecord, confirmationMatches, leakKey, leakVerdict, type MergeMaterial, type PublicationIdentity, publicationFiles, refusals, scanBundle, signBundle,} from "./publish";
const IDENTITY: PublicationIdentity = { slug: "_widget", displayName: "Widget", productUrl: "https://example.invalid/widget",};
function mergedPr(overrides: Partial<Pr> = {}): Pr { return { number: 12, branch: "fix/trailing-commas", base: "main", title: "teach the parser to keep trailing commas", body: "The parser dropped them.\n", status: "merged", openedBy: "Publisher", createdAt: "2026-07-13T10:00:00Z", headSha: "1111111111111111111111111111111111111111", mergedAt: "2026-07-14T09:12:03Z", mergeSha: "3f7a1c9d2b8e4f60a1c3d5e7f9b2a4c6d8e0f2a4", events: [ { at: "2026-07-13T11:00:00Z", actor: "reviewer", type: "comment", body: "reads well", }, { at: "2026-07-13T12:00:00Z", actor: "reviewer", type: "review", verdict: "approve", body: "ship it", }, { at: "2026-07-14T09:12:03Z", actor: "Publisher", type: "merged" }, ], ...overrides, };}
function material(overrides: Partial<MergeMaterial> = {}): MergeMaterial { return { mergeSha: "3f7a1c9d2b8e4f60a1c3d5e7f9b2a4c6d8e0f2a4", mergedAt: "2026-07-14T09:12:03+00:00", mergedBy: "Publisher", patch: "diff --git a/src/parse.ts b/src/parse.ts\n@@ -1 +1 @@\n-a\n+a,\n", numstat: "18\t4\tsrc/parse.ts\n62\t0\tsrc/parse.test.ts\n", nameStatus: "M\tsrc/parse.ts\nA\tsrc/parse.test.ts\n", ...overrides, };}
describe("publicationFiles", () => { it("zips numstat with name-status and sorts by path", () => { expect(publicationFiles(material().numstat, material().nameStatus)).toEqual( [ { path: "src/parse.test.ts", status: "A", added: 62, removed: 0 }, { path: "src/parse.ts", status: "M", added: 18, removed: 4 }, ], ); });
it("does not depend on git's ordering", () => { // The bundle's bytes are signed, so the same merge has to produce the same // list however git happened to order the two outputs. const forward = publicationFiles( "18\t4\tsrc/parse.ts\n62\t0\tsrc/parse.test.ts\n", "M\tsrc/parse.ts\nA\tsrc/parse.test.ts\n", ); const reversed = publicationFiles( "62\t0\tsrc/parse.test.ts\n18\t4\tsrc/parse.ts\n", "A\tsrc/parse.test.ts\nM\tsrc/parse.ts\n", ); expect(reversed).toEqual(forward); });
it("orders by code unit, not by the machine's collation", () => { // The case that separates the two comparators, and the reason the sort is // `compareCodeUnits` rather than `localeCompare`: git is case-sensitive, so // `A.ts` and `a.ts` are two real files, and ICU collation orders them the // opposite way from their code units — `"A.ts".localeCompare("a.ts")` is 1 // where `"A.ts" < "a.ts"` is true. That ordering feeds the bundle's *signed* // bytes, so under `localeCompare` two reviewers on differently-configured // machines rebuild the same merge, get different bytes, and a signature // verifies against one of them and not the other. The existing // order-independence case above cannot catch this: `src/parse.ts` and // `src/parse.test.ts` sort the same way under both. const files = publicationFiles( "1\t0\ta.ts\n1\t0\tA.ts\n", "A\ta.ts\nA\tA.ts\n", ); expect(files.map((file) => file.path)).toEqual(["A.ts", "a.ts"]); });
it("reads a binary file's dashes as zero rather than NaN", () => { expect(publicationFiles("-\t-\tlogo.png\n", "A\tlogo.png\n")).toEqual([ { path: "logo.png", status: "A", added: 0, removed: 0 }, ]); });});
describe("buildRecord", () => { it("is byte-stable across runs", () => { const left = buildRecord(mergedPr(), material(), IDENTITY, []); const right = buildRecord(mergedPr(), material(), IDENTITY, []); expect(JSON.stringify(right)).toBe(JSON.stringify(left)); });
it("carries comments and reviews but not the merge event", () => { const record = buildRecord(mergedPr(), material(), IDENTITY, []); expect(record.comments.map((comment) => comment.body)).toEqual([ "reads well", "ship it", ]); });
it("takes its identity from the publisher, never from the PR", () => { const record = buildRecord(mergedPr(), material(), IDENTITY, []); expect(record.slug).toBe("_widget"); expect(record.displayName).toBe("Widget"); // Nothing anywhere in the record says where the work came from. expect(JSON.stringify(record)).not.toContain("base"); });});
describe("scanBundle sees the patch", () => { /** * The hole this whole command exists to close. * * The build-time gate cannot scan a diff: `toServedSiteData` strips `patch` * field by field and `verify.ts` fails the build if it ever comes back, so * the served `site.json` has no patch text in it at all. Publication time is * the only moment a diff is scannable, and this asserts that the scan * actually reaches one — with a hit that exists *only* in the patch and * nowhere in the prose. */ it("finds a leak that appears only inside the diff", () => { const record = buildRecord( mergedPr(), material({ patch: "diff --git a/run.sh b/run.sh\n@@ -1 +1 @@\n" + "-echo hi\n+cat /Users/someone/.ssh/config\n", }), IDENTITY, [], ); const report = scanBundle(record); const hits = report.hits.filter( (hit) => hit.pattern === "absolute /Users path", ); expect(hits).toHaveLength(1); expect(hits[0]?.bucket).toBe("patch"); // And the excerpt is shorter than the match, per leak-scan's own rule. expect(hits[0]?.excerpt.length).toBeLessThan("/Users/someone/".length); });
it("reports a clean bundle as clean", () => { const report = scanBundle( buildRecord(mergedPr(), material(), IDENTITY, []), ); expect(report.hits).toEqual([]); // Rule 7: the scan has to have seen something before "clean" means anything. expect(report.fields).toBeGreaterThan(0); expect(report.patch.chars).toBeGreaterThan(0); });});
describe("leakVerdict", () => { const hit = { pattern: "absolute /Users path", bucket: "patch" as const, path: "patch", excerpt: "/Users/…", };
it("blocks an unacknowledged hit", () => { expect(leakVerdict([hit], []).blocking).toEqual([hit]); });
it("lets a specifically acknowledged hit through, and records it", () => { const verdict = leakVerdict([hit], [leakKey(hit)]); expect(verdict.blocking).toEqual([]); expect(verdict.acknowledged).toEqual([ { pattern: hit.pattern, path: hit.path, excerpt: hit.excerpt }, ]); });
it("does not accept a blanket acknowledgement", () => { // One keystroke must not clear a diff nobody read. `all` names no hit, so // it blocks the hit and is itself reported as stale. const verdict = leakVerdict([hit], ["all"]); expect(verdict.blocking).toEqual([hit]); expect(verdict.unmatched).toEqual(["all"]); });
it("treats an acknowledgement matching no hit as an error", () => { expect(leakVerdict([], ["patch::uuid"]).unmatched).toEqual(["patch::uuid"]); });});
describe("confirmationMatches", () => { it("accepts the exact title", () => { expect(confirmationMatches(mergedPr().title, mergedPr().title)).toBe(true); });
it("forgives a terminal's trailing whitespace", () => { expect(confirmationMatches(`${mergedPr().title} `, mergedPr().title)).toBe( true, ); });
it("rejects y, which is the reflex the prompt exists to defeat", () => { expect(confirmationMatches("y", mergedPr().title)).toBe(false); expect(confirmationMatches("yes", mergedPr().title)).toBe(false); expect(confirmationMatches("", mergedPr().title)).toBe(false); });
it("rejects a near miss rather than fuzzy-matching it", () => { expect(confirmationMatches("Teach the parser", mergedPr().title)).toBe( false, ); });});
describe("refusals", () => { const clean = leakVerdict([], []);
it("passes a merged PR with a slug, a key and no hits", () => { expect(refusals(IDENTITY, mergedPr(), clean, "/keys/publish")).toEqual([]); });
it("refuses a slug outside the publication namespace", () => { const problems = refusals( { ...IDENTITY, slug: "org/widget" }, mergedPr(), clean, "/keys/publish", ); expect(problems[0]).toMatch(/is not a publication slug/); });
it("refuses a PR that is not merged", () => { const problems = refusals( IDENTITY, mergedPr({ status: "open", mergeSha: null }), clean, "/keys/publish", ); expect(problems.some((line) => line.includes("only a merged PR"))).toBe( true, ); });
it("refuses when no signing key was named, with no ambient fallback", () => { const problems = refusals(IDENTITY, mergedPr(), clean, null); expect(problems.some((line) => line.includes("no --signing-key"))).toBe( true, ); });});
describe("signBundle", () => { it("refuses, writing nothing, when the signing key cannot be used", () => { const root = mkdtempSync(join(tmpdir(), "prs-publish-")); try { execFileSync("git", ["init", "--quiet", root]); const record = buildRecord(mergedPr(), material(), IDENTITY, []); expect(() => signBundle( { kind: "publication", record }, { repoRoot: root, slug: "_widget", signingKey: join(root, "no-such-key"), parent: null, }, ), ).toThrow(/could not be signed/);
// Fails closed: the ref does not exist, so nothing could be pushed. const refs = execFileSync("git", ["for-each-ref", "refs/meta"], { cwd: root, encoding: "utf8", }); expect(refs.trim()).toBe(""); } finally { rmSync(root, { recursive: true, force: true }); } });
it("writes a signed, verifiable ref when the key works", () => { const root = mkdtempSync(join(tmpdir(), "prs-publish-")); try { execFileSync("git", ["init", "--quiet", root]); const key = join(root, "signer"); execFileSync("ssh-keygen", [ "-q", "-t", "ed25519", "-N", "", "-C", "t", "-f", key, ]);
const record = buildRecord(mergedPr(), material(), IDENTITY, []); const commit = signBundle( { kind: "publication", record }, { repoRoot: root, slug: "_widget", signingKey: key, parent: null }, );
const allowed = join(root, "allowed"); execFileSync("sh", [ "-c", `printf 'p@example.invalid %s\\n' "$(cat ${key}.pub)" > ${allowed}`, ]); // The reader's own check, run against what the writer produced: the two // halves of the design meeting in one test. const verified = execFileSync( "git", [ "-c", "gpg.format=ssh", "-c", `gpg.ssh.allowedSignersFile=${allowed}`, "verify-commit", "--raw", commit, ], { cwd: root, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }, ); expect(String(verified)).toBeDefined();
const blob = execFileSync("git", ["show", `${commit}:publication.json`], { cwd: root, encoding: "utf8", }); expect(blob).toContain('"slug": "_widget"'); expect(blob).toContain("diff --git"); } finally { rmSync(root, { recursive: true, force: true }); } });
it("records an acknowledged hit inside the signed bytes", () => { const record = buildRecord(mergedPr(), material(), IDENTITY, [ { pattern: "absolute /Users path", path: "patch", excerpt: "/Users/…" }, ]); expect(record.acknowledged).toHaveLength(1); expect(record.schema).toBe(PUBLICATION_SCHEMA); });});infra/prs/src/publish.ts
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363/** * `pr publish <n>` — the one path by which work from a repo the public site * cannot read may become a page on it. * * The write half of kanban #48's Design C. It runs in a clone of the repo the * work came from, and everything it produces is a signed commit on a ref that * is pushed, by a human, to a separate public publication repo. * * ## Every step here exists to make the accidental path fail closed * * The failure this is built against is not an attacker. It is an unattended * agent running one command with the wrong number. So the ordering matters: * * 1. The bundle is built from an **immutable merge sha**. Nothing is read from * a branch that could have moved. * 2. `scanForLeaks` runs over the bundle **including the patch**. This is the * only point in the entire system where a diff is scannable at all — the * served `site.json` carries no patch text, so the build-time gate * structurally cannot look at one. A hit blocks; it does not warn. * 3. The full diff and prose are printed, and the reviewer must type the PR * title back. Never a bare `y`: the one failure a signature cannot catch is * a human confirming the wrong bundle, and a keystroke that means "yes" to * any prompt is exactly the reflex that produces it. * 4. The commit is signed with a key named explicitly on the command line. * There is deliberately **no fallback to `user.signingkey`** or to any * other ambient configuration, because falling back to ambient config is * precisely how a key that an agent can reach gets used without anyone * deciding that it should be. * 5. Nothing is pushed. This command writes a local ref and stops. * * An agent can run every step of this and publish nothing. * * ## What this cannot promise * * The guarantee reduces entirely to "the signing key is not usable by an * unattended process". If the key is an unencrypted file on disk, or is loaded * in a running `ssh-agent`, or is the same key that pushes, then an agent that * can push can also sign and this is Design A — a legible audit trail after * the fact, not prevention. Nothing in this repository configures signing * today, so that property is currently unestablished rather than merely * unverified. Do not describe this command as preventing anything without * first answering that question. * * ## Why the leak scanner is imported rather than reimplemented * * `scanForLeaks` and its eight patterns live in `infra/scripts/site/leak-scan.ts` * and are pure — no I/O, `unknown` in. Importing across `infra/` is a new edge * and it is the right one: the alternative is a second copy of the pattern list * that drifts from the first, and the two would disagree exactly when a new * pattern was added for a reason. */import { execFileSync, spawnSync } from "node:child_process";import { compareCodeUnits } from "@code/shared/order";import { type AcknowledgedLeak, type PublicationBundle, type PublicationRecord, PUBLICATION_BLOB, PUBLICATION_SCHEMA, isPublicationSlug, publicationRef, serializeBundle,} from "@code/shared/publication";import { type LeakHit, leakProblem, scanForLeaks,} from "../../scripts/site/leak-scan";import type { Pr } from "./model";
/** Everything read off the merge, before any decision is made about it. */export interface MergeMaterial { readonly mergeSha: string; readonly mergedAt: string; readonly mergedBy: string; readonly patch: string; /** `git diff --numstat <sha>~1 <sha>`. */ readonly numstat: string; /** `git diff --name-status <sha>~1 <sha>`. */ readonly nameStatus: string;}
/** The public identity the publisher chose. Never derived from the repo. */export interface PublicationIdentity { readonly slug: string; readonly displayName: string; readonly productUrl: string | null;}
/** * `--numstat` and `--name-status` into one file list, sorted by path. * * Sorted here rather than trusting git's order, because the bundle's bytes are * signed: two runs over the same merge have to produce the same list, and * `git diff`'s ordering is stable in practice but is not a promise this cares * to depend on. A binary file reports `-` for both counts and becomes 0/0, * which is what the numbers mean for it. */export function publicationFiles( numstat: string, nameStatus: string,): readonly { path: string; status: string; added: number; removed: number }[] { const status = new Map<string, string>(); for (const line of nameStatus.split("\n")) { if (line === "") continue; const [code, path] = line.split("\t"); if (code === undefined || path === undefined) continue; status.set(path, code); }
const files = numstat.split("\n").flatMap((line) => { if (line === "") return []; const [added, removed, path] = line.split("\t"); if (added === undefined || removed === undefined || path === undefined) { return []; } return [ { path, status: status.get(path) ?? "M", added: added === "-" ? 0 : Number.parseInt(added, 10), removed: removed === "-" ? 0 : Number.parseInt(removed, 10), }, ]; }); return [...files].sort((left, right) => compareCodeUnits(left.path, right.path), );}
/** * The bundle, assembled deterministically. * * Pure, and that is what makes byte-stability testable: the same merge and the * same PR record produce the same bytes on every run, so the signature is over * the content rather than over an accident of when it ran. */export function buildRecord( pr: Pr, merge: MergeMaterial, identity: PublicationIdentity, acknowledged: readonly AcknowledgedLeak[],): PublicationRecord { return { schema: PUBLICATION_SCHEMA, slug: identity.slug, displayName: identity.displayName, productUrl: identity.productUrl, number: pr.number, title: pr.title, body: pr.body, branch: pr.branch, openedBy: pr.openedBy, mergeSha: merge.mergeSha, mergedAt: merge.mergedAt, mergedBy: merge.mergedBy, comments: pr.events .filter((event) => event.type === "comment" || event.type === "review") .map((event) => ({ at: event.at, actor: event.actor, body: event.body ?? "", })), files: publicationFiles(merge.numstat, merge.nameStatus), patch: merge.patch, acknowledged: [...acknowledged].sort((left, right) => compareCodeUnits(leakKey(left), leakKey(right)), ), };}
/** The stable identity of one hit, for acknowledging it on the command line. */export function leakKey(hit: { readonly path: string; readonly pattern: string;}): string { return `${hit.path}::${hit.pattern}`;}
export interface LeakVerdict { /** Hits nobody acknowledged. Non-empty means the publication is refused. */ readonly blocking: readonly LeakHit[]; /** Hits that were acknowledged, to be recorded inside the signed bytes. */ readonly acknowledged: readonly AcknowledgedLeak[]; /** Acknowledgements that matched no hit — a stale flag, which is an error. */ readonly unmatched: readonly string[];}
/** * Which hits block, given what the publisher acknowledged. * * An acknowledgement names one hit by `path::pattern`. Blanket flags are * deliberately impossible: `--acknowledge-leak all` would be one keystroke * away from publishing every hit in a diff nobody read, which is the same * shape of accident as kanban #48's Design B accepting one extra integer. * * An acknowledgement that matches nothing is an error rather than a no-op. * A stale flag means the diff moved under the reviewer since they looked at * it, and the safe reading of "I approved a hit that is no longer there" is * that the review was of different content. */export function leakVerdict( hits: readonly LeakHit[], acknowledgements: readonly string[],): LeakVerdict { const wanted = new Set(acknowledgements); const blocking = hits.filter((hit) => !wanted.has(leakKey(hit))); const acknowledged = hits .filter((hit) => wanted.has(leakKey(hit))) .map((hit) => ({ pattern: hit.pattern, path: hit.path, excerpt: hit.excerpt, })); const present = new Set(hits.map(leakKey)); const unmatched = acknowledgements.filter((key) => !present.has(key)); return { blocking, acknowledged, unmatched };}
/** * Whether the typed confirmation is the PR title. * * Trailing whitespace is forgiven because a terminal adds it; nothing else is. * Case is not folded and the text is not normalised: the point of the prompt * is that the reviewer read the title off the screen, and a fuzzy match is a * prompt that can be satisfied without having done so. */export function confirmationMatches(typed: string, title: string): boolean { return typed.trimEnd() === title.trimEnd();}
/** Reasons this publication is refused, in the order a reader should see them. */export function refusals( identity: PublicationIdentity, pr: Pr, verdict: LeakVerdict, signingKey: string | null,): readonly string[] { const problems: string[] = []; if (!isPublicationSlug(identity.slug)) { problems.push( `--slug ${JSON.stringify(identity.slug)} is not a publication slug: one segment ` + "beginning with `_`, e.g. `_widget`. That namespace is the reason a " + "publication can never collide with a repository path on the server.", ); } if (pr.status !== "merged" || pr.mergeSha === null) { problems.push( `pr #${pr.number} is ${pr.status} — only a merged PR has the immutable ` + "merge sha a bundle is built from", ); } if (signingKey === null) { problems.push( "no --signing-key given. This command never falls back to " + "`user.signingkey` or any other ambient configuration, because a key " + "found by fallback is a key nobody decided to use.", ); } for (const hit of verdict.blocking) { problems.push( `${leakProblem(hit)} — acknowledge it explicitly with ` + `--acknowledge-leak ${JSON.stringify(leakKey(hit))} if it is genuinely publishable`, ); } for (const key of verdict.unmatched) { problems.push( `--acknowledge-leak ${JSON.stringify(key)} matches no hit in this bundle — ` + "the content moved since it was reviewed, so the review was of something else", ); } return problems;}
/** * Scan the whole bundle, patch included. * * The record is serialized and re-parsed rather than handed over as an object, * so the scanner sees exactly the bytes that will be signed. A scan of the * in-memory value would be a scan of something adjacent to the artifact. */export function scanBundle( record: PublicationRecord,): ReturnType<typeof scanForLeaks> { const bundle: PublicationBundle = { kind: "publication", record }; return scanForLeaks(JSON.parse(serializeBundle(bundle)));}
export interface SignOptions { readonly repoRoot: string; readonly slug: string; readonly signingKey: string; /** Existing tip of the bundle chain, for an update rather than a create. */ readonly parent: string | null;}
/** * Commit the bundle, signed, and move the local ref. * * `-S` with `gpg.format=ssh` and an explicit `user.signingkey`. If the key * cannot be used — passphrase-protected with no agent, on a hardware token * that is not present, absent altogether — `commit-tree` fails and this * throws, which is the whole design working: nothing is written, nothing is * pushed, and no private name has been emitted anywhere. */export function signBundle( bundle: PublicationBundle, options: SignOptions,): string { const git = (args: string[], input?: string): string => execFileSync("git", args, { cwd: options.repoRoot, encoding: "utf8", ...(input === undefined ? {} : { input }), }).trim();
const blob = git(["hash-object", "-w", "--stdin"], serializeBundle(bundle)); const tree = git(["mktree"], `100644 blob ${blob}\t${PUBLICATION_BLOB}\n`);
const message = bundle.kind === "tombstone" ? `withdraw ${bundle.tombstone.slug}` : `publish ${bundle.record.slug}: ${bundle.record.title}`;
const args = [ "-c", "gpg.format=ssh", "-c", `user.signingkey=${options.signingKey}`, "commit-tree", tree, "-S", "-m", message, ]; if (options.parent !== null) args.push("-p", options.parent);
const signed = spawnSync("git", args, { cwd: options.repoRoot, encoding: "utf8", }); if (signed.status !== 0) { throw new Error( `refusing to publish: the bundle could not be signed with ${options.signingKey} — ` + `${(signed.stderr ?? "").trim()}\n` + "Nothing was written and nothing was pushed.", ); } const commit = signed.stdout.trim();
const ref = publicationRef(options.slug); execFileSync( "git", [ "update-ref", ref, commit, ...(options.parent === null ? [""] : [options.parent]), ], { cwd: options.repoRoot, encoding: "utf8" }, ); return commit;}infra/scripts/site-build.ts
31 unmodified lines3233343536373839404142434445464748495051525354555657585960616245 unmodified lines1081091109111111211311413 unmodified lines12812913013113213313413513613713811113914014114219 unmodified lines16216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319421 unmodified lines21621721821922022122222322422522622722822923023123223323423523623723823924024124224316624424524624731 unmodified linesimport { buildSiteData, DEFAULT_HOST } from "@code/forge";import { repoRoot } from "./affected";import { LEAK_PATTERNS } from "./site/leak-scan";import { writeCards } from "./site/og-card";import { publicationProblems, publicationSlugs, publicationVerdict, withPublications,} from "./site/publications";import { publishSite } from "./site/publish";import { verifyBuiltSite, VerificationError } from "./site/verify";
const WEB_DIR = "apps/web";
/** * What the site calls itself, hand-mirrored from `apps/web/src/site.ts`. * * Duplicated on purpose: `apps/web/index.html` and the manifest already * carry a copy each (they cannot import from `src/`), and `meta.test.ts` * asserts the copies against `SITE_NAME`. This is one more copy of the same * string, over the same reason — infra never imports from an app — and the * OG-card wordmark is inspected in a rendered PNG rather than against the * source, so a rename here that missed a copy would show up on a card the * moment someone pasted a link. */const SITE_NAME = "code.fugl.dev";
/** * One of the app's own scripts, run in the app's directory. *45 unmodified lines result: Awaited<ReturnType<typeof buildSiteData>>, options: SiteBuildOptions = {},): Promise<void> { const { data, enumeratedCount, publicTips } = result; const { data, enumeratedCount, publicTips, publicationScans, host } = result; const budget = options.budget; // The published count always; the enumerated count only when asked for. //13 unmodified lines );
// 2. Render, and write what the browser gets. // // Publications are folded into the data first, so everything downstream — // pages, `site.json`, the OG cards, and the sets `verify.ts` compares — // sees one kind of thing. Before this, slugs entered `expected` and nothing // put them in the artifact, so the first build with a verified bundle failed // the set comparison. `withPublications` also drops the publication repos' // own empty cards; see its doc for why that keeps the sets agreeing. const site = withPublications(data, publicationScans, host); const published = await publishSite(data, { const published = await publishSite(site, { root: repoRoot, source: "forge", budget,19 unmodified lines // present here is the whole difference between a deploy and a demo. await runWebScript("prerender", ["--in", published.prerenderInputFile]);
// 4a. Generate the Open Graph cards. Runs after the prerenderer has written // `dist/` (so the file the cards land in already exists) and before // verify reads it back (so the count check below is not overtaken by a // failed deploy step). // // The count is the ADDENDUM rule 7 floor: `writeCards` returns how many // PNGs it wrote, and the assertion below refuses to call the step done // unless that number equals `repos.length + 1`. `verify.ts`'s existing // checks — `strayDataFiles`, `repoPagesInDist`, `SITE_ROOT_DOCUMENTS` — // read `data/`, `latest/` and a fixed list respectively, so a card that // silently failed to generate under `og/` would be invisible to every // one of them; the count here is what makes it visible. const cards = await writeCards( join(repoRoot, WEB_DIR, "dist"), SITE_NAME, published.site, ); const expectedCards = published.site.repos.length + 1; if (cards.count !== expectedCards) { throw new Error( `og-card: wrote ${String(cards.count)} PNG(s), expected ${String(expectedCards)} (default + ${String(published.site.repos.length)} repos)`, ); } console.log( ` wrote ${String(cards.count)} OG card(s): 1 default + ${String(cards.repoCards.length)} repo(s), ${String(cards.defaultCard.bytes)} B default`, );
// 5. Read the artifact back. See site/verify.ts for why this is not // redundant with the three steps above having succeeded. //21 unmodified lines .map((tip) => tip.path) .filter((path) => admitted.has(path));
// Publication slugs join that set, and they arrive by the same kind of route // rather than by a different kind of authority. `publicationSlugs` reads only // `scan.admitted`, which `packages/forge/src/publication.ts` fills solely // with bundles whose commit this build handed to `git verify-commit` against // the `allowed_signers` file the publication repo itself carries. So a slug // in `expected` is this build's own statement that it checked a signature, // which is what keeps the comparison in `verify.ts` an assertion rather than // a restatement of its own input. There is no committed allowlist anywhere // in this design and nothing here consults one; a list handed to the build // would be kanban #48's Design B, which was rejected for exactly that. // // The floor is checked before any of it is believed, on the same reasoning // as the OG-card count above: a publication read that examined nothing must // not be indistinguishable from a publication repo with nothing to say. const bundleProblems = publicationProblems(publicationScans); if (bundleProblems.length > 0) { throw new Error( `publications: ${bundleProblems.length} problem(s), site not published:\n ${bundleProblems.join("\n ")}`, ); } const expectedPaths = [...repoPaths, ...publicationSlugs(publicationScans)]; console.log(` ${publicationVerdict(publicationScans)}`);
const verified = await verifyBuiltSite(join(repoRoot, WEB_DIR, "dist"), { generatedAt: data.generatedAt, repoPaths, repoPaths: expectedPaths, }); console.log( `verified ${WEB_DIR}/dist — real data, generated ${data.generatedAt}`,infra/scripts/site/og-card.test.ts
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217/** * What a card is checked for, and why each check is here. * * A generated Open Graph card is exactly the kind of build product that * fails silently. Nothing on the site renders it — Slack does — and a card * that came out zero bytes, or a card that came out different every build, * is discoverable only by paying attention to what other people's link * previews look like. So this file leans on `ADDENDUM-load-bearing-tests.md` * rather than on inspection: every assertion below has a mutation that makes * it go red, listed in the commit body, because otherwise the test is a name * in a suite. */import { mkdir, readFile, rm, writeFile } from "node:fs/promises";import { join } from "node:path";import { toServedSiteData } from "@code/shared/site";import { beforeAll, describe, expect, it } from "vitest";import { FIXTURE_SITE_DATA } from "../../../apps/web/src/fixtures/site-data";import { OG_DEFAULT_URL_PATH, OG_HEIGHT, OG_WIDTH, renderCard, renderDefaultCardSpec, renderRepoCardSpec, repoCardUrlPath, writeCards,} from "./og-card";
/** * A `dist/`-shaped scratch directory this test owns. `os.tmpdir()` is shared * across every process on the machine — including the other worktrees this * repo carries — and asserting over a namespace nobody else can write to is * what rule 5 of the addendum names as strictly stronger than counting * entries in a shared one. */async function scratchDist(name: string): Promise<string> { const root = join(import.meta.dirname, "../.test-scratch", name); await rm(root, { recursive: true, force: true }); await mkdir(root, { recursive: true }); return root;}
const SITE_NAME = "code.fugl.dev";const SITE = toServedSiteData(FIXTURE_SITE_DATA, "fixture");
/** * Warm the resvg wasm and the five font files once, before any test times a * render. The first `renderCard` call in a process pays ~500 ms for those * reads; subsequent calls are ~40 ms. Without this beforeAll, the first test * eats the warmup cost, and vitest's default 5 s timeout can start looking * dicey if a render is scheduled behind another slow suite. */beforeAll(async () => { await renderCard(renderDefaultCardSpec(SITE_NAME));});
describe("renderCard is deterministic", () => { /** * Rule 6: a determinism claim needs its own assertion. The deploy * fingerprint (infra/scripts/site/fingerprint.ts) hashes the artifact, so * a card whose bytes drift between runs of the same input makes every * deploy look changed. satori outlines every glyph to `<path>` and resvg * writes the raster deterministically from that SVG; the assertion is * what makes that reliance visible. * * Mutation: replace `spec.brand` with `spec.brand + String(Date.now())` in * `cardTree`. The byte comparison fails on both cases below. */ it("emits byte-identical output for identical input", async () => { const a = await renderCard(renderDefaultCardSpec(SITE_NAME)); const b = await renderCard(renderDefaultCardSpec(SITE_NAME)); expect(a.length).toBe(b.length); expect(Buffer.from(a).equals(Buffer.from(b))).toBe(true); });
it("emits byte-identical output across every fixture repo", async () => { for (const entry of SITE.repos) { const a = await renderCard(renderRepoCardSpec(SITE_NAME, entry)); const b = await renderCard(renderRepoCardSpec(SITE_NAME, entry)); expect(a.length, entry.repo.path).toBe(b.length); expect(Buffer.from(a).equals(Buffer.from(b)), entry.repo.path).toBe(true); } });});
/** * Parse the IHDR chunk — the eight-byte signature followed by * `length(4)+type(4)+width(4)+height(4)+...`. Every claim about dimensions * has to come from parsing what was written, not from the constants that * wrote it — otherwise a test that says "the card is 1200x630" is a test * that says "the constant is 1200x630", and either the drawer or the * encoder disagreeing goes unmentioned. */function parseIhdr(bytes: Uint8Array): { width: number; height: number } { const dv = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength); return { width: dv.getUint32(16, false), height: dv.getUint32(20, false) };}
describe("renderCard produces the dimensions the readers of these tags agree on", () => { /** * Parsed from the bytes rather than trusted — rule 2 by mutation: replace * `width: OG_WIDTH` in the satori options with `width: OG_WIDTH - 4` and * this assertion fails, not the constant it was derived from. */ it("is 1200x630 for the default card", async () => { const bytes = await renderCard(renderDefaultCardSpec(SITE_NAME)); expect(parseIhdr(bytes)).toEqual({ width: OG_WIDTH, height: OG_HEIGHT }); });
it("is 1200x630 for every fixture repo", async () => { for (const entry of SITE.repos) { const bytes = await renderCard(renderRepoCardSpec(SITE_NAME, entry)); expect(parseIhdr(bytes), entry.repo.path).toEqual({ width: OG_WIDTH, height: OG_HEIGHT, }); } });});
describe("writeCards writes one file per page and reports the count", () => { /** * The count is the whole floor: `site-build.ts` asserts against * `repos.length + 1` before printing PASS, so a card that failed to * generate cannot pass unnoticed. Rule 7 — the gate expresses the floor * as a number, not an intent. */ it("returns count equal to repos.length + 1", async () => { const dist = await scratchDist("count"); const result = await writeCards(dist, SITE_NAME, SITE); expect(result.count).toBe(SITE.repos.length + 1); expect(result.repoCards.length).toBe(SITE.repos.length); });
it("writes the default card at its declared URL path", async () => { const dist = await scratchDist("default-file"); const result = await writeCards(dist, SITE_NAME, SITE); expect(result.defaultCard.urlPath).toBe(OG_DEFAULT_URL_PATH); const bytes = await readFile(join(dist, result.defaultCard.file)); expect(parseIhdr(bytes)).toEqual({ width: OG_WIDTH, height: OG_HEIGHT }); });
it("writes one file per repo, at the repoCardUrlPath", async () => { const dist = await scratchDist("per-repo"); const result = await writeCards(dist, SITE_NAME, SITE); for (const [index, entry] of SITE.repos.entries()) { const written = result.repoCards[index]; if (written === undefined) throw new Error("missing card entry"); expect(written.urlPath, entry.repo.path).toBe( repoCardUrlPath(entry.repo.path), ); const bytes = await readFile(join(dist, written.file)); expect(parseIhdr(bytes), entry.repo.path).toEqual({ width: OG_WIDTH, height: OG_HEIGHT, }); } });});
/** * The mutation record. Rule 2 says the mutation must be recorded where a * later reader can re-run it — the record was checked against the * assertions above and each named test went red under the named change. * * - `renderCard is deterministic` — inject entropy through `spec.brand + * String(Date.now())` in `cardTree`. Both byte-comparison tests fail. * - `renderCard produces the dimensions ...` — set `width: OG_WIDTH - 4` * in the satori options passed from `renderCard`. All four IHDR checks * (two here, two in `writeCards` file checks) fail. * - `writeCards ... returns count equal to repos.length + 1` — replace * the write loop's `for (const entry of site.repos)` with * `for (const entry of site.repos.slice(1))`. The count reads as * `repos.length` and this test fails; the mirror in `site-build.ts` * throws with the same shape at build time. * - `writeCards ... writes the default card at its declared URL path` — * drop the `await writeFile(target, bytes)` call inside `write`. Three * tests fail with ENOENT. */
/** * A regression sentinel: verify that a card the writer *thinks* it produced * really landed as a valid PNG on disk with the right size. This is the * exact seam rule 4 speaks to — the writer returns a `CardWritten` * claiming bytes and a path, and this loop asks the filesystem the same * question. */describe("writeCards' return value agrees with what landed on disk", () => { it("reports byte counts matching the file sizes", async () => { const dist = await scratchDist("agreement"); const result = await writeCards(dist, SITE_NAME, SITE); for (const card of [result.defaultCard, ...result.repoCards]) { const onDisk = await readFile(join(dist, card.file)); expect(onDisk.length, card.file).toBe(card.bytes); } });});
/** * A gate-would-see-it test: if a card is stubbed to zero bytes on disk, * does anything downstream notice? `verifyBuiltSite` is what the build * depends on for that, but the card generator's own contract is that the * returned `bytes` counts what it wrote — so this stubs out the file and * asserts the caller can tell. */describe("a zero-byte card is visible to the disk check", () => { it("shows a mismatch when the file is overwritten", async () => { const dist = await scratchDist("mutation"); const result = await writeCards(dist, SITE_NAME, SITE); // Simulate a card that silently failed to write its bytes. await writeFile(join(dist, result.defaultCard.file), new Uint8Array(0)); const onDisk = await readFile(join(dist, result.defaultCard.file)); // The returned metadata is unchanged; the disk contradicts it. expect(onDisk.length).not.toBe(result.defaultCard.bytes); expect(onDisk.length).toBe(0); });});infra/scripts/site/og-card.ts
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537/** * The one 1200×630 PNG per page that a link to this site unfurls as. * * Runs after the prerender writes `dist/` and before `verify.ts` reads it back * (kanban 0007). The card is generated rather than authored because a merge * anywhere on the server changes what a repo's latest work is and the card is * the one thing about the page that has to say so; the alternative is a fleet * of hand-designed images going stale in silence. * * ## Layout with satori, raster with resvg-wasm * * `satori` turns a JSX-like layout into an SVG that already carries every text * glyph as an outlined path — no text-node in the output, no font resolution * at render time. `@resvg/resvg-wasm` then rasterizes that SVG to a PNG. * Together they give a per-repo card in IBM Plex Sans and Mono (the site's own * type) with zero native binaries and no per-platform install surface: satori * is pure JS, resvg-wasm is one `.wasm` file this module loads off disk with * `node:fs`. The two options the user rejected are worth restating so they are * not re-opened casually: `sharp` was rejected because its SVG text goes * through librsvg's system-font path, which reads different bytes on a * different build machine and defeats `fingerprint.ts`'s hash; an SVG-only * `og:image` was rejected because X, Facebook, and LinkedIn do not render SVG * cards at all, so the unfurl would stay broken while every test passed. * * ## Same origin * * The Worker's response `Content-Security-Policy` sends `img-src 'self'` * (apps/web/worker/index.ts), and an `og:image` on a card of a policy that is * not `'self'` fails to render for every visitor. So the cards are written * into `dist/` and referenced by an absolute URL built against `SITE_ORIGIN`, * the same way `canonicalUrl` builds an `og:url` — `og:image` must be * absolute or the readers of these tags drop it rather than resolving it. * * ## Deterministic * * Same input, byte-identical output — asserted in `og-card.test.ts` and * required because the site's deploy fingerprint (infra/scripts/site/ * fingerprint.ts) hashes the artifact. A card that shipped different bytes on * every run would defeat that check by making every deploy look changed. * satori's output is a function of the JSX and the font bytes; the fonts are * read once from disk and the tree is built inside this file, so nothing here * reads the clock or any environment. * * ## Offline, always * * `initWasm` is fed the `.wasm` bytes read from `node_modules` with * `readFile` rather than the URL variant that would `fetch`. The build * machine has no network in the `post-receive` shape and no route out in the * deploy shape, and a card generator that opened one would fail closed at * both. */import { readFile } from "node:fs/promises";import { fileURLToPath } from "node:url";import { initWasm, Resvg } from "@resvg/resvg-wasm";import type { ServedRepo, ServedSiteData } from "@code/shared/site";import satori from "satori";import { repoRoot } from "../affected";
/** The one canvas size the readers of these tags agree on. */export const OG_WIDTH = 1200;export const OG_HEIGHT = 630;
/** * `dist`-relative URL path each page's `og:image` points at. * * `dist/og/…`, not `dist/data/…` — `verify.ts`'s `strayDataFiles` allow-lists * everything under `data/` and would fail on a PNG there, and the walk in * `repoPagesInDist` reads `dist/latest/` only. `og/` is a new prefix that * nothing else in the build reads, so a PNG landing here is invisible to * every check that already exists — the card generator has to be its own * gate. See `writeCards` for the count assertion that makes it one. */export const OG_DEFAULT_URL_PATH = "/og/default.png";
/** Where a repo card lives under `dist/`. The `.png` is on the last segment. */export function repoCardUrlPath(repoPath: string): string { return `/og/${repoPath}.png`;}
/** * The palette, hand-mirrored from `packages/ui/src/theme.css`. * * The hexes there are output — the tokens are authored in oklch — so this * list carries what the tokens resolve to rather than the tokens themselves. * If a token moves, the hex here has to move with it; see the file header on * theme.css for why the two live together and how to re-derive one from the * other. */const PAPER = "#EFF1F4";const GRAPHITE = "#1A1C20";const SLATE = "#545860";const MERGE = "#523FAF";const RULE = "#D7D9DD";
// -----------------------------------------------------------------------------// Loading the font files and the resvg wasm — once per process, from disk
/** * Path to a font file that `@fontsource` ships under `packages/ui`, resolved * from the repo root. * * The root `package.json` does not depend on `@fontsource` packages, so * `import.meta.resolve` from an infra script cannot find them. `packages/ui` * does depend on them and its `node_modules/@fontsource/<pkg>/files/` — the * word between the slashes is the package name — carries the raw `.woff` * cuts satori accepts. That path is stable across a `bun install`: * fontsource keeps its `files/` layout across point releases, and a version * bump that moved it would surface here as a failed read at build time * rather than as a card of the wrong face. * * `.woff` rather than `.woff2` because satori's font parser is SFNT-based * and does not consume `.woff2` in-process. The `.woff` sibling is ~30% * larger and carries the same glyph coverage; both ship with every * `@fontsource` weight. */function fontsourceFile(family: string, weight: number): string { return `${repoRoot}/packages/ui/node_modules/@fontsource/${family}/files/${family}-latin-${String(weight)}-normal.woff`;}
interface LoadedFont { readonly name: string; readonly data: Uint8Array; readonly weight: 400 | 500 | 600; readonly style: "normal";}
/** * The five weights the card renders in, loaded once and reused. Every call * to satori gets the same list — a different order or a missing entry would * change satori's font-selection state and break determinism. * * The pair is what the site itself uses: Plex Sans for prose, Plex Mono for * the repo path and PR number (see `theme.css:215-216`). Weights match the * three the site imports (`theme.css:76-80`), so a card and a page loaded * side by side read as the same face rather than as two. */let fontsPromise: Promise<readonly LoadedFont[]> | null = null;
function loadFonts(): Promise<readonly LoadedFont[]> { if (fontsPromise !== null) return fontsPromise; fontsPromise = (async () => { const cuts: readonly { family: string; name: string; weight: 400 | 500 | 600; }[] = [ { family: "ibm-plex-sans", name: "IBM Plex Sans", weight: 400 }, { family: "ibm-plex-sans", name: "IBM Plex Sans", weight: 500 }, { family: "ibm-plex-sans", name: "IBM Plex Sans", weight: 600 }, { family: "ibm-plex-mono", name: "IBM Plex Mono", weight: 400 }, { family: "ibm-plex-mono", name: "IBM Plex Mono", weight: 500 }, ]; return Promise.all( cuts.map(async (cut): Promise<LoadedFont> => { const data = new Uint8Array( await readFile(fontsourceFile(cut.family, cut.weight)), ); return { name: cut.name, data, weight: cut.weight, style: "normal", }; }), ); })(); return fontsPromise;}
/** * `@resvg/resvg-wasm` runs against a wasm module the caller has to supply. * * `initWasm(buffer)` is called with the bytes read from `node_modules` and * never with the URL variant — the URL variant is a `fetch` under the hood * and this build has no network. The promise is memoised because `initWasm` * is a one-shot: a second call at any point throws. */let resvgInitialised: Promise<void> | null = null;
function initResvg(): Promise<void> { if (resvgInitialised !== null) return resvgInitialised; resvgInitialised = (async (): Promise<void> => { const url = import.meta.resolve("@resvg/resvg-wasm/index_bg.wasm"); const path = fileURLToPath(url); const bytes = await readFile(path); await initWasm(bytes); })(); return resvgInitialised;}
// -----------------------------------------------------------------------------// The card layout
/** * What the card is about. One shape for both kinds — the site default and a * repo — so the layout is one function and the two callers differ only in * what they compute the strings from. `subject` and `date` are the two lines * a viewer would look for and neither may be blank on a card that shipped. */export interface CardSpec { /** The wordmark line at the top. Always the site name in practice. */ readonly brand: string; /** The line the eye lands on. Repo path, or the site's headline. */ readonly headline: string; /** * The medium (`monospace` for a repo path, `sans` for a headline). Not a * property of the string — a repo path in a sans face and a headline in a * mono one both read wrong — so it belongs on the spec, not on the string. */ readonly headlineFace: "sans" | "mono"; /** One or two lines below it. The PR title, or the site's description. */ readonly subject: string; /** A short caption in slate — the merge date, or the site's tagline. */ readonly date: string; /** * Optional accent line — a PR number in merge purple, only on repo cards. * Placed opposite `date` on the bottom row. */ readonly accent?: string;}
/** * The card, as a satori-consumable element tree. Written as data because a * `.tsx` file inside `infra/scripts/` would need its own tsconfig and a * `React` shim to compile, and satori accepts either shape. * * The layout is bespoke rather than proportional: the card is one size, the * pages that fill it have known shapes, and a flexbox layout at absolute * pixels is what makes the visual weight predictable across every repo. * Widths are set so a long unbreakable string clips with an ellipsis rather * than overflowing — the fixture carries such a title deliberately (see * `apps/web/src/fixtures/site-data.ts`'s AP-Bio entry) so the case is * exercised on every render. */function cardTree(spec: CardSpec): unknown { const headlineFontFamily = spec.headlineFace === "mono" ? "IBM Plex Mono" : "IBM Plex Sans"; const contentWidth = OG_WIDTH - 72 * 2;
return { type: "div", props: { style: { display: "flex", flexDirection: "column", width: OG_WIDTH, height: OG_HEIGHT, backgroundColor: PAPER, color: GRAPHITE, fontFamily: "IBM Plex Sans", padding: 72, }, children: [ // Top: small wordmark in slate. { type: "div", props: { style: { fontSize: 32, fontWeight: 500, color: SLATE, fontFamily: "IBM Plex Mono", }, children: spec.brand, }, }, // Headline: repo path or site headline. `overflow: hidden` + // `text-overflow: ellipsis` + `white-space: nowrap` clips a repo // path that is wider than the card rather than pushing the layout // past its edge. `max-width` is what the ellipsis takes effect at. { type: "div", props: { style: { fontSize: 96, fontWeight: 600, color: GRAPHITE, fontFamily: headlineFontFamily, marginTop: 32, maxWidth: contentWidth, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap", letterSpacing: -2, }, children: spec.headline, }, }, // The merge-violet accent bar. Same design rule as everywhere else // on the site: this hue marks a value that came out of a git // repository. { type: "div", props: { style: { width: 240, height: 8, backgroundColor: MERGE, marginTop: 24, }, }, }, // Subject: PR title or site description. Two lines maximum; satori // supports `-webkit-line-clamp` and clips with an ellipsis when the // text is longer. `word-break: break-word` lets an unbreakable // string (a file path with no spaces) fold before the ellipsis — a // PR title referencing `src/units/cell-cycle/rubric.json` needs // that or the whole line elides. { type: "div", props: { style: { fontSize: 44, fontWeight: 400, lineHeight: 1.25, color: GRAPHITE, marginTop: 40, maxWidth: contentWidth, display: "-webkit-box", WebkitBoxOrient: "vertical", WebkitLineClamp: 2, overflow: "hidden", wordBreak: "break-word", }, children: spec.subject, }, }, // Bottom row, pushed down by `marginTop: auto`. A hairline of // `rule` grey anchors it even when the subject was short. `flex: // space-between` puts the date on the left and the PR number on // the right; an omitted `accent` leaves the right cell as an empty // flex child. { type: "div", props: { style: { marginTop: "auto", paddingTop: 24, borderTop: `2px solid ${RULE}`, display: "flex", justifyContent: "space-between", alignItems: "baseline", }, children: [ { type: "div", props: { style: { fontSize: 28, fontWeight: 400, color: SLATE, fontFamily: "IBM Plex Mono", }, children: spec.date, }, }, { type: "div", props: { style: { fontSize: 40, fontWeight: 500, color: MERGE, fontFamily: "IBM Plex Mono", }, children: spec.accent ?? "", }, }, ], }, }, ], }, };}
// -----------------------------------------------------------------------------// The public entry points
/** * The PNG bytes for one card. Deterministic — pinned in `og-card.test.ts`. * * Loads fonts and resvg lazily; the first call in a process is slow (the * `.wasm` and 5 woff files come off disk once) and every later call reuses * the loaded state. Callers do not need to init anything. */export async function renderCard(spec: CardSpec): Promise<Uint8Array> { const [fonts] = await Promise.all([loadFonts(), initResvg()]); const svg = await satori(cardTree(spec) as never, { width: OG_WIDTH, height: OG_HEIGHT, fonts: fonts.map((font) => ({ name: font.name, // `Buffer.from` on a `Uint8Array` shares the backing storage rather // than copying it, and satori's `FontOptions.data` type asks for a // `Buffer` or an `ArrayBuffer` even though the runtime accepts either. data: Buffer.from(font.data), weight: font.weight, style: font.style, })), }); const resvg = new Resvg(svg, { // No font block: satori has outlined every glyph, so resvg never asks // about a font. `system` fallbacks are irrelevant for the same reason — // a card is entirely `<path>` by the time it gets here. fitTo: { mode: "width", value: OG_WIDTH }, background: PAPER, }); return new Uint8Array(resvg.render().asPng());}
/** * The site default card — the one every non-repo page unfurls as. * * Kept as a pure function of the site name so a test can render it without * touching disk. `siteName` is hand-mirrored from `apps/web/src/site.ts`, * which is imported through the app rather than through anything infra can * reach — see the constant with that name in `site-build.ts`. */export function renderDefaultCardSpec(siteName: string): CardSpec { return { brand: siteName, headline: "recent merges", headlineFace: "sans", subject: "Public repositories on a self-hosted git server, and the most recent pull request merged into each.", date: "building in the open", };}
/** * A repo card, from the same {@link ServedRepo} the page is built from. * * Empty-merge case is real (`russ/ts-template` in the fixture) and rendered * with an honest caption — "no merges yet" is what the page says too, and * the card that unfurls it should agree with the page it links to. */export function renderRepoCardSpec( siteName: string, entry: ServedRepo,): CardSpec { const merge = entry.latestMerge; if (merge === null) { return { brand: siteName, headline: entry.repo.path, headlineFace: "mono", subject: "No merged pull requests yet.", date: entry.lastActivity.slice(0, 10), }; } return { brand: siteName, headline: entry.repo.path, headlineFace: "mono", subject: merge.title, date: `merged ${merge.mergedAt.slice(0, 10)}`, accent: `PR #${String(merge.number)}`, };}
// -----------------------------------------------------------------------------// Writing to disk, with a floor
/** * Everything a repo card needs, without importing from `apps/web/src`. * Callers in the build script hand a {@link ServedSiteData} in and this * file computes the card set from it. */export interface CardWritten { /** URL path, absolute — what an `og:image` tag would point at. */ readonly urlPath: string; /** `dist`-relative file path. */ readonly file: string; /** Byte length of the written PNG. */ readonly bytes: number;}
export interface CardsResult { /** How many cards were written — always `repos.length + 1`. */ readonly count: number; readonly defaultCard: CardWritten; readonly repoCards: readonly CardWritten[];}
/** * Write every card into `dist/` and return what was written. * * The count is what `site-build.ts` asserts against `repos.length + 1` * before reporting success, so a card silently failing to generate cannot * pass as done. That is the ADDENDUM rule 7 floor: the caller sees a * number, not an intent, and it is a number this function has produced by * counting the files it wrote rather than the files it was asked to write. * * Renders are sequential rather than parallel — satori and resvg both hold * process-wide state (font tables, wasm arena), and driving them * concurrently from a handful of writers moved the wallclock by nothing * measurable while making the memory profile harder to think about. Four * cards in about a second on this machine; parallelising would help * nobody. */export async function writeCards( dist: string, siteName: string, site: ServedSiteData,): Promise<CardsResult> { const { mkdir, writeFile } = await import("node:fs/promises"); const { dirname, join } = await import("node:path");
const write = async ( urlPath: string, bytes: Uint8Array, ): Promise<CardWritten> => { // urlPath begins with `/`; strip it before joining with `dist`. const file = urlPath.replace(/^\//, ""); const target = join(dist, file); await mkdir(dirname(target), { recursive: true }); await writeFile(target, bytes); return { urlPath, file, bytes: bytes.length }; };
const defaultBytes = await renderCard(renderDefaultCardSpec(siteName)); const defaultCard = await write(OG_DEFAULT_URL_PATH, defaultBytes);
const repoCards: CardWritten[] = []; for (const entry of site.repos) { const bytes = await renderCard(renderRepoCardSpec(siteName, entry)); const written = await write(repoCardUrlPath(entry.repo.path), bytes); repoCards.push(written); }
return { count: 1 + repoCards.length, defaultCard, repoCards, };}infra/scripts/site/publications.test.ts
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523/** * The seam, end to end, against a real repository and real keys. * * The claim under test is the one the whole card is judged against: a slug * reaches `expected` only for a bundle this build cryptographically verified. * Proving that with a stubbed source would prove only that the plumbing passes * a boolean along, so the repository here is real, the ed25519 keys are real, * and the verification is `git verify-commit` doing its actual job. */import { execFile } from "node:child_process";import { mkdtemp, readFile, rm } from "node:fs/promises";import { tmpdir } from "node:os";import { join } from "node:path";import { promisify } from "node:util";import { type AdmittedPublication, collectPublications, gitPublicationSource,} from "@code/forge";import type { RepoEntry } from "@code/shared/forge";import { ALLOWED_SIGNERS_PATH, PUBLICATION_BLOB, PUBLICATION_REF_PREFIX, PUBLICATION_SCHEMA, publicationRef, serializeBundle, type PublicationRecord,} from "@code/shared/publication";import { afterEach, beforeEach, describe, expect, it } from "vitest";import { publicationEntry, publicationProblems, publicationSlugs, publicationVerdict, type RepoScan, withPublications,} from "./publications";
const execFileAsync = promisify(execFile);
const COMMIT_ENV = { GIT_AUTHOR_NAME: "Publisher", GIT_AUTHOR_EMAIL: "publisher@example.invalid", GIT_COMMITTER_NAME: "Publisher", GIT_COMMITTER_EMAIL: "publisher@example.invalid", GIT_AUTHOR_DATE: "2026-07-14T09:12:03+00:00", GIT_COMMITTER_DATE: "2026-07-14T09:12:03+00:00",};
/** * The sanitisation boundary. * * A signature attests that the publisher meant those bytes. It does not attest * that they are safe as markup, and the leak scan asks a third question again * — "does this look like a secret" — which a control character does not. So a * bundle can be signed by a listed key, scan clean, and still carry text that * no other author-controlled string in this codebase reaches a page without * passing through `plainText` first. * * These are the fields a publisher types, driven with the things `plainText` * exists to remove. */describe("publicationEntry sanitises every string it carries onto a page", () => { // Built with fromCharCode rather than written as escapes, and the reason is // this card's own history: a `\0` typed into a source file in this repo has // twice arrived as a raw NUL byte, which makes the file binary, makes `grep` // silently refuse to search it, and makes an empty grep result look like // absence. Pure-ASCII source cannot have that happen to it. const NUL = String.fromCharCode(0); const ESC = String.fromCharCode(27); const BEL = String.fromCharCode(7);
const hostile: PublicationRecord = { schema: PUBLICATION_SCHEMA, slug: "_widget", displayName: `Wid${NUL}get`, productUrl: null, number: 12, title: `ti${ESC}tle`, body: `bo${ESC}[2Jdy`, branch: `bran${NUL}ch`, openedBy: `open${BEL}er`, mergeSha: "3f7a1c9d2b8e4f60a1c3d5e7f9b2a4c6d8e0f2a4", mergedAt: "2026-07-14T09:12:03+00:00", mergedBy: `mer${BEL}ger`, comments: [ { at: "2026-07-14T08:00:00Z", actor: `act${NUL}or`, body: `bo${ESC}dy`, }, ], files: [{ path: `sr${NUL}c/a.ts`, status: "M", added: 1, removed: 0 }], patch: "diff --git a/a.ts b/a.ts\n", acknowledged: [], };
const entry = publicationEntry(hostile, { repo: "org/showcase", host: "git.example.invalid", });
/** Every string the entry puts on a page, flattened. */ const rendered = [ entry.repo.displayName, entry.repo.name, entry.latestMerge?.title ?? "", entry.latestMerge?.body ?? "", entry.latestMerge?.branch ?? "", entry.latestMerge?.mergedBy ?? "", entry.latestMerge?.openedBy ?? "", ...(entry.latestMerge?.comments ?? []).flatMap((c) => [c.actor, c.body]), ...(entry.latestMerge?.files ?? []).map((f) => f.path), ];
it("strips control characters from every rendered field", () => { for (const text of rendered) { const control = [...text].filter((character) => { const code = character.charCodeAt(0); // Newline and tab are legitimate content; nothing else below 0x20 is. return code < 0x20 && code !== 0x0a && code !== 0x09; }); expect(control).toEqual([]); } });
it("keeps the readable text rather than dropping the field", () => { // Inertness, not erasure. A title mangled into "" would satisfy the // assertion above while losing the publication's name, so the exact // surviving text is pinned too. expect(entry.repo.displayName).toBe("Widget"); expect(entry.latestMerge?.title).toBe("title"); expect(entry.latestMerge?.mergedBy).toBe("merger"); expect(entry.latestMerge?.comments[0]?.actor).toBe("actor"); expect(entry.latestMerge?.files[0]?.path).toBe("src/a.ts"); });
it("carries the slug as the path, never the source repo", () => { expect(entry.repo.path).toBe("_widget"); expect(JSON.stringify(entry)).not.toContain("private"); });
it("points its clone URLs at the publication repo, which really is clonable", () => { expect(entry.repo.cloneHttps).toBe( "https://git.example.invalid/org/showcase", ); expect(entry.repo.cloneSsh).toBe( "git@git.example.invalid:org/showcase.git", ); });
it("carries no contribution grid rather than an invented one", () => { // A bundle knows one date. Drawing a grid from it would imply the source // repo's activity shape, which is an open question on this card and not // one an adapter answers by default. expect(entry.commitDays).toEqual([]); });});
/** * The publication repo is infrastructure, and the site does not list it. * * Its default branch carries `allowed_signers` and no `Merge PR #` commits, so * left alone it draws a card with zero merges named for the mechanism rather * than for any work — the "correct-looking page about a repository" shape * `verify.ts` exists to be suspicious of. Suppressing it here is also what * keeps the sets agreeing: `site-build.ts` intersects `publicTips` with the * paths that survived into `published.site.repos`, so a repo dropped here * leaves `expected` on its own with no second rule. * * Pinned because a mutation removing the suppression left every other test in * this file green. */describe("withPublications", () => { function repoEntry(path: string): RepoEntry { return { repo: { path, org: path.includes("/") ? (path.split("/")[0] ?? null) : null, name: path.split("/").at(-1) ?? path, displayName: path, cloneHttps: `https://git.example.invalid/${path}`, cloneSsh: `git@git.example.invalid:${path}.git`, }, mirrors: { codeberg: null, tangled: null }, latestMerge: null, mergedPrCount: 0, lastActivity: "2026-07-01T00:00:00Z", commitDays: [], }; }
const bundle: AdmittedPublication = { slug: "_alpha", record: { schema: PUBLICATION_SCHEMA, slug: "_alpha", displayName: "Alpha", productUrl: null, number: 1, title: "a title", body: "a body", branch: "br", openedBy: "o", mergeSha: "3f7a1c9d2b8e4f60a1c3d5e7f9b2a4c6d8e0f2a4", mergedAt: "2026-07-20T00:00:00Z", mergedBy: "m", comments: [], files: [], patch: "p", acknowledged: [], }, };
const scans: RepoScan[] = [ { repo: "org/showcase", scan: { admitted: [bundle], refsDiscovered: 1, verified: 1, dropped: 0, withdrawn: 0, }, }, ];
const data = { generatedAt: "2026-07-21T00:00:00Z", repos: [repoEntry("org/showcase"), repoEntry("org/ordinary")], };
it("drops the publication repo's own card and adds the bundle's", () => { const paths = withPublications( data, scans, "git.example.invalid", ).repos.map((entry) => entry.repo.path); // The set, asserted. `org/showcase` is the mechanism; `_alpha` is the work. expect(paths).toEqual(["_alpha", "org/ordinary"]); });
it("leaves data untouched when there are no publication repos", () => { expect(withPublications(data, [], "git.example.invalid")).toBe(data); });});
describe("only a verified bundle's slug reaches expected", () => { let root: string; let gitDir: string; let signerKey: string; let rogueKey: string;
async function git(args: readonly string[], input?: string): Promise<string> { const child = execFileAsync("git", ["--git-dir", gitDir, ...args], { encoding: "utf8", env: { ...process.env, ...COMMIT_ENV }, maxBuffer: 16 * 1024 * 1024, }); if (input !== undefined) child.child.stdin?.end(input); return (await child).stdout.trim(); }
async function publish(slug: string, key: string | null): Promise<void> { const text = serializeBundle({ kind: "publication", record: { schema: PUBLICATION_SCHEMA, slug, displayName: "Widget", productUrl: null, number: 12, title: "a published title", body: "a body", branch: "a-branch", openedBy: "Publisher", mergeSha: "3f7a1c9d2b8e4f60a1c3d5e7f9b2a4c6d8e0f2a4", mergedAt: "2026-07-14T09:12:03+00:00", mergedBy: "Publisher", comments: [], files: [{ path: "a.ts", status: "M", added: 1, removed: 0 }], patch: "diff --git a/a.ts b/a.ts\n@@ -1 +1 @@\n-a\n+b\n", acknowledged: [], }, }); const blob = await git(["hash-object", "-w", "--stdin"], text); const tree = await git( ["mktree"], `100644 blob ${blob}\t${PUBLICATION_BLOB}\n`, ); const signing = key === null ? [] : ["-c", "gpg.format=ssh", "-c", `user.signingkey=${key}`]; const commit = await git([ ...signing, "commit-tree", tree, "-m", `publish ${slug}`, ...(key === null ? [] : ["-S"]), ]); await git(["update-ref", publicationRef(slug), commit]); }
async function scanRepo(): Promise<RepoScan> { const scan = await collectPublications({ source: gitPublicationSource({ gitDir, allowedSignersFile: join(root, "allowed.materialised"), defaultRef: "refs/heads/main", bundleRefPrefix: PUBLICATION_REF_PREFIX, }), onDrop: () => undefined, }); return { repo: "org/showcase", scan }; }
beforeEach(async () => { root = await mkdtemp(join(tmpdir(), "pubseam-")); gitDir = join(root, "repo.git"); await execFileAsync("git", ["init", "--quiet", "--bare", gitDir]); signerKey = join(root, "signer"); rogueKey = join(root, "rogue"); for (const key of [signerKey, rogueKey]) { await execFileAsync("ssh-keygen", [ "-q", "-t", "ed25519", "-N", "", "-C", "t", "-f", key, ]); } const pub = (await readFile(`${signerKey}.pub`, "utf8")).trim(); const blob = await git( ["hash-object", "-w", "--stdin"], `publisher@example.invalid ${pub}\n`, ); const tree = await git( ["mktree"], `100644 blob ${blob}\t${ALLOWED_SIGNERS_PATH}\n`, ); const head = await git(["commit-tree", tree, "-m", "signers"]); await git(["update-ref", "refs/heads/main", head]); });
afterEach(async () => { await rm(root, { recursive: true, force: true }); });
it("admits the signed slug and excludes the unsigned and rogue-signed ones", async () => { await publish("_alpha", signerKey); await publish("_beta", rogueKey); await publish("_gamma", null);
const slugs = publicationSlugs([await scanRepo()]);
// Rule 7: the set, asserted — not a count, and not a scan of its contents. expect(slugs).toEqual(["_alpha"]); expect(slugs).not.toContain("_beta"); expect(slugs).not.toContain("_gamma"); });
it("keeps a withdrawn slug out of expected", async () => { await publish("_alpha", signerKey); const withdrawn = serializeBundle({ kind: "tombstone", tombstone: { schema: PUBLICATION_SCHEMA, slug: "_alpha", at: "2026-08-01T00:00:00Z", reason: "retired", }, }); const blob = await git(["hash-object", "-w", "--stdin"], withdrawn); const tree = await git( ["mktree"], `100644 blob ${blob}\t${PUBLICATION_BLOB}\n`, ); const commit = await git([ "-c", "gpg.format=ssh", "-c", `user.signingkey=${signerKey}`, "commit-tree", tree, "-m", "withdraw", "-S", ]); await git(["update-ref", publicationRef("_alpha"), commit]);
expect(publicationSlugs([await scanRepo()])).toEqual([]); });
it("sees and refuses a nested out-of-namespace ref by name", async () => { // The case M10 exposed. `for-each-ref refs/meta/publications/*` matches // with wildmatch in pathname mode, where `*` does not cross `/`, so this // ref was invisible to the build entirely — and anyone with push access to // the publication repo can create it. Refusing it by name is strictly // better than not seeing it: the published set is the same either way, but // only one of the two says so. await publish("_alpha", signerKey); const blob = await git( ["hash-object", "-w", "--stdin"], serializeBundle({ kind: "publication", record: { schema: PUBLICATION_SCHEMA, slug: "_alpha", displayName: "W", productUrl: null, number: 1, title: "t", body: "b", branch: "br", openedBy: "o", mergeSha: "abc", mergedAt: "2026-01-01T00:00:00Z", mergedBy: "m", comments: [], files: [], patch: "p", acknowledged: [], }, }), ); const tree = await git( ["mktree"], `100644 blob ${blob}\t${PUBLICATION_BLOB}\n`, ); const commit = await git([ "-c", "gpg.format=ssh", "-c", `user.signingkey=${signerKey}`, "commit-tree", tree, "-m", "nested", "-S", ]); await git(["update-ref", `${PUBLICATION_REF_PREFIX}org/widget`, commit]);
const drops: string[] = []; const scan = await collectPublications({ source: gitPublicationSource({ gitDir, allowedSignersFile: join(root, "allowed.materialised"), defaultRef: "refs/heads/main", bundleRefPrefix: PUBLICATION_REF_PREFIX, }), onDrop: (reason) => drops.push(reason), });
expect(publicationSlugs([{ repo: "org/showcase", scan }])).toEqual([ "_alpha", ]); // Seen, counted, and refused for being outside the namespace — not skipped. expect(scan.refsDiscovered).toBe(2); expect(drops.some((d) => d.includes("not a publication slug"))).toBe(true); });
/** * A validly signed bundle, moved to a ref that names a different slug. The * signature is over `_alpha`'s bytes and the ref says `_other`, so one of the * two is lying and neither is safe to publish under. Pinned here rather than * only in `packages/forge` because this is where a slug becomes a path in * `expected`, and a mutation that removed the check left this file green. */ it("drops a signed bundle whose slug disagrees with its ref", async () => { await publish("_alpha", signerKey); const commit = await git(["rev-parse", publicationRef("_alpha")]); await git(["update-ref", publicationRef("_other"), commit]); await git(["update-ref", "-d", publicationRef("_alpha")]);
const drops: string[] = []; const scan = await collectPublications({ source: gitPublicationSource({ gitDir, allowedSignersFile: join(root, "allowed.materialised"), defaultRef: "refs/heads/main", bundleRefPrefix: PUBLICATION_REF_PREFIX, }), onDrop: (reason) => drops.push(reason), });
expect(publicationSlugs([{ repo: "org/showcase", scan }])).toEqual([]); expect(drops.some((d) => d.includes('names slug "_alpha"'))).toBe(true); });
it("never emits a path that could collide with a repository", async () => { await publish("_alpha", signerKey); for (const slug of publicationSlugs([await scanRepo()])) { expect(slug.startsWith("_")).toBe(true); } });
it("fails the build when a publication repo yielded nothing", async () => { // No bundles pushed at all: the floor refuses, because a repo declared as // a publication repo and carrying nothing is a read that went wrong. const problems = publicationProblems([await scanRepo()]); expect(problems[0]).toMatch(/org\/showcase: publication scan discovered 0/); });
it("reports numbers beside the verdict, and says so distinctly when there are no repos", async () => { await publish("_alpha", signerKey); await publish("_gamma", null); const verdict = publicationVerdict([await scanRepo()]); expect(verdict).toContain("2 bundle ref(s) discovered"); expect(verdict).toContain("1 signature(s) verified"); expect(verdict).toContain("1 published"); expect(verdict).toContain("1 dropped"); // The empty case must not read like the verified case. expect(publicationVerdict([])).toMatch(/no publication repository/); expect(publicationVerdict([])).not.toContain("PASS"); });});infra/scripts/site/publications.ts
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292/** * The seam between verified publication bundles and the build's `expected` set. * * UNCOMMITTED AND UNWIRED, deliberately. `infra/scripts/site-build.ts` and * `infra/scripts/site/verify.ts` belong to another card that has work in flight * in both; this module is the half that can be written without touching either, * so that when the sequencing allows it, the change to those two files is a * handful of lines rather than a design. * * ## The one property this file exists to hold * * `verify.ts`'s strong check is an exact three-way set comparison, and its * power comes entirely from `expected` and the artifact having arrived by * different routes. `expected` is `publicTips` — what a stranger could see on * the server — compared against what the artifact actually carries. Derive * `expected` from the same intent that produced the content and the assertion * becomes a restatement of its own input; that is kanban #48's Design B, and it * is rejected for exactly this reason. * * So {@link publicationSlugs} reads only `scan.admitted`, and `admitted` is * populated in `packages/forge/src/publication.ts` solely by bundles whose * commit `git verify-commit` accepted against the `allowed_signers` file the * publication repo itself carries. There is no committed allowlist anywhere in * this design, nothing here consults one, and no path exists by which an * unverified bundle's slug becomes an expectation. A slug in `expected` is a * statement that *this build checked a signature*, not that somebody wrote the * slug down. * * It follows that this function must never be given anything but a scan. Handed * a list of slugs it would be Design B with extra steps. * * ## `verify.ts` needed no change, and that is the evidence * * Worth stating outright, because it is the strongest available argument that * this is Design C and not Design B wearing its clothes, and it is the kind of * fact that reads as an implementation detail until someone explains it. * * `repoSetProblems` compares three sets of strings. It does not know what a * repository is, does not know what a publication is, and did not have to * learn: a slug is a string that either is or is not in the artifact, exactly * as a repo path is. So the gate absorbed an entirely new *kind* of published * thing without being widened, weakened, or given a special case — zero hunks * in that file. * * Contrast the design that was rejected. Design B extends `expected` from a * committed allowlist, which means the gate has to be taught which entries came * from the enumeration and which from the list, and to hold them to different * standards. The moment a gate needs to know where its expectations came from * in order to judge them, it has stopped being an independent check on the * build and become a restatement of the build's intent. * * So "the gate did not need to change" is not a convenience. It is the * observable consequence of the provenance staying independent, and if a future * change to this seam starts requiring hunks in `verify.ts`, that is the signal * to stop and ask which direction the provenance just flowed. */import type { AdmittedPublication, PublicationScan } from "@code/forge";import { plainText, publicationFloorProblem, siteData } from "@code/forge";import { compareCodeUnits } from "@code/shared/order";import type { PublicationRecord } from "@code/shared/publication";import type { RepoEntry, SiteData } from "@code/shared/forge";
/** One publication repo's read, named so a failure can be reported against it. */export interface RepoScan { /** The publication repo's own server path — public, and safe to name. */ readonly repo: string; readonly scan: PublicationScan;}
/** * The slugs that may enter `expected`, sorted. * * Sorted so the value does not depend on the order repos were read in, which * is `Promise.all` over an enumeration and therefore not a promise about * anything. */export function publicationSlugs( scans: readonly RepoScan[],): readonly string[] { return scans .flatMap((entry) => entry.scan.admitted.map((bundle) => bundle.slug)) .sort((left, right) => compareCodeUnits(left, right));}
/** Every admitted bundle across every publication repo, for the render step. */export function publicationEntries( scans: readonly RepoScan[],): readonly AdmittedPublication[] { return scans.flatMap((entry) => entry.scan.admitted);}
/** * Why this build must not publish, or an empty list. * * A floor failure is fatal to the build rather than to one repo. The failure it * describes is "this read examined nothing", and a build that treats that as * "there was nothing to publish" quietly drops every publication the moment a * fetch goes wrong — which is the failure mode that looks exactly like success. */export function publicationProblems( scans: readonly RepoScan[],): readonly string[] { return scans.flatMap((entry) => { const problem = publicationFloorProblem(entry.scan); return problem === null ? [] : [`${entry.repo}: ${problem}`]; });}
/** * The line the build prints, with the numbers beside the word. * * Three distinct sentences rather than one with zeroes in it. "no publication * repository" and "verified 0 bundles" are different facts, and a build that * says the same thing for both is the thing rule 7 is about. */export function publicationVerdict(scans: readonly RepoScan[]): string { if (scans.length === 0) { return "publications: no publication repository was enumerated — none expected"; } const discovered = total(scans, (scan) => scan.refsDiscovered); const verified = total(scans, (scan) => scan.verified); const dropped = total(scans, (scan) => scan.dropped); const withdrawn = total(scans, (scan) => scan.withdrawn); const published = publicationSlugs(scans).length; return ( `publications PASS — ${scans.length} repo(s), ${discovered} bundle ref(s) discovered, ` + `${verified} signature(s) verified, ${published} published, ` + `${withdrawn} withdrawn, ${dropped} dropped` );}
function total( scans: readonly RepoScan[], pick: (scan: PublicationScan) => number,): number { return scans.reduce((sum, entry) => sum + pick(entry.scan), 0);}
/** * A bundle's raw git status letter as the union `DiffFile` speaks. * * Anything unrecognised becomes `modified` rather than throwing. A status * letter this does not know — a `T` for a type change, a `C` for a copy — is * a file that was genuinely touched, and refusing the whole publication over * the label on one row would be a worse answer than a slightly imprecise * label on that row. */function diffStatus( letter: string,): "added" | "modified" | "deleted" | "renamed" { const first = letter.charAt(0).toUpperCase(); if (first === "A") return "added"; if (first === "D") return "deleted"; if (first === "R") return "renamed"; return "modified";}
/** * A verified bundle, as an ordinary {@link RepoEntry}. * * There is no new page component and no new shape. `RepoEntry` and `MergedPr` * already describe what a bundle carries — a title, a body, comments, a patch, * a file list, a merge sha and a date — so a publication renders through the * same components as everything else. That is a deliberate choice rather than * a shortcut, and it has a cost worth naming: nothing on the rendered page says * this came from a repository the site cannot read. Giving it a `productUrl` * and a provenance marker needs `RepoEntry` extended in `packages/shared` and * the page components changed, which is its own card. * * ## Every string goes through `plainText`, and that is the point * * A signature attests that the publisher meant those bytes. It does not attest * that they are safe as markup. The bundle was leak-scanned at publication * time, which asks a different question again — "does this look like a secret" * — and a control character is not a secret. * * Every other author-controlled string that reaches a page in this codebase is * already sanitised at the boundary where it stops being data and starts being * content: `parseProjectConfig` does it to `displayName`, `mergedPr` does it to * `title`, `branch` and `mergedBy`. Bundle fields have never passed through it, * because until now nothing rendered them. This function is that boundary, and * if it ever stops calling `plainText` then presentation becomes the first * place unsanitised author text lands on a public page. */export function publicationEntry( record: PublicationRecord, source: { readonly repo: string; readonly host: string },): RepoEntry { return { repo: { // The slug, never the source repo's path. `_`-prefixed and single // segment, so it cannot collide with a server path — see // `@code/shared/publication`. path: record.slug, org: null, name: record.slug, displayName: plainText(record.displayName), // The PUBLICATION repo's clone URLs, not the source's — the source has // none that a stranger could use, and inventing one would name a repo // this design exists to keep unnamed. These are honest: the bundle // really is anonymously clonable there, which is what the reader would // be checking. cloneHttps: `https://${source.host}/${source.repo}`, cloneSsh: `git@${source.host}:${source.repo}.git`, }, mirrors: { codeberg: null, tangled: null }, latestMerge: { number: record.number, title: plainText(record.title), branch: plainText(record.branch), mergeSha: record.mergeSha, mergedAt: record.mergedAt, mergedBy: plainText(record.mergedBy), openedBy: plainText(record.openedBy), body: plainText(record.body), // Every published comment renders as a comment, and a review verdict // does not survive. That is a fidelity loss, not a rendering choice: // `buildRecord` in `infra/prs/src/publish.ts` flattens both `comment` // and `review` events into `{at, actor, body}`, so the bundle does not // carry `kind` or `verdict` at all and there is nothing here to read. // Fixing it means widening `PublicationComment`, which is a change to // the signed schema and belongs with the `productUrl` card rather than // riding along with a sanitisation fix. Reporting an approve as a plain // comment understates it; inventing `kind: "review"` would overstate it. comments: record.comments.map((comment) => ({ at: comment.at, actor: plainText(comment.actor), kind: "comment" as const, verdict: null, body: plainText(comment.body), })), // The bundle carried the prose, so this is the enriched case by // construction — there was no merge commit to fall back to. enriched: true, patch: record.patch, files: record.files.map((file) => ({ path: plainText(file.path), status: diffStatus(file.status), additions: file.added, deletions: file.removed, })), }, mergedPrCount: 1, lastActivity: record.mergedAt, // Empty, and deliberately so. A contribution grid drawn from a bundle // would either be invented — one cell on the merge date, implying the // source repo did exactly that much that day — or would require the source // repo's real activity shape, which discloses working volume and cadence // for a repository nobody agreed to publish. That is an open question on // this card and not one this adapter gets to answer by default. An empty // grid says "not known", which is true. commitDays: [], };}
/** * The site data the artifact is built from, with publications folded in. * * Two edits, and the second is the one worth explaining. * * Publications are added as ordinary entries. And the publication repos * *themselves* are removed: each is a real public repo whose default branch * carries an `allowed_signers` file and no `Merge PR #` commits, so it would * otherwise draw an empty card advertising the mechanism rather than any work * — which is precisely the "correct-looking page about a repository" shape * `verify.ts` exists to be suspicious of. * * Removing it here is also what keeps the sets agreeing, without a second * rule. `site-build.ts` derives `expected` by intersecting `publicTips` with * the paths that survived into `published.site.repos`, so a repo dropped here * leaves that intersection on its own. Suppressing it anywhere later would * have meant teaching `expected` about publication repos as a special case. */export function withPublications( data: SiteData, scans: readonly RepoScan[], host: string,): SiteData { if (scans.length === 0) return data; const infrastructure = new Set(scans.map((entry) => entry.repo)); const kept = data.repos.filter( (entry) => !infrastructure.has(entry.repo.path), ); const published = scans.flatMap((entry) => entry.scan.admitted.map((bundle) => publicationEntry(bundle.record, { repo: entry.repo, host }), ), ); // Re-sorted by forge's own comparator so a publication takes its place in // the recency order rather than being appended after it. return siteData([...kept, ...published], data.generatedAt);}infra/scripts/site/publish.test.ts
100 unmodified lines101102103104105106107108109110111235 unmodified lines347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386100 unmodified lines latestMerge, mergedPrCount: 1, lastActivity: "2026-07-01T10:00:00.000Z", commitDays: [ { date: "2026-06-29", count: 2 }, { date: "2026-06-30", count: 0 }, { date: "2026-07-01", count: 1 }, ], };}
235 unmodified lines }, );
// Since kanban 0046, body/comments render independently of the diff — a // `RenderedMerge` with `""` tree/html still ships to carry the description // and discussion. `renderedCount` must NOT inflate on those, or a build // where pierre broke for every repo would report success on prose alone. // The doc on `PublishResult.renderedCount` states it, and this pins it: // a merge whose patch renders to nothing (empty patch string) produces a // `RenderedMerge` with populated `bodyHtml` — and `renderedCount` still // reads zero. it( "counts diffs, not any-render, so a body-only render does not inflate the counter", { timeout: 120_000 }, async () => { const bodied = merge({ patch: "", files: [], body: "a body someone wrote", comments: [], }); const result = await publishSite(data(entry("org/bodied", bodied)), { root, source: "forge", }); expect(result.repoCount).toBe(1); // Diff count: zero. A pierre-empty render is not a rendered diff. expect(result.renderedCount).toBe(0); // But the prerender input still carries a `RenderedMerge` for the repo // — that is what keeps the description panel visible on the page. const input = await readJson<PrerenderInput>(result.prerenderInputFile); expect(input.renders).toHaveLength(1); expect(input.renders[0]?.html).toBe(""); expect(input.renders[0]?.bodyHtml).not.toBe(""); }, );
// Two packages name this location independently — `@code/shared/site` for // the reader and `@code/forge` for its own CLI's default output. If they // ever disagree, `site:build` writes one file and `site:data` writesinfra/scripts/site/publish.ts
51 unmodified lines5253545555565758596061626337 unmodified lines1011021039910410510610710810911011111211311411511611730 unmodified lines14814915015115215315415515615715815916032 unmodified lines19319419519619719819920020120220320467 unmodified lines2722732742752762772782792802812822832842852862872882482492892902912922932942952961 unmodified line2982993002572583013023032602613043053063073087 unmodified lines31631731831932032132232332432532632732832933033133233333433533614 unmodified lines35135235329535435535635735835936036151 unmodified lines toServedSiteData,} from "@code/shared/site";import type { SharedBlocks } from "@code/viewer";import { renderFileTree, renderPatch, warmHighlighter } from "@code/viewer";import { renderFileTree, renderMarkdown, renderPatch, warmHighlighter,} from "@code/viewer";
/** * Where per-repo render documents were written before prerendering landed.37 unmodified lines readonly site: ServedSiteData; /** Repos in the served data, which may be fewer than were enumerated. */ readonly repoCount: number; /** Repos that got a rendered diff. */ /** * Repos that got a rendered diff — pierre drew the patch. This is a strict * subset of the repos that got a `RenderedMerge` entry: since kanban 0046, * body/comments render independently of the diff, so a repo whose diff * failed still has a `RenderedMerge` (with `""` tree/html) carrying its * description and discussion — and it does *not* count here. That preserves * this counter's meaning for the callers that read it aloud * (`infra/scripts/site-build.ts`, `infra/scripts/site-publish.ts`) as "N of * M have a rendered diff": a build where pierre broke for every repo now * says `renderedCount: 0` rather than reporting success on prose alone. */ readonly renderedCount: number; /** Repos that lost their diff or their page, with the reason. */ readonly omitted: readonly {30 unmodified lines * it is small, it does not grow with the size of the change, and it is the * only thing on the page that shows the whole shape of the merge once the * diffs below it have been cut off by the budget. * * `bodyHtml` and `commentsHtml` are set by the caller, not here, and this * function throws only when the diff cannot be drawn. That split is what keeps * kanban 0008's promise once markdown lives on {@link RenderedMerge}: a broken * pierre render loses the diff and nothing else, so the caller must be able to * emit a `RenderedMerge` with the body and comments filled in even when the * `try`/`catch` around this function fires. */async function renderMerge( entry: RepoEntry,32 unmodified lines html: rendered.html, shown, withheld, // Placeholders filled in by the caller — see `publishSite`. They live // on `RenderedMerge` rather than here because the body/comment render // must survive a diff failure, which this function has no way to do // from inside a `throw`. bodyHtml: "", commentsHtml: [], }, sharedBlocks: rendered.sharedBlocks, };67 unmodified lines continue; }
// Markdown first, outside the `try` around `renderMerge`. Body and // comments come from `refs/meta/prs/<n>` and have nothing to do with // pierre, so the two failure modes are independent: kanban 0008 keeps a // repo whose diff will not render, and the description and discussion // sections belong to that same repo. Rendering them before the diff and // pushing a `RenderedMerge` in the `catch` below is what carries that // promise through this file. const merge = entry.latestMerge; const bodyHtml = renderMarkdown(merge.body ?? ""); const commentsHtml = merge.comments.map((comment) => renderMarkdown(comment.body), );
try { const { merge, sharedBlocks } = await renderMerge(entry, options.budget); renders.push(merge); const { merge: rendered, sharedBlocks } = await renderMerge( entry, options.budget, ); renders.push({ ...rendered, bodyHtml, commentsHtml });
// "N of M" as the page will say it, counted from the renderer's own // partition of the patch. Forge counts the same merge independently, off1 unmodified line // agreeing is worth checking and worth saying when they do not: a // disagreement means one of them is reading the merge wrong, and the // page would show whichever number happened to be wired up. const total = merge.shown.length + merge.withheld.length; const claimed = entry.latestMerge.files.length; const total = rendered.shown.length + rendered.withheld.length; const claimed = merge.files.length; progress( `${repoPath}: ${merge.shown.length} of ${total} files, ` + `${byteLength(merge.html)} bytes` + `${repoPath}: ${rendered.shown.length} of ${total} files, ` + `${byteLength(rendered.html)} bytes` + (total === claimed ? "" : ` (forge counted ${claimed})`) + // Not a failure — the markup is still correct — but the page is // roughly twice the size it should be, and the cause would be an7 unmodified lines const reason = message(error); omitted.push({ repo: repoPath, reason }); omit(repoPath, reason); // Keep the description and the discussion visible even though the diff // could not be drawn. Empty `tree`/`html`/`shown`/`withheld` is what the // repo page reads as "no diff panel", the same as it read before this // change, and `bodyHtml`/`commentsHtml` carry through independent of // pierre. renders.push({ repo: repoPath, mergeSha: merge.mergeSha, tree: "", html: "", shown: [], withheld: [], bodyHtml, commentsHtml, }); } }
14 unmodified lines prerenderInputFile, site, repoCount: admitted.length, renderedCount: renders.length, // Count the diffs, not every `RenderedMerge`. The catch block above // pushes a `RenderedMerge` with `html: ""` when pierre fails, so it can // still carry body/comment HTML; those entries do not have a rendered // diff and must not inflate this counter — see the field's doc. renderedCount: renders.filter((merge) => merge.html !== "").length, omitted, };}infra/scripts/site/verify.test.ts
114 unmodified lines115116117118119120121122123124125114 unmodified lines }, mergedPrCount: 1, lastActivity: MERGED_AT, commitDays: [ { date: "2026-06-29", count: 1 }, { date: "2026-06-30", count: 0 }, { date: "2026-07-01", count: 3 }, ], };}
infra/scripts/site/verify.ts
194 unmodified lines195196197198198199200201194 unmodified lines * admits any number of segments, and a repo at the server root is one. The * root itself is excluded, because `latest/index.html` is the index of the * whole site rather than anybody's page. Kanban 0040 has the shapes this * misses — a bare `.html` file that `auto-trailing-slash` serves at the same * misses — a bare `.html` file that `drop-trailing-slash` serves at the same * URL, anything outside `latest/`, a symlinked directory — and why widening it * is an allow-list over the whole of `dist/` rather than a wider walk here. *kanban/0052-give-a-published-bundle-a-producturl-and-say-it-came-from-a-.md
12345678910111213141516171819202122---id: 52title: give a published bundle a productUrl, and say it came from a private repotier: 3ceremony: standardarea: apps/websource: - PR #32 review, filed rather than fixed thererelates: - 48 - 7---
publicationEntry renders a verified bundle as an ordinary repo page, deliberately — no new component, no new shape, and Repo.tsx/Home.tsx untouched. Two things are missing as a result.
productUrl is carried inside the signed bundle and rendered nowhere, because RepoEntry has no field for it. And nothing on the page tells the reader the work came from a repository the site cannot read, which a page sourced that way arguably owes them. Both need RepoEntry extended in packages/shared/src/forge.ts plus Repo.tsx and Home.tsx.
Fold in PublicationComment gaining kind/verdict at the same time. buildRecord flattens comment and review events into {at, actor, body}, so a published approve currently renders as a plain comment; understating an approve was chosen over inventing kind: "review". Fixing it changes the signed schema, so it wants to land with the other schema-adjacent work rather than on its own.
The adapter rendering a publication as an ordinary repo page is a choice, not an oversight — the seam between 'renders at all' and 'renders with its own affordances' is where PR #32 stopped.
Also open, and deliberately not answered by the adapter: commitDays is empty for a publication. Drawing a grid from a bundle's single date either implies the source repo did exactly that much that day, or needs the repo's real activity shape — which discloses working volume and cadence for a repository nobody agreed to publish. That is kanban 0048's open question 3, not something a default should settle.package.json
56 unmodified lines5758596061626364656667686956 unmodified lines "@code/forge": "workspace:*", "@code/shared": "workspace:*", "@code/viewer": "workspace:*", "@resvg/resvg-wasm": "^2.6.2", "@types/bun": "^1.3.14", "@types/node": "^26.1.1", "oxfmt": "^0.59.0", "oxlint": "^1.74.0", "oxlint-tsgolint": "^0.25.0", "satori": "^0.19.3", "typescript": "catalog:", "vitest": "catalog:", "wrangler": "4.118.0"packages/forge/src/assemble.test.ts
143 unmodified lines1441451461471481491506 unmodified lines157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188143 unmodified lines mergedPrCount: 0, merge: null, prRefs: [], commitDays: [], }); expect(entry.latestMerge).toBeNull(); expect(entry.mergedPrCount).toBe(0);6 unmodified lines mergedPrCount: 12, merge: trail, prRefs: [], commitDays: [{ date: "2026-08-04", count: 3 }], }); expect(entry.mergedPrCount).toBe(12); expect(entry.latestMerge?.enriched).toBe(false); });
it("passes the trail's commitDays through to the entry", () => { // The seam the contribution grid depends on: the per-day series read by // git-source.ts has to reach the RepoEntry that ships. A `repoEntry` that // dropped the field would type-check (the field is required, so it would // fail to compile — but only if there is a caller like this one asserting // its contents rather than the type alone). const days = [ { date: "2026-08-02", count: 2 }, { date: "2026-08-03", count: 0 }, { date: "2026-08-04", count: 5 }, ]; const entry = repoEntry(repo, mirrors, { lastActivity: "2026-08-04T00:00:00Z", mergedPrCount: 1, merge: trail, prRefs: [], commitDays: days, }); expect(entry.commitDays).toEqual(days); });});
describe("siteData", () => {packages/forge/src/assemble.ts
3 unmodified lines4567891053 unmodified lines6465666768697071727374757640 unmodified lines1171181191201211221233 unmodified lines * makes the merge-trail path testable without a git server. */import type { CommitDay, MergedPr, MirrorLinks, RepoEntry,53 unmodified lines * seen as a change. See kanban #34. */ readonly prRefs: readonly PublicMetaRef[]; /** * Per-day commit counts on the default branch, oldest first, dense in the * site's zone. Feeds the contribution grid; see * {@link ../git-source!commitDays} for the definition and * {@link @code/shared/forge!CommitDay} for the shape. */ readonly commitDays: readonly CommitDay[];}
/**40 unmodified lines latestMerge: trail.merge === null ? null : mergedPr(trail.merge), mergedPrCount: trail.mergedPrCount, lastActivity: trail.lastActivity, commitDays: trail.commitDays, };}
packages/forge/src/build.ts
16 unmodified lines171819202122234 unmodified lines2829303132333435363738395 unmodified lines454647484950515253545556575810 unmodified lines69707172737475767778798081828314 unmodified lines989910010110210310410510610710810911011111211311411511611711810 unmodified lines12913013113213313413514 unmodified lines15015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724816 unmodified linesimport { tmpdir } from "node:os";import { join } from "node:path";import type { SiteData } from "@code/shared/forge";import { PUBLICATION_REF_PREFIX } from "@code/shared/publication";import { type AnonymousProbe, anonymousEnumeration,4 unmodified linesimport { type AdmittedRepo, collectRepos } from "./collect";import { exec } from "./exec";import { gitRepoSource } from "./git-source";import { collectPublications, type PublicationScan } from "./publication";import { fetchPublicationRepo, gitPublicationSource,} from "./publication-source";import { repoHandle } from "./redact";
export const DEFAULT_HOST = "git.fugl.dev";
5 unmodified lines readonly onProblem?: (note: string) => void; /** Called only for repos that are admitted into the public site data. */ readonly onAdmit?: (admitted: AdmittedRepo) => void; /** * Once per publication bundle that will not publish, with the reason. * * Separate from `onProblem` because these name a repo that declared itself * public *and* a publication repo, so nothing here is redacted, and because * a dropped bundle is the mechanism working rather than a repo being wrong. */ readonly onBundleDrop?: (reason: string) => void; /** Injectable for tests; defaults to the wall clock. */ readonly now?: () => Date; /**10 unmodified lines
export interface BuildResult { readonly data: SiteData; /** * The host this run enumerated, echoed back. * * A caller that needs to build a URL against the same server should not have * to re-derive which host was used — it defaults when unset, so the caller's * copy and this one can disagree, and a clone URL is exactly the kind of * string where that disagreement is invisible until someone clicks it. */ readonly host: string; /** * How many paths the enumeration returned, before either filter. *14 unmodified lines readonly publicTips: readonly AdmittedRepo[]; /** False when an admitted public repo could not be read completely. */ readonly complete: boolean; /** * One entry per repo that declared itself a publication repo, carrying the * bundles this build **verified** and the counts of what it examined. * * The counts are not decoration: this is a gate whose input is discovered, * so `infra/scripts/site/publications.ts` refuses a scan that examined * nothing rather than reading it as a repo with nothing to say (ADDENDUM * rule 7). And `admitted` is the only route by which a slug may become an * expectation downstream — see the note on independent provenance in * `./publication.ts`. */ readonly publicationScans: readonly { repo: string; scan: PublicationScan; }[];}
export async function buildSiteData(10 unmodified lines const onProblem = options.onProblem ?? (() => undefined); const now = options.now ?? (() => new Date()); const publicTips: AdmittedRepo[] = []; const publicationRepos: string[] = []; let complete = true;
const scratch = await mkdtemp(14 unmodified lines }, onAdmit: (admitted) => { publicTips.push(admitted); if (admitted.publicationRepo) publicationRepos.push(admitted.path); options.onAdmit?.(admitted); }, });
// Only repos that already cleared anonymous enumeration and their own // `public = true` are candidates here — `publicationRepos` is filled from // `onAdmit`, which fires after both. So a publication read never reaches a // repo the ordinary gate would have refused. const publicationScans = await readPublications({ host, paths: publicationRepos, workDir: join(scratch, "publications"), onDrop: options.onBundleDrop ?? (() => undefined), onProblem, });
return { data: siteData(repos, now().toISOString()), host, enumeratedCount: enumerated.length, publicTips, complete, publicationScans, }; } finally { await rm(scratch, { recursive: true, force: true }); }}
/** * Read every publication repo, each into its own scratch git dir. * * A failed fetch is a `PublicationScan` with nothing discovered rather than an * omitted entry, so the floor downstream sees it and refuses. Dropping the * entry entirely would make an unreadable publication repo indistinguishable * from one that was never declared — which is the failure that looks like * success, and the reason the counts exist at all. */async function readPublications(options: { readonly host: string; readonly paths: readonly string[]; readonly workDir: string; readonly onDrop: (reason: string) => void; readonly onProblem: (note: string) => void;}): Promise<readonly { repo: string; scan: PublicationScan }[]> { if (options.paths.length === 0) return []; await mkdir(options.workDir, { recursive: true });
return Promise.all( options.paths.map(async (path) => { const gitDir = join(options.workDir, `${repoHandle(path)}.git`); const fetched = await fetchPublicationRepo({ host: options.host, path, gitDir, defaultRef: PUBLICATION_DEFAULT_REF, bundleRefPrefix: PUBLICATION_REF_PREFIX, }); if (!fetched) { options.onProblem( `${path}: declared a publication repo and could not be fetched anonymously`, ); return { repo: path, scan: { admitted: [], refsDiscovered: 0, verified: 0, dropped: 0, withdrawn: 0, }, }; } const scan = await collectPublications({ source: gitPublicationSource({ gitDir, allowedSignersFile: join(gitDir, "allowed_signers.materialised"), defaultRef: PUBLICATION_DEFAULT_REF, bundleRefPrefix: PUBLICATION_REF_PREFIX, }), onDrop: (reason) => { options.onDrop(`${path}: ${reason}`); }, }); return { repo: path, scan }; }), );}
/** Where the publication repo's default branch lands in the scratch clone. */const PUBLICATION_DEFAULT_REF = "refs/forge/publication-default";
/** The real probe: subprocesses, and a readability check on the key itself. */const realProbe: AnonymousProbe = { run: (command) => exec(command.command, command.args),packages/forge/src/collect.test.ts
33 unmodified lines3435363738394097 unmodified lines138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176189 unmodified lines36636736836937037137264 unmodified lines43743843944044144244333 unmodified lines prJson: null, }, prRefs: [], commitDays: [{ date: "2026-08-04", count: 1 }],};
/**97 unmodified lines tipSha: TRAIL.tipSha, prRefs, configSha: sha256(PUBLIC_CONFIG), publicationRepo: false, }, ]); });
/** * The wire kanban #48 added, asserted for the same reason as `configSha` * above and `prRefs` before it: both *ends* of this field are covered — * `project-config.test.ts` pins that only the literal `true` parses, and the * build's consumer is tested on a hand-built scan — while the run between * them was exercised only with `false`, which is also what a constant would * produce. A repo that declares itself a publication repo and arrives at * `onAdmit` as an ordinary one publishes nothing, silently. */ it("carries a repo's publicationRepo declaration through to onAdmit", async () => { const declared = PUBLIC_CONFIG.replace( "\tpublic = true", "\tpublic = true\n\tpublicationRepo = true", ); const admitted: AdmittedRepo[] = []; await collectRepos({ host: "git.fugl.dev", paths: ["russ/ok"], source: sourceOf({ "russ/ok": declared }, { "russ/ok": TRAIL }), onOmit: () => undefined, onProblem: () => undefined, onAdmit: (entry) => { admitted.push(entry); }, }); expect(admitted.map((entry) => entry.publicationRepo)).toEqual([true]); });
/** * The wire kanban #36 added, asserted the same way and for the same reason: * `collectRepo` could hand `onAdmit` a constant, or the sha of some other189 unmodified lines mergedPrCount: 7, merge: null, prRefs: [], commitDays: [{ date: "2026-08-04", count: 1 }], };
it("drops the repo rather than publishing the contradiction", async () => {64 unmodified lines mergedPrCount: 0, merge: null, prRefs: [], commitDays: [], }, }, ),packages/forge/src/collect.ts
76 unmodified lines777879808182838485868788899063 unmodified lines15415515615715815916076 unmodified lines readonly prRefs: readonly PublicMetaRef[]; /** sha256 of the `project.config` text the privacy gate admitted on. */ readonly configSha: string; /** * The repo's own declaration that it carries signed publication bundles * (kanban #48). Handed over here rather than re-read later because this is * the one place the config was actually parsed, and re-fetching a repo's * config to ask a second question about it would be a second answer that * could disagree with the first. */ readonly publicationRepo: boolean;}
/**63 unmodified lines // From the admission itself, so this is the sha of the very text that // opened the gate for this repo — not of a second read of the same ref. configSha: admission.configSha, publicationRepo: admission.config.publicationRepo, });
const repo = repoRef(path, admission.config.displayName, options.host);This view needs a browser with declarative shadow DOM: Chrome 111, Safari 16.4, or Firefox 123. Read the source instead.
52 of 80 files shown — 28 past this page's size limit. Browse or clone the repository to read the whole merge.
clone
$ git clone https://git.fugl.dev/russ/codeanonymous, no account$ git clone ssh://git.fugl.dev/russ/codeneeds the bastion ProxyCommand