Public: anyone can read this; the crew can change it.
Feed model and ranker: a per-viewer read model of forge events, ranked, each item with a one-sentence reason #364
Check · Review · Land · Live done — Landed and live in production.
The feed model, ranker and API are in: a per-viewer read of forge events, asks and decision outcomes, ranked, each item with a one-sentence reason. `tsc` is clean and the new tests pass. I also ran the looks, asks, notify, event-audience, map and migrate suites alongside them, and they pass. I did not run the full `forge.json` check or the whole test suite; `test/feed.test.ts` is added to the chec…
shipped production is serving it deploy 1f3280d
Sign in to join this conversation.
new above
offline
The feed model, ranker and API are in: a per-viewer read of forge events, asks and decision outcomes, ranked, each item with a one-sentence reason.
tscis clean and the new tests pass. I also ran the looks, asks, notify, event-audience, map and migrate suites alongside them, and they pass. I did not run the fullforge.jsoncheck or the whole test suite;test/feed.test.tsis added to the check list.What changed
GET /api/v1/feed?limit=&cursor=returns{items, next_cursor}for the signed-in viewer only. An item is{id, kind, app, actor, at, link, reason, score}, andkindis one of run, ask, pull, decision, map or cost. The limit and cursor are validated.src/feed/items.ts):- Events go through
listEventsVisibleToand thenaudienceFilter. - Asks come fromaskLinesFor, so only what the viewer may decide. - Decision outcomes appear only for repos the viewer can write. - Cost items need owner-facts access.POST /api/v1/feed/interactionsrecords opened, dwelled, steered, told, swiped, shipped, shared, dismissed or muted in a newinteractiontable (migration 35). Browser sessions are same-origin checked and tokens needrw. Anevent:item takes its repo and actor from the event; any other item names its app asowner/name. Anything the viewer can't read returns 404, so nothing leaks.run.*events. Looks emitted nothing, so I addedlook.taken, sent when a full set of pictures is taken.askLinesFornow also returnskind,repoIdandnumber.src/feed/affinity.ts) is a decayed count per app and per actor with a 7-day half-life. It is seeded from ownership, pins, crew, the viewer's own merges, proposals and tells, and ledger outcomes, then moved by interactions.src/feed/rank.ts):score = urgency + 1.5·affinity + recency − repeats.- Each item already placed from the same app costs 1.2, and from the same actor 0.6. - A dismissal is −4 and a mute is −8. - The reason is the sentence for the term that led, for example
because you shipped “Fix the nav” yesterday.docs/FEED.mddescribes the feed and the CLAUDE.md map has a line forsrc/feed/. The tests cover ranking order, reason text, repeat decay, affinity decay, dismiss, mute, no leak across viewers (private repos, webhook events), cursor paging, input validation and a cross-origin refusal.Choices to check
score|idand resumes by id, or by score if that item is gone. Placed scores never rise, so paging is stable.Not done
To try it:
bun test test/feed.test.ts, or callGET /api/v1/feedwith a token.🤖 Generated with Claude Code
Closes #360