Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions KNOWLEDGE_BASE.md
Original file line number Diff line number Diff line change
Expand Up @@ -234,9 +234,9 @@ Commits, replies, quotes, scheduling, terminal submissions, and app (signed API)

Approved top-level commits about specific developments in technology, current affairs or substantive real-world events can receive brief, shared headlines. A headline identifies a particular release, feature, event, change, problem or finding, not a broad category such as "Laravel" or "AI tools". Technical and developer developments carry the most weight. /dev/null community activity also qualifies, including livestreams, Tom, app betas and feature updates. Quote commits include the directly quoted post's text when it is approved and still available, so a brief reaction can share the development of the post it quotes. Replies are not used for headline classification. Casual small talk, broad technology chat without a specific development, and evergreen lifestyle or hobby advice are skipped. OpenAI's Decisions API assesses this text for suitability and checks the closest existing headline names, shortlisted by semantic similarity; when none match, DeepSeek through OpenCode generates a headline from the same text. Posts can be processed together, and newly generated headlines about the same development share the first headline rather than creating duplicates. Different developments involving the same product do not share a headline merely because of that product. No account metadata is sent. A headline expires after fourteen days without a new commit link. This categorization does not change the original post and does not yet add visible headline labels to the interface.

The desktop sidebar shows signed-in members up to five **Hot topics** above **Who to follow**, with bold headlines and a truncated recap underneath when one is available. These aggregate approved top-level commits posted in the past eight hours, with extra weight for trending discussions. A topic needs at least two eligible top-level commits visible to you; replies do not count, and the list can show fewer than five topics or none. Only each author's strongest post contributes to a topic's ranking, so repeated posts from one profile do not lift it. The list respects mutes and blocks. The sidebar no longer repeats your own profile summary; your profile is still available from the main navigation.
The desktop sidebar shows signed-in members up to five **Hot topics** above **Who to follow**, with bold headlines and a truncated recap underneath when one is available. These aggregate approved top-level commits posted in the past twelve hours, with extra weight for trending discussions. A topic qualifies through top-level commits from more than one author visible to you, or a single sufficiently trending thread with engagement from other developers. Replies can contribute to that thread's trend weight, but do not count as separate top-level commits. Self-only activity adds no weight, while the author answering other developers receives the existing conversation bonus. Older threads need more engagement to qualify on trend weight alone. The list can show fewer than five topics or none. Only each author's strongest post contributes to a topic's ranking, so repeated posts from one profile do not lift it. The list respects mutes and blocks. The sidebar no longer repeats your own profile summary; your profile is still available from the main navigation.

Hot topics can also receive a short AI-written recap based on approved, available top-level commits from the past eight hours and public replies in their threads, including nested replies. Both the AI usefulness check and the recap writer receive the reply context: a question can support a recap when its answers provide useful substance. The sample includes one strongest root post per author, up to one hundred roots, plus up to one hundred of the newest replies across those threads. Recaps aim to explain the underlying situation or event in one or two short sentences, rather than cataloguing individual posts. They are not independently verified reporting and can be reused for up to two hours. Only post and reply text and the headline are sent to the AI providers, without account metadata. Removing a linked top-level source commit, rejecting it, or deleting its author's account clears the associated recap. Removing a reply or its author's account does not clear an existing recap: its paraphrased contribution can remain until the recap expires, at most two hours after generation. Signed-in members can open a headline to read its available recap above commits linked to that headline and posted within the past week, shown newest first. This list does not use semantic search and does not count toward the commit search limit. Expired recaps are omitted, and a recap is hidden if linked public threads include a post or reply from an author the viewer has muted or blocked. The results respect visibility, mutes and blocks too.
Hot topics can also receive a short AI-written recap based on approved, available top-level commits from the past twelve hours and public replies in their threads, including nested replies. Both the AI usefulness check and the recap writer receive the reply context: a question can support a recap when its answers provide useful substance. The sample includes one strongest root post per author, up to one hundred roots, plus up to one hundred of the newest replies across those threads. Recaps aim to explain the underlying situation or event in one or two short sentences, rather than cataloguing individual posts. They are not independently verified reporting and can be reused for up to two hours. Only post and reply text and the headline are sent to the AI providers, without account metadata. Removing a linked top-level source commit, rejecting it, or deleting its author's account clears the associated recap. Removing a reply or its author's account does not clear an existing recap: its paraphrased contribution can remain until the recap expires, at most two hours after generation. Signed-in members can open a headline to read its available recap above commits linked to that headline and posted within the past week, shown newest first. This list does not use semantic search and does not count toward the commit search limit. Expired recaps are omitted, and a recap is hidden if linked public threads include a post or reply from an author the viewer has muted or blocked. The results respect visibility, mutes and blocks too.

Sources: [composer](resources/js/components/commit-composer.tsx), [commit model](app/Models/Commit.php), [commit validation](app/Http/Requests/CreateCommitRequest.php), [text normalization](app/Services/CommitBodies.php), [submission throttle](app/Http/Middleware/ThrottleCommits.php).

Expand Down
2 changes: 1 addition & 1 deletion app/Queries/HeadlineSummaryCommitsQuery.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ public function get(Headline $headline, CarbonInterface $until): array
{
$sources = Commit::query()->topLevel()->approved()->whereAttachedTo($headline, 'headlines')
->where('body', '!=', '')
->whereBetween('created_at', [$until->copy()->subHours(8), $until])
->whereBetween('created_at', [$until->copy()->subHours(HotHeadlinesQuery::WINDOW_HOURS), $until])
->select('commits.id')->distinct('commits.author_id')
->orderBy('commits.author_id')->orderByDesc('hot_score')->orderByDesc('id');

Expand Down
14 changes: 11 additions & 3 deletions app/Queries/HotHeadlinesQuery.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@

final readonly class HotHeadlinesQuery
{
public const int WINDOW_HOURS = 12;

public function __construct(private TheAlgorithm $theAlgorithm)
{
//
Expand All @@ -20,8 +22,11 @@ public function __construct(private TheAlgorithm $theAlgorithm)
/**
* Each author contributes only their strongest recent commit per headline: one, plus its positive hot-score lead
* over a new, unengaged commit. That lead already measures engagement logarithmically and discounts age, so one
* viral thread does not contribute its raw engagement count. The eight-hour window uses posting time, never the
* viral thread does not contribute its raw engagement count. The twelve-hour window uses posting time, never the
* time a backfill linked the headline.
* A topic needs multiple root authors or one root with the age-discounted weight of two fresh outside repliers.
* Self-only activity adds no weight, but answering outside repliers receives the existing conversation bonus.
* Repeated roots cannot combine to meet the cutoff.
* Ties use author breadth and name, never post count or recency, so repeated weaker posts cannot lift a headline.
* Rankings are deterministic, without the Trending log's random draw or network adjustments.
*
Expand All @@ -31,13 +36,16 @@ public function get(?User $viewer = null): array
{
$now = now();
$baseline = $this->theAlgorithm->score($now, ThreadEngagement::none());
$minimumHotScore = $this->theAlgorithm->score($now, new ThreadEngagement(
stargazers: 0, quoters: 0, repliers: 2, repliersAnsweredByAuthor: 0,
));

$contributions = Commit::query()
->topLevel()
->approved()
->visibleTo($viewer)
->withoutMutedAuthorsFor($viewer)
->whereBetween('commits.created_at', [$now->copy()->subHours(8), $now])
->whereBetween('commits.created_at', [$now->copy()->subHours(self::WINDOW_HOURS), $now])
->toBase()
->join('commit_headline', 'commit_headline.commit_id', '=', 'commits.id')
->select('commit_headline.headline_id')
Expand All @@ -51,7 +59,7 @@ public function get(?User $viewer = null): array
->select('headlines.id', 'headlines.name')
->selectRaw('sum(contributions.commits_count)::bigint as commits_count, count(*) as authors_count, sum(1 + greatest(0::float8, contributions.hot_score - ?::float8)) as score', [$baseline])
->groupBy('headlines.id', 'headlines.name')
->havingRaw('sum(contributions.commits_count) > 1')
->havingRaw('count(*) > 1 or max(contributions.hot_score) >= ?::float8', [$minimumHotScore])
->orderByDesc('score')
->orderByDesc('authors_count')
->orderBy('headlines.name')
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,8 @@

it("only suggests an org's members within it", function (): void {
[$taylor, $jess] = followersOf($this->user, 2);
$taylor->profile->update(['handle' => 'taylor']);
$jess->profile->update(['handle' => 'jess']);
$taylor->profile->update(['handle' => 'taylor', 'name' => 'Taylor']);
$jess->profile->update(['handle' => 'jess', 'name' => 'Jess']);
User::factory()->create(['handle' => 'taylorotwell']);
$org = createOrg($this->user, [$taylor, $jess]);

Expand Down
4 changes: 2 additions & 2 deletions tests/Feature/Controllers/MentionSuggestionControllerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -100,8 +100,8 @@
it("only suggests an org's members within it, people the user follows first", function (): void {
$mark = User::factory()->create(['handle' => 'mark']);
[$taylor, $jess] = followersOf($mark, 2);
$taylor->profile->update(['handle' => 'taylor']);
$jess->profile->update(['handle' => 'jess']);
$taylor->profile->update(['handle' => 'taylor', 'name' => 'Taylor']);
$jess->profile->update(['handle' => 'jess', 'name' => 'Jess']);
User::factory()->create(['handle' => 'taylorotwell']);
$org = createOrg($mark, [$taylor, $jess]);
$mark->following()->attach($taylor);
Expand Down
9 changes: 6 additions & 3 deletions tests/Unit/Jobs/GenerateHeadlineSummaryTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
declare(strict_types=1);

use App\Actions\CreateHeadlineSummary;
use App\Actions\UpdateCommitHotScores;
use App\Ai\Agents\HeadlineSummaryWriter;
use App\Contracts\Services\HeadlineProvider;
use App\Enums\CommitStatus;
Expand All @@ -20,8 +21,8 @@
$headline = Headline::factory()->create(['name' => 'Vector databases']);
$strongest = Commit::factory()->hasAttached($headline)->create(['body' => 'Postgres worked well for our vector search.', 'hot_score' => 100]);
Commit::factory()->for($strongest->user)->hasAttached($headline)->create(['body' => 'Repeated weaker reaction', 'hot_score' => 1]);
Commit::factory()->hasAttached($headline)->create(['body' => 'We needed a separate index for larger datasets.', 'created_at' => now()->subHours(8)]);
Commit::factory()->hasAttached($headline)->create(['body' => 'Too old', 'created_at' => now()->subHours(8)->subSecond()]);
Commit::factory()->hasAttached($headline)->create(['body' => 'We needed a separate index for larger datasets.', 'created_at' => now()->subHours(12)]);
Commit::factory()->hasAttached($headline)->create(['body' => 'Too old', 'created_at' => now()->subHours(12)->subSecond()]);
Commit::factory()->hasAttached($headline)->create(['body' => 'In the future', 'created_at' => now()->addSecond()]);
Commit::factory()->pending()->hasAttached($headline)->create();
Commit::factory()->rejected()->hasAttached($headline)->create();
Expand Down Expand Up @@ -52,6 +53,7 @@
Commit::factory()->for($root, 'parent')->create(['body' => '']);
Commit::factory()->for($root, 'parent')->create(['created_at' => now()->addSecond()]);
Commit::factory()->for(Commit::factory(), 'parent')->create();
resolve(UpdateCommitHotScores::class)->handle([$root->id]);
$input = json_encode(['headline' => $headline->name, 'posts' => [json_encode([
'post' => 'Has anyone measured index memory?',
'replies' => ['Our index used 2 GB for one million vectors.', 'Reducing dimensions halved that in our test.'],
Expand Down Expand Up @@ -84,10 +86,11 @@
$headline = Headline::factory()->create();
$root = Commit::factory()->hasAttached($headline)->create(['body' => 'An index memory regression.']);
Commit::factory()->for($root->user)->hasAttached($headline)->create(['hot_score' => -1]);
$older = Commit::factory()->hasAttached($headline)->create(['created_at' => now()->subHours(9)]);
$older = Commit::factory()->hasAttached($headline)->create(['created_at' => now()->subHours(13)]);
Commit::factory()->for($older, 'parent')->create(['body' => 'A reply in an expired thread.']);
Commit::factory()->for($root, 'parent')->create(['body' => 'Outside the reply sample.', 'created_at' => now()->subMinute()]);
Commit::factory()->count(100)->for($root, 'parent')->create(['body' => 'Recent measurements.']);
resolve(UpdateCommitHotScores::class)->handle([$root->id]);
$input = json_encode(['post' => 'An index memory regression.', 'replies' => array_fill(0, 100, 'Recent measurements.')], JSON_THROW_ON_ERROR);
$this->mock(HeadlineProvider::class)->shouldReceive('summarize')->once()->with($headline->name, [$input])->andReturn('A grounded recap.');

Expand Down
Loading
Loading