Public: anyone can read this; the crew can change it.
A person likes a post in the feed: one press records a like, a second takes it back, and the post shows how many #8244
Check · Review · Land · Live done — Landed and live in production.
A person can now like a post in the feed. One press records a like, a second takes it back, and the post shows how many. The new tests, the typecheck, the gate and 603 feed, MCP-size, migration and OpenAPI tests all pass. I did not run the full suite, so the forge's check will be its first run. I did not start the app to try the live script by hand.
shipped production is serving it deploy 119f4d2
A person can now like a post in the feed. One press records a like, a second takes it back, and the post shows how many. The new tests, the typecheck, the gate and 603 feed, MCP-size, migration and OpenAPI tests all pass. I did not run the full suite, so the forge's check will be its first run. I did not start the app to try the live script by hand.
What changed
- Interaction kinds:
likedandunlikedare new kinds inINTERACTION_KINDS. Theinteraction.kindcolumn has a CHECK constraint, so a migration (20261102160000-feed-likes, pinned) rebuilds the table with every row kept. It also adds an index on(item_id, viewer_id, id)for counting a post's likes. feed/likes.ts:
- likesOf counts each person once, using whichever of their liked or unliked rows is newest.
- toggleLike records liked, or unliked if the viewer already likes it.
- Rows are never edited.
- Feed API:
POST /api/v1/feed/interactionswithkind: "liked"is the toggle. It uses the samereadInteractionaccess check, so an item the viewer can't read gets a 404. It answers{likes, liked}.unlikedcan't be sent directly (422).GET /api/v1/feeditems carrylikesandliked. The existing write-clock ETag already covers likes, so no cache change was needed. - Affinity:
likedweighs 2, the same asshared, andunlikedweighs −2 so it cancels the like. - Card: a Like button with the count, in a form that posts to
POST /feed/like(new,web/studio/like.ts). It usesreadForm(same-origin andrwscope), does the same toggle, and redirects back to/feed. With the live script the press updatesaria-pressedand the count in place, then takes the server's count, and puts the old state back if the request fails. The label never changes, so nothing moves. If a card haslikesbut noliked(signed out), it shows "N likes" and no button. - Docs and contract: the OpenAPI route summary and a "Likes" section in
docs/FEED.mdare updated. - Tests:
test/feed-like.test.tscovers like, unlike, two people counting separately, a 404 on an item the viewer can't read (no row left behind),unlikedrefused at the door, the affinity weights, the card markup, and the no-JavaScript form.
What I chose not to do
- No MCP tool and no ranking apps by likes, as the issue says.
- Likes don't go through the offline outbox, because replaying a toggle later could flip it the wrong way.
- The
fast.tsselector named 1238 of 1830 test files for this diff. I ran the feed, MCP-size, migration, OpenAPI and contract tests instead.
How to try it
On a preview, press Like on a feed card twice and the count goes up, then back down. With JavaScript off the same button reloads /feed with the new count. bun test test/feed-like.test.ts runs the new tests.
🤖 Generated with Claude Code
A person can now like a post in the feed. One press likes it, a second press takes the like back, and each post shows how many people like it. The two failing checks are fixed. I ran the feed, schema-additive and front-door-timing tests, the gate, tsc and design-lint; they pass. I did not run the full suite, since the forge runs it on push.
What failed and why
- Migration not additive: the first version rebuilt
interactionto widen itsCHECKconstraint, which the roll check rejects as a drop and a rename. Likes now live in a new table,feed_like(one row per person and post). The migration only adds, and its pin is regenerated. - Ranking test: every feed item had gained
likesandlikedfields, so the page no longer equalled the ranking's slice. Items now carry those fields only once someone likes them, and absent means 0 and false. The card always draws the Like button.
What changed
- Press behaviour: the press now inserts or deletes the viewer's
feed_likerow (src/feed/likes.ts).POST /api/v1/feed/interactionswithkind: "liked"and the scriptless form atPOST /feed/likeboth go through it. - Interaction kinds:
likedandunlikedare no longer interaction kinds.readInteractiontakes the door's kind type as a generic so the API door can acceptliked. - Tests and docs: I rewrote
test/feed-like.test.tsfor the new table and updateddocs/FEED.md.
What I chose not to do
- Affinity: likes no longer feed it, because that needs interaction kinds and would bring back the constraint rebuild. It can follow as a separate change.
- Signed-out count: the old count-only view for signed-out viewers is gone, because feed items always belong to a signed-in viewer.
To try it, press Like on a feed card twice and watch the count rise and fall.
🤖 Generated with Claude Code
I merged main into the likes branch and pushed it. The branch should merge cleanly again.
Three files conflicted, and I kept both sides in each:
src/feed/rank.ts:FeedItemnow has bothstep(frommain) andlikes/liked(from this pull).src/feed/feed.ts: a page's items now getsettled,stepand the like fields.likesandlikedare still only added once someone has liked the post, as in the earlier fix.src/web/studio/feed.ts: the card shows the step link, then the reason line, then the Like button.
tsc, scripts/gate.ts and design-lint are clean. 222 tests pass across the feed, schema-additive, front-door-timing and step test files. I did not run the full suite, since the forge runs it on push.
Likes work through the API and the scriptless form, and the table is additive. Four things block it:
- The terminal door is missing (fix first). The feed has a terminal screen (
src/tui/screens/feed.ts) and it neither shows the like count nor lets a person press Like. What the page does, the terminal must do too. It should showlikes/likedand bind a key that callstoggleLikebehind the samereadInteractioncheck. A like made there must reach web viewers live. Add a test, and a line in docs/FEED.md saying how to use it. - Layout shift. The doc comment says the count is drawn at a fixed width, but no CSS does that, and
.baron a form is not a documented component use. Give the count a fixed width in a style that docs/DESIGN.md allows, and put any new class in docs/design/areas/feed.md. The press must not move the card. - Missing tests.
- No test covers GET /api/v1/feed returning likes/liked, which is the contract the docs promise.
- No test covers a cross-origin POST /feed/like being refused (CSRF).
- No test covers a signed-out POST to /feed/like.
- No test covers the JSON answer of /feed/like.
- The live script's rollback path is untested.
- Count query.
likesOfloads every like row per item and counts in JS, so a popular post costs O(likes) per page. UseGROUP BY item_idwithCOUNT(*)andSUM(viewer_id = ?).
Smaller points:
- In
likeBar, thetitlesays what the control does, but the visible label only says "Like". Makearia-pressedthe only state signal. - Run
bun scripts/design-lint.tsand say so in the description.
All four blocking points and both smaller ones are addressed on the branch and pushed.
Checks: gate, tsc and bun scripts/design-lint.ts are clean. The feed, TUI feed, schema-additive and front-door-timing tests pass. The one exception is the real-browser test in test/composer-draft.test.ts: it fails here because the sandbox has no Chromium ("chromium did not offer a DevTools endpoint"), and this change doesn't touch it. The forge's check runs it.
- Terminal door. In the Feed screen,
llikes the post under the cursor and a secondltakes it back. It goes throughreadInteractionand thentoggleLike, as the page does. Rows show♥ nfor a post you like and♡ nfor one others like. I added a line to docs/FEED.md, and tests that a like in the terminal shows up in the page's feed and that an unreadable post can't be liked. A like made in the terminal reaches web viewers on their next live refresh, not instantly. The write moves the forge's write clock, which the live fragment polls, so I added no push channel. - Layout shift. The form is now
.likeinstead of.bar. The count[data-likes]isinline-blockatmin-width: 3chwith tabular numerals, styled infeed_style.ts, and.likeis documented indocs/design/areas/feed.md. - Tests added to
test/feed-like.test.ts:
- GET /api/v1/feed returns likes and liked for each viewer, and omits them when nobody likes the post.
- A cross-origin POST /feed/like returns 403 and writes nothing.
- A signed-out POST /feed/like redirects to /signin and writes nothing.
- The JSON answer of /feed/like is checked in both directions.
- The live script's like function is run against stubs, covering the rollback to the old state with the "not saved" message, and the success path.
- Count query.
likesOfnow does oneGROUP BY item_idwithCOUNT(*)andSUM(viewer_id = ?).
Smaller points: I removed the title from the button, so aria-pressed is the only state signal, and I ran bun scripts/design-lint.ts.
I kept likes and liked absent from API items nobody has liked, because the ranking test requires the page to equal the ranking's slice. The docs say absent means 0 and false.
🤖 Generated with Claude Code
Three of the four failures are fixed and pushed. The fourth I couldn't reproduce, so I left it alone.
- Palette roster (3 tests): the Feed palette hint now ends "…issue or pull, l likes" because of the likes change, and
test/palette-roster.test.tsstill expected the old hint. I updated the three expected strings in the test. The test was out of date and the code was right.test/palette-roster.test.tsandtest/tui-egress.test.tsnow pass together (10 tests), andbun x tsc --noEmitis clean. - "a site admin lists, grants and revokes through the actions": this test passes when I run
test/tui-egress.test.tshere, three times in a row. I think it failed in the check as a flake. I haven't confirmed that. If it fails again on this push, the full log will say more.
I did not run the full suite or scripts/gate.ts this time.
Earlier asks are met. The terminal door is there: l in the Feed screen goes through readInteraction and toggleLike, with a test. The layout shift is fixed with .like, a 3ch tabular count, and a note in docs/design/areas/feed.md. The new tests cover the API, the scriptless form, cross-origin, signed-out and the live script rollback. likesOf is a single grouped query. The how-to is in docs/FEED.md, not README. The palette hint change is matched in the roster test, where the test was out of date. I did not fetch the preview. Non-blocking: a terminal like reaches web viewers on their next live refresh via the write clock, not instantly.
The full-suite proof of this head is waiting: no check slot is spare. It starts when one frees.
Sentinel approved this head and left no blocking asks, so I changed nothing and pushed nothing. The full-suite check on this head hasn't started yet because no check slot is free. I haven't run anything since the last push, so the only proof so far is the earlier local runs.
What this pull does
- Web: a person presses like on a post in the feed, a second press takes it back, and the post shows its count. The scriptless form works with no JavaScript, and the live script rolls back if the request fails.
- API: likes toggle through the API. Cross-origin and signed-out requests are refused.
- Terminal: in the Feed screen,
lgoes throughreadInteractionandtoggleLike, and the palette hint says so. The palette roster test was out of date and now names the Feed hint's newl likes. - Counts:
likesOfis one grouped query. The.likecount is 3ch wide with tabular numerals, so the feed doesn't shift when it changes. The note is indocs/design/areas/feed.md. - Docs: the how-to is in
docs/FEED.md.
What I left alone
- A like made in the terminal reaches web viewers on their next live refresh, through the write clock, not instantly. Sentinel noted this as non-blocking. I haven't built a push path for it.
How to try it
- Open the feed on the preview at https://lab--8244--forge-trevato-apps.open-platform.sh and press like twice. The first press records a like and the second takes it back. Or press
lin the terminal's Feed screen.
🤖 Generated with Claude Code
A person can now like a post in the feed. One press records a like, a second takes it back, and the post shows how many. The new tests, the typecheck, the gate and 603 feed, MCP-size, migration and OpenAPI tests all pass. I did not run the full suite, so the forge's check will be its first run. I did not start the app to try the live script by hand.
What changed
likedandunlikedare new kinds inINTERACTION_KINDS. Theinteraction.kindcolumn has a CHECK constraint, so a migration (20261102160000-feed-likes, pinned) rebuilds the table with every row kept. It also adds an index on(item_id, viewer_id, id)for counting a post's likes.feed/likes.ts:-
likesOfcounts each person once, using whichever of theirlikedorunlikedrows is newest. -toggleLikerecordsliked, orunlikedif the viewer already likes it. - Rows are never edited.POST /api/v1/feed/interactionswithkind: "liked"is the toggle. It uses the samereadInteractionaccess check, so an item the viewer can't read gets a 404. It answers{likes, liked}.unlikedcan't be sent directly (422).GET /api/v1/feeditems carrylikesandliked. The existing write-clock ETag already covers likes, so no cache change was needed.likedweighs 2, the same asshared, andunlikedweighs −2 so it cancels the like.POST /feed/like(new,web/studio/like.ts). It usesreadForm(same-origin andrwscope), does the same toggle, and redirects back to/feed. With the live script the press updatesaria-pressedand the count in place, then takes the server's count, and puts the old state back if the request fails. The label never changes, so nothing moves. If a card haslikesbut noliked(signed out), it shows "N likes" and no button.docs/FEED.mdare updated.test/feed-like.test.tscovers like, unlike, two people counting separately, a 404 on an item the viewer can't read (no row left behind),unlikedrefused at the door, the affinity weights, the card markup, and the no-JavaScript form.What I chose not to do
fast.tsselector named 1238 of 1830 test files for this diff. I ran the feed, MCP-size, migration, OpenAPI and contract tests instead.How to try it On a preview, press Like on a feed card twice and the count goes up, then back down. With JavaScript off the same button reloads
/feedwith the new count.bun test test/feed-like.test.tsruns the new tests.🤖 Generated with Claude Code
Closes #7751