Skip to content

Add nightly Moonbeam argument leaderboard extraction, API, and muzzle.lol review UI#258

Open
Copilot wants to merge 22 commits into
masterfrom
copilot/add-argument-leaderboard-table
Open

Add nightly Moonbeam argument leaderboard extraction, API, and muzzle.lol review UI#258
Copilot wants to merge 22 commits into
masterfrom
copilot/add-argument-leaderboard-table

Conversation

Copilot AI commented May 21, 2026

Copy link
Copy Markdown
  • Identify the single review comment to address and inspect the affected backend code
  • Run backend lint, build, and tests to establish the current baseline
  • Make the smallest change needed to limit or paginate argument history loading
  • Run targeted validation for the touched backend code
  • Run final review validation and report the completed fix
Original prompt

DB Layer:

  • Create a new table for argument leaderboards capturing:
    1. Argument summary and each participant's side/viewpoint (text/JSON field).
    2. Winner of the argument (foreign key to slack_user).
    3. Point value (integer, 0–5), representing the substance and depth of the argument.

Service Layer:

  • When a user asks "who won an argument" in Moonbeam:
    • Use ChatGPT to determine:
      • The argument context.
      • Each participant and their viewpoints.
      • The winner (as mapped to a slack_user DB record).
      • Calculate point valuation (0–5) based on argument substance (length, back-and-forth, message count, etc.).
  • Save extracted data (argument details, winner, points) in the new DB table.

UI Layer:

  • Add a page to the muzzle.lol website displaying:
    • The argument leaderboard (ranking users by argument wins/points).
    • Listed columns: argument, each participant's side, winner, point value.
    • An intuitive leaderboard at the top (argument wins by user).
    • Enable navigation to review detailed outcomes of previous arguments.
  • Ensure the UI is clear, readable, and user-friendly, making it easy to identify top debaters and review past debates.

This pull request was created from Copilot chat.

Copilot AI changed the title [WIP] Add new table for argument leaderboards Add Moonbeam argument leaderboard persistence, API, and muzzle.lol review UI May 21, 2026
Copilot AI requested a review from sfreeman422 May 21, 2026 15:01
@sfreeman422

Copy link
Copy Markdown
Collaborator

@copilot refactor this to instead behave in such a way that as part of it's nightly job, this service will analyze all channels for arguments between users and determine who won. it should be intolerant of false positives, IE very selective in the conversations it believes are arguments. We should not be weakly checking if a tagged message is asking who won etc. This should largely be handled by the AI

Copilot AI commented May 21, 2026

Copy link
Copy Markdown
Author

@copilot refactor this to instead behave in such a way that as part of it's nightly job, this service will analyze all channels for arguments between users and determine who won. it should be intolera...

Addressed in a9a2285. Argument extraction now runs in a selective nightly analysis job across channels, and the tagged-message winner check was removed from Moonbeam participation.

Copilot AI changed the title Add Moonbeam argument leaderboard persistence, API, and muzzle.lol review UI Add nightly Moonbeam argument leaderboard extraction, API, and muzzle.lol review UI May 21, 2026
Comment thread packages/backend/src/ai/ai.constants.ts Outdated
Comment thread packages/backend/src/ai/ai.constants.ts Outdated
- any conversation where the winner is ambiguous
- weak or low-substance disagreements that do not merit leaderboard tracking

If you find a qualifying argument, return ONLY a single JSON object in this exact shape:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot this should be an array.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 6d22437. The extractor prompt and parser now use a JSON array response shape ([] when nothing qualifies).

Comment thread packages/backend/src/shared/db/models/ArgumentLeaderboard.ts

Copy link
Copy Markdown
Collaborator

@copilot fix the failing checks come on COME ON

Copilot AI commented May 21, 2026

Copy link
Copy Markdown
Author

@copilot fix the failing checks come on COME ON

Fixed in f91b236. I traced the failing CI job to the backend coverage gate (79.8% branch coverage) and added focused dashboard controller tests to cover the new authenticated dashboard routes, including /dashboard/arguments.

@sfreeman422 sfreeman422 marked this pull request as ready for review May 21, 2026 18:25

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 26 out of 26 changed files in this pull request and generated 3 comments.

Comments suppressed due to low confidence (2)

packages/backend/src/argument/argument.persistence.service.ts:88

  • Participants are resolved without filtering out bot SlackUser records. That can allow bot accounts to be stored in participants / participantViewpoints and appear in the UI. Consider adding isBot: false to the participant lookup (and keeping the "at least 2 participants" check as "at least 2 human participants").
    const participantUsers = await slackUserRepo.find({
      where: participants.map((participant) => ({ slackId: participant.slackId, teamId: input.teamId })),
    });

packages/backend/src/argument/argument.persistence.service.ts:156

  • The leaderboard SQL does not filter out bot winners. Other leaderboards in the codebase exclude bots (e.g. u.isBot = 0 in DashboardPersistenceService), so adding the same filter here will prevent bot users from ranking on the argument leaderboard.
          `SELECT u.name AS name,
                  u.slackId AS slackId,
                  CAST(COUNT(*) AS SIGNED) AS wins,
                  CAST(COALESCE(SUM(a.pointValue), 0) AS SIGNED) AS points
            FROM argument_leaderboard a
            INNER JOIN slack_user u ON u.id = a.winnerId
            WHERE a.teamId = ?
            GROUP BY u.id, u.name, u.slackId
            ORDER BY wins DESC, points DESC, u.name ASC`,

Comment on lines +64 to +67
const slackUserRepo = getRepository(SlackUser);
const winner = await slackUserRepo.findOne({
where: { slackId: input.winnerSlackId, teamId: input.teamId },
});

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 40a5c51. Winners and participant lookups now require isBot: false, and the leaderboard aggregate query also excludes bot winners.

Comment on lines +69 to +74
const participantSlackIds = extractParticipantSlackIds(historyMessages, {
excludeSlackIds: [MOONBEAM_SLACK_ID],
});
if (participantSlackIds.length < 2) return;

await this.extractArgument(teamId, channelId, historyMessages);

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in ce9c4b4. The job now checks the extracted participant Slack IDs against known non-bot SlackUser records and skips the OpenAI call unless at least two human participants remain.

Comment thread README.md Outdated
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 26 out of 26 changed files in this pull request and generated 4 comments.

Comments suppressed due to low confidence (4)

packages/backend/src/argument/argument.persistence.service.ts:92

  • slackUserRepo.find({ where: participants.map(...) }) can be called with participants.length === 0, which (depending on TypeORM behavior) may result in an unfiltered query. Add an early return when fewer than 2 participants remain after normalization, and include isBot: false in the participant lookup to prevent bot records from being attached.
    const participants = Array.from(
      new Map(
        input.participants
          .map(normalizeParticipant)
          .filter((participant): participant is ArgumentParticipant => participant !== null)
          .map((participant) => [participant.slackId, participant]),
      ).values(),
    );

    const participantUsers = await slackUserRepo.find({
      where: participants.map((participant) => ({ slackId: participant.slackId, teamId: input.teamId, isBot: false })),
    });
    const extractedParticipantViewpoints = buildParticipantViewpoints(participants);
    const resolvedParticipants = buildArgumentParticipants(participantUsers, extractedParticipantViewpoints);
    const participantViewpoints = buildParticipantViewpoints(resolvedParticipants);

packages/backend/src/argument/argument.persistence.service.ts:99

  • After resolving participants, there is no validation that the resolved participant set still contains winnerSlackId. If the winner exists but is not among the resolved participants (e.g., participant resolution failed), you can persist an outcome where the winner wasn’t a participant. Consider skipping the save unless the winner is included in the resolved participants list.
    const extractedParticipantViewpoints = buildParticipantViewpoints(participants);
    const resolvedParticipants = buildArgumentParticipants(participantUsers, extractedParticipantViewpoints);
    const participantViewpoints = buildParticipantViewpoints(resolvedParticipants);

    if (resolvedParticipants.length < 2) {
      this.logger.warn('Skipping argument outcome save because fewer than two participants were extracted', {
        teamId: input.teamId,
        channelId: input.channelId,
      });
      return null;
    }

packages/backend/src/argument/argument.persistence.service.ts:156

  • The aggregate leaderboard SQL does not exclude bot winners (missing u.isBot = 0). If a bot is ever stored as a winner, it will be ranked here. Add the same bot filter used elsewhere in dashboard queries.
        repo.query<LeaderboardRow[]>(
          `SELECT u.name AS name,
                  u.slackId AS slackId,
                  CAST(COUNT(*) AS SIGNED) AS wins,
                  CAST(COALESCE(SUM(a.pointValue), 0) AS SIGNED) AS points
            FROM argument_leaderboard a
            INNER JOIN slack_user u ON u.id = a.winnerId
            WHERE a.teamId = ? AND u.isBot = 0
             GROUP BY u.id, u.name, u.slackId
            ORDER BY wins DESC, points DESC, u.name ASC`,

packages/backend/src/argument/argument.persistence.service.spec.ts:174

  • The getArgumentLeaderboard test only asserts the COUNT cast is present in the SQL. Once the query is updated to exclude bots, add an assertion that the SQL contains u.isBot = 0 so bot filtering can’t regress silently.
        winner: { id: 2, slackId: 'U2', name: 'Bob' },
        pointValue: '4',
        createdAt: '2026-05-21T00:00:00.000Z',
      },

Comment thread packages/backend/src/shared/db/models/ArgumentLeaderboard.ts Outdated
Comment on lines +64 to +67
const slackUserRepo = getRepository(SlackUser);
const winner = await slackUserRepo.findOne({
where: { slackId: input.winnerSlackId, teamId: input.teamId, isBot: false },
});
Comment on lines +159 to +163
repo.find({
where: { teamId },
relations: ['participants', 'winner'],
order: { createdAt: 'DESC' },
}),
Comment on lines +33 to +59
it('saves an argument outcome with a resolved winner', async () => {
findOne.mockResolvedValue({ id: 7, slackId: 'U2', name: 'Bob' });
findUsers.mockResolvedValue([
{ id: 6, slackId: 'U1', name: 'Alice' },
{ id: 7, slackId: 'U2', name: 'Bob' },
]);
save.mockImplementation(async (entry: { createdAt?: Date }) => ({
...entry,
id: 11,
createdAt: entry.createdAt ?? new Date('2026-05-21T00:00:00.000Z'),
}));

const result = await service.saveArgumentOutcome({
teamId: 'T1',
channelId: 'C1',
argumentSummary: 'Tabs versus spaces',
participants: [
{ slackId: 'U1', name: 'Alice', viewpoint: 'tabs are faster' },
{ slackId: 'U2', name: 'Bob', viewpoint: 'spaces are clearer' },
],
winnerSlackId: 'U2',
pointValue: 6,
});

expect(findOne).toHaveBeenCalledWith({ where: { slackId: 'U2', teamId: 'T1', isBot: false } });
expect(findUsers).toHaveBeenCalledWith({
where: [
sfreeman422 and others added 2 commits May 22, 2026 12:55
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 26 out of 26 changed files in this pull request and generated 2 comments.

Comment on lines +161 to +166
repo.find({
where: { teamId },
relations: ['participants', 'winner'],
order: { createdAt: 'DESC' },
take: ARGUMENT_HISTORY_LIMIT,
}),
Comment on lines +93 to +94
const participantViewpoints = buildParticipantViewpoints(resolvedParticipants);

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants