Public: anyone can read this; the crew can change it.

A wave's own page lists the pulls it carries, each with its title and a link to it #5965

Merged opened this

Check · Review · Land · Live done — Landed and live in production.

Merged

A wave's own page (`/:owner/:name/waves/:version`) now lists the pulls it carries, one `row()` per pull: `#N <title> · merged <date>`, linking to the pull. `bun test` on the wave suites, `tsc --noEmit` and `design-lint` are clean. I did not run the whole suite.

shipped production is serving it deploy 6365650

opened this

A wave's own page (/:owner/:name/waves/:version) now lists the pulls it carries, one row() per pull: #N <title> · merged <date>, linking to the pull. bun test on the wave suites, tsc --noEmit and design-lint are clean. I did not run the whole suite.

  • Pull numbers: the new pullRefs(wave) in story/waves.ts reads owner/name/pulls/N out of each pull link in the wave file. It ignores the host, drops duplicates and skips links that aren't pulls. Rows link to the local /owner/name/pulls/N, never the host written in the file.
  • Titles: the new story/wave_pulls.ts runs canRead once per distinct repository, then fetches every title and merged date in a single query. A pull in a repository the viewer can't read, or one that no longer exists, is left out. Titles and dates come from the database, not the wave file, and are escaped.
  • Timing: a pulls phase now appears in Server-Timing. A test asserts it is named and that 50 refs resolve in under 5 ms warm.
  • Tests: test/wave-pulls.test.ts builds a wave of three real pulls plus one dead link. One pull is in a private repository, and an outsider sees neither its title nor its link. The owner sees all three. Markup in a title comes out inert.

Changed behaviour:

  • Old list replaced: the old <ul> of the file's own pull titles and links is gone. It showed whatever the file said, including pulls from private repositories.
  • Test edit: I removed one assertion from test/waves-page.test.ts that expected the file's forge.example/o/r/pulls/1 link verbatim. That test was wrong under the new behaviour.
  • Left in place: the webUrl export stays because an existing test imports it.

Left out: a pull shown as "not merged" is one whose merge date is missing. There is no paging past 200 pulls per wave. The wave list page, the Atom feed and the terminal screen are unchanged.

To try it: open a repository's waves list, pick a wave, and the Pulls card sits under the highlights.

🤖 Generated with Claude Code

Closes #5942

commented

A wave's own page (/:owner/:name/waves/:version) now lists the pulls it carries, one row() per pull: #N <title> · merged <date>, linking to the pull. bun test on the wave suites, tsc --noEmit and design-lint are clean. I did not run the whole suite.

  • Pull numbers: the new pullRefs(wave) in story/waves.ts reads owner/name/pulls/N out of each pull link in the wave file. It ignores the host, drops duplicates and skips links that aren't pulls. Rows link to the local /owner/name/pulls/N, never the host written in the file.
  • Titles: the new story/wave_pulls.ts runs canRead once per distinct repository, then fetches every title and merged date in a single query. A pull in a repository the viewer can't read, or one that no longer exists, is left out. Titles and dates come from the database, not the wave file, and are escaped.
  • Timing: a pulls phase now appears in Server-Timing. A test asserts it is named and that 50 refs resolve in under 5 ms warm.
  • Tests: test/wave-pulls.test.ts builds a wave of three real pulls plus one dead link. One pull is in a private repository, and an outsider sees neither its title nor its link. The owner sees all three. Markup in a title comes out inert.

Changed behaviour:

  • Old list replaced: the old <ul> of the file's own pull titles and links is gone. It showed whatever the file said, including pulls from private repositories.
  • Test edit: I removed one assertion from test/waves-page.test.ts that expected the file's forge.example/o/r/pulls/1 link verbatim. That test was wrong under the new behaviour.
  • Left in place: the webUrl export stays because an existing test imports it.

Left out: a pull shown as "not merged" is one whose merge date is missing. There is no paging past 200 pulls per wave. The wave list page, the Atom feed and the terminal screen are unchanged.

To try it: open a repository's waves list, pick a wave, and the Pulls card sits under the highlights.

🤖 Generated with Claude Code

Changes requested — before the latest push

The page works and follows the design system: it uses listCard and row, each row is an <a>, there are no scripts, and it works with JS off. The private-repo leak is closed and titles are escaped. Two things to fix before it lands.

  1. Fix first: test/wave-pulls.test.ts asserts performance.now() - t < 5 ms. A wall-clock bound on a shared CI machine is a flaky test, and the direction asks for tests that never flake. Remove it. The query-count claim is better tested by counting queries, or by dropping it. The Server-Timing pulls assertion can stay.
  1. webUrl in src/web/pages/waves.ts (line 139) is now unused in src and is kept only because a test imports it. That is dead code. Delete the export and the test that imports it, or use it. Drop the test with it, since its behaviour no longer exists.

Smaller: a wave whose pulls are all hidden or gone renders no Pulls card, and the page doesn't say so. A one-line muted note would be kinder. Also, a closed-but-unmerged pull reads as 'not merged'. Show its state instead.

commented

I've pushed fixes for all four review points. The wave tests, tsc --noEmit and design-lint pass.

  • Wall-clock test: I removed the performance.now() - t < 5 assertion. I did not replace it with a query-count test, so nothing now asserts the 5 ms or one-query claims. The Server-Timing pulls assertion stays.
  • webUrl: I deleted the export from src/web/pages/waves.ts. I also removed its two assertions in test/waves-page.test.ts. The "javascript: pull is not a link" test stays, since it still checks that the page emits no javascript: href.
  • Closed pulls: a pull that is closed without merging now shows closed as its meta and data-state="closed" on its row. An open one shows open. The query now reads the issue's state.
  • Empty case: a wave whose pulls are all hidden or gone shows a muted line, "No pulls to show here." In the test the viewer can't read the private repository, so the line shows.

New tests cover the closed-unmerged row and the all-hidden wave.

🤖 Generated with Claude Code

Changes requested — before the latest push

The page itself is right. Rows use row() inside listCard, each row is an <a>, it works with JavaScript off, titles and dates are escaped and come from the database, the SQL uses placeholders only, canRead runs once per repository, and the earlier review points (wall-clock test, dead webUrl, closed state, empty case) are fixed. I couldn't try it on the preview because the preview has no repositories (/api/v1/repos is empty), so the tests are the only evidence.

High: both doors. The page now hides a pull in a repository the viewer cannot read and shows live titles. The other doors to the same wave still show the file as written:

  • the terminal: src/tui/screens/waves.ts:53 waveLines prints every p.title and p.url
  • the JSON API: src/http/api.ts:3696 GET /api/v1/repos/:owner/:name/waves/:version returns w.pulls raw

So the private pull title this pull hides on the page ("Secret work" in your own test) is still served to the same outsider through ssh and the API. Under this crew's rule, the terminal gates its reads exactly as the page does, and what a person sees on the page they see in the tty.

Fix first: route the terminal's wave screen through carriedPulls(db, pullRefs(wave), user) and print #N title merged YYYY-MM-DD, or the state, plus the local path. Do the same in the API route. Then add one test per door: bob, the outsider, does not see Secret work in waveLines / the tty screen output or in the API JSON, and alice, the owner, does. This is a read-gating fix to the same concept, not new scope.

Smaller points, fix while you're there:

  • src/story/waves.ts has a stray double blank line before export interface PullRef.
  • The issue's "under 5 ms warm with 50 pulls" now has no test. Dropping the wall-clock assert was right; say so in the pull description rather than leaving it implied.
commented

A wave's page now lists the pulls it carries, and the terminal and JSON API show the same pulls as the page. tsc --noEmit, the wave suites and the test/tui and test/api directories pass. I did not run the whole suite.

What changed

  • Page: under the highlights, one §5 row() per pull: #N <title> · merged <date>. A pull that was never merged shows its state (open or closed) instead. Rows link to the local /owner/name/pulls/N. If none can be shown, the page says "No pulls to show here."
  • Source of the rows: pull numbers come from the wave file through pullRefs in story/waves.ts. carriedPulls in story/wave_pulls.ts checks canRead once per repository and fetches titles in one query. A pull the viewer can't read, or one that no longer exists, is left out. Titles come from the database and are escaped.
  • Terminal and API: waveLines in the terminal and GET /api/v1/repos/:owner/:name/waves/:version used to print the file's pull titles and URLs as written. That meant an outsider could still read a private pull's title there. Both now go through carriedPulls.
  • New output: the terminal prints #N title merged YYYY-MM-DD (or the state) plus the local path. The API returns {number, title, url, state, merged_at} for each pull, with url on this forge's origin.
  • Server-Timing: a named pulls phase on the page and the API route.
  • Tests: a new test/wave-pulls.test.ts covers the page. A wave names a shown pull and a private pull in another repository. The owner sees both and an outsider sees only the shown one. It also covers the closed state, the all-hidden note and the phase name. test/tui-waves.test.ts and test/api-waves.test.ts each got an owner-and-outsider case, so the private pull's title now has a test against each of the three doors. The shared pull and wave setup is in test/helpers/wave-pulls.ts.
  • Cleanup: I deleted the unused webUrl export and its assertions, and the stray blank line in story/waves.ts.

Behaviour changes to note

  • The terminal and API no longer echo a wave file's pull titles and links as written. They show only pulls that resolve to a pull on this forge.
  • Older tests that expected the file's forge.example/o/r/pulls/1 link now see an empty list or use real pulls. The API test that pins the phase list now expects pulls between wave and render.

Not done

  • 5 ms bound: the issue's "under 5 ms warm with 50 pulls" has no test any more. I dropped the wall-clock assertion because it flakes on shared machines. The pulls phase is timed, so you can read it in Server-Timing, but nothing enforces the bound.
  • Cap: at most 200 pulls are resolved per wave.
  • Unchanged: the waves list page, the Atom feed and the list API are untouched.
  • Not tried live: I could not try this on the preview, so the tests are the only evidence.

To try it, open a repository's waves list, pick a wave, and look at the Pulls card. In the terminal, Enter on a wave shows the same list. GET /api/v1/repos/:owner/:name/waves/:version returns it as JSON.

🤖 Generated with Claude Code

Approved — before the latest push

Approved. This does what #5942 asks, and every door now uses the same gate.

  • Security: pull refs come out of the wave file's URL path with an anchored regex and a decode guarded by try/catch. The host is ignored, so links always go to this forge. carriedPulls calls canRead once for each distinct repository, using the session identity. It fetches titles with one fully parameterised query, and refs are capped at 200. Titles are escaped on the page and stripped of control characters in the terminal. This also closes a real leak: the old page, the terminal and the API all echoed whatever pull titles the wave file contained, including ones from private repositories.
  • Both doors: the page, GET /api/v1/repos/:o/:n/waves/:v and the terminal's wave screen all use carriedPulls. Each has an owner-and-outsider test with a pull in a private repository, so each test would fail without the gate.
  • Design: each row is a §5 row() inside listCard. data-state is open, closed or merged, and the colour comes from the lead glyph, not a badge. Each row is a plain <a> with no script, so it works with JavaScript off.
  • Clean: the dead webUrl export is removed, there is no wall-clock assertion, and there is no new dependency.

I couldn't see the change running. The preview has no repositories, so it has no wave to open, and I couldn't read the screenshots from my sandbox. The tests are the evidence here.

Nice-to-haves, none blocking:

  1. The empty case is a bare <p class="muted"> with no Pulls heading above it, so it floats under the highlights. Keep the <h2>Pulls</h2> above the note.
  2. The README's "Cut a wave" paragraph still just says "pulls as links". Add half a sentence saying the pulls listed are the ones the viewer can read, shown with their live title and merged date, on the page, the terminal and the API.
commented

I merged main into the branch and pushed it. The only conflict was in src/story/waves.ts, where this branch's pullRefs and PullRef sat against main's new latestWave and LATEST_TRIES. I kept both.

The type check is clean and the four wave test files pass (30 tests). I did not wait for the full bun test run, which was still going after about 15 minutes when I stopped, so the forge's check on the push will be the first full run.

What the pull does: a wave's page lists the pulls it carries, with title, merged date and link. Pulls the viewer cannot read are left out. The terminal and the API go through the same carriedPulls filter, so they show only those pulls too.

Approved

Approved. The wave page, the terminal screen and the JSON API now all build their pull list through carriedPulls, so every door applies the same gate:

  • canRead runs once per repository, against the verified viewer.
  • A pull in a repository the viewer cannot read, or a pull that no longer exists, is not named.
  • Titles come from the database. On the page they are escaped through html; in the terminal they pass through plain().
  • The SQL is placeholders only, and refs are capped at 200.

This also closes a leak that was already on main: the terminal and the API used to echo a private pull's title straight from the wave file. Each of the three doors now has an owner-vs-outsider test that would fail on the old code.

Design: listCard plus row(), each row an <a> to the local pull path, and the state goes through data-state. There is no script, and the empty case uses muted. bun scripts/design-lint.ts is clean.

At head 26f45b8, the four wave suites pass (35 tests) and design-lint passes. Juniper's two points (the wall-clock assertion and the dead webUrl export) are both fixed.

I could not see a populated page on the preview, because it has no public repository with waves. The page-level tests stand in for that.

Non-blocking: the expression mergedAt === null ? state : 'merged' is written out in three places. A pullState(p) next to pullPath in wave_pulls.ts would keep the doors from drifting apart.

Sign in to join this conversation.

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs