diff --git a/package.json b/package.json index 7e426a83d..3e015a7e0 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,7 @@ "probot-app" ], "scripts": { - "inspect": "node --inspect-brk ./dist/index.js", + "inspect": "node --inspect-brk ./dist/main.js", "build:ci": "ncc build src/adapters/github/github-actions.ts -o ./", "build:serverless": "ncc build src/index.ts -o ./", "build": "tsc", @@ -25,7 +25,7 @@ "start:serverless": "tsx src/adapters/github/github-actions.ts", "start:watch": "nodemon --exec 'yarn start'", "utils:cspell": "cspell --config .cspell.json 'src/**/*.{js,ts,json,md,yml}'", - "start": "probot run ./dist/index.js", + "start": "probot run ./dist/main.js", "prepare": "husky install", "test": "jest" }, diff --git a/src/adapters/supabase/helpers/tables/settlement.ts b/src/adapters/supabase/helpers/tables/settlement.ts index 98a30502b..a4cbee46a 100644 --- a/src/adapters/supabase/helpers/tables/settlement.ts +++ b/src/adapters/supabase/helpers/tables/settlement.ts @@ -3,7 +3,23 @@ import Decimal from "decimal.js"; import { Comment, Payload } from "../../../../types/payload"; import { Database } from "../../types/database"; import { Super } from "./super"; -import { PermitTransactionData } from "../../../../handlers/comment/handlers/issue/generate-permit-2-signature"; + +interface PermitTransactionData { + permit: { + permitted: { + token: string; + amount: string; + }; + nonce: string; + deadline: string; + }; + transferDetails: { + to: string; + requestedAmount: string; + }; + owner: string; + signature: string; +} type DebitInsert = Database["public"]["Tables"]["debits"]["Insert"]; type CreditInsert = Database["public"]["Tables"]["credits"]["Insert"]; diff --git a/src/handlers/assign/action.ts b/src/handlers/assign/action.ts index 8d138aa7a..b50901542 100644 --- a/src/handlers/assign/action.ts +++ b/src/handlers/assign/action.ts @@ -1,5 +1,5 @@ +import { getLinkedPullRequests } from "../../helpers/get-linked-issues-and-pull-requests"; import { closePullRequest } from "../../helpers/issue"; -import { getLinkedPullRequests } from "../../helpers/parser"; import { calculateDurations, calculateLabelValue } from "../../helpers/shared"; import { Context } from "../../types/context"; import { Label } from "../../types/label"; diff --git a/src/handlers/assign/auto.ts b/src/handlers/assign/auto.ts index 64c2ab1b7..78423e412 100644 --- a/src/handlers/assign/auto.ts +++ b/src/handlers/assign/auto.ts @@ -1,5 +1,5 @@ -import { getAllPullRequests, getPullByNumber, getIssueByNumber, addAssignees } from "../../helpers/issue"; -import { getLinkedIssues } from "../../helpers/parser"; +import { getLinkedIssues } from "../../helpers/get-linked-issues-and-pull-requests"; +import { addAssignees, getAllPullRequests, getIssueByNumber, getPullByNumber } from "../../helpers/issue"; import { Context } from "../../types/context"; // Check for pull requests linked to their respective issues but not assigned to them diff --git a/src/handlers/comment/handlers/issue/issue-closed.ts b/src/handlers/comment/handlers/issue-closed.ts similarity index 89% rename from src/handlers/comment/handlers/issue/issue-closed.ts rename to src/handlers/comment/handlers/issue-closed.ts index 8a4ddb061..5719d4329 100644 --- a/src/handlers/comment/handlers/issue/issue-closed.ts +++ b/src/handlers/comment/handlers/issue-closed.ts @@ -1,14 +1,12 @@ -import Runtime from "../../../../bindings/bot-runtime"; -import { checkUserPermissionForRepoAndOrg, getAllIssueComments } from "../../../../helpers/issue"; -import { Context } from "../../../../types/context"; -import { Comment, Issue, Payload, StateReason } from "../../../../types/payload"; -import structuredMetadata from "../../../shared/structured-metadata"; -import { getCollaboratorsForRepo } from "./get-collaborator-ids-for-repo"; -import { getPullRequestComments } from "./get-pull-request-comments"; +import Runtime from "../../../bindings/bot-runtime"; +import { checkUserPermissionForRepoAndOrg, getAllIssueComments } from "../../../helpers/issue"; +import { Context } from "../../../types/context"; +import { Comment, Issue, Payload, StateReason } from "../../../types/payload"; +import structuredMetadata from "../../shared/structured-metadata"; +import { getCollaboratorsForRepo } from "./issue/get-collaborator-ids-for-repo"; +import { getPullRequestComments } from "./issue/get-pull-request-comments"; export async function issueClosed(context: Context) { - // TODO: delegate permit calculation to GitHub Action - const payload = context.event.payload as Payload; const issue = payload.issue as Issue; @@ -18,6 +16,7 @@ export async function issueClosed(context: Context) { // === Calculate Permit === // const pullRequestComments = await getPullRequestComments(context, owner, repository, issueNumber); + const repoCollaborators = await getCollaboratorsForRepo(context); const workflow = "compute.yml"; const computeRepository = "ubiquibot-config"; diff --git a/src/handlers/comment/handlers/issue/all-comment-scoring.ts b/src/handlers/comment/handlers/issue/all-comment-scoring.ts deleted file mode 100644 index 802511006..000000000 --- a/src/handlers/comment/handlers/issue/all-comment-scoring.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { Comment } from "../../../../types/payload"; -import { commentScoringByContributionClass } from "./comment-scoring-by-contribution-style"; -import { CommentScoring } from "./comment-scoring-rubric"; -import { ContributorClassesKeys, ContributorView } from "./contribution-style-types"; -import { sortCommentsByClass } from "./filter-comments-by-contribution-type"; -import { sortUsersByClass } from "./identify-user-ids"; -import { perUserCommentScoring } from "./per-user-comment-scoring"; - -import { ContextIssue } from "./specification-scoring"; -export async function allCommentScoring({ - context, - issue, - comments, - view, -}: ContextIssue & { comments: Comment[]; view: ContributorView }): Promise { - const usersByClass = await sortUsersByClass(context, issue, comments); - const commentsByClass = sortCommentsByClass(usersByClass, comments, view); - const contributionClasses = Object.keys(usersByClass).map((key) => key as ContributorClassesKeys); - return contributionClasses - .filter((className: string) => className.endsWith("Comment")) - .flatMap((contributionStyle) => { - const commentsOfRole = commentsByClass[contributionStyle as keyof typeof commentsByClass]; - const scoring = commentScoringByContributionClass[contributionStyle](); - - const selection = usersByClass[contributionStyle as keyof typeof usersByClass]; - - if (!selection) { - context.logger.verbose(`No ${String(contributionStyle)} found`); - return []; - } - - // Ensure selection is always an array - const users = Array.isArray(selection) ? selection : [selection]; - - users.forEach((user) => { - if (!commentsOfRole) { - return []; - } - perUserCommentScoring( - context, - user, - commentsOfRole.filter((comment) => comment.user.id === user.id), - scoring - ); - }); - return scoring; - }); -} diff --git a/src/handlers/comment/handlers/issue/assignee-scoring.ts b/src/handlers/comment/handlers/issue/assignee-scoring.ts deleted file mode 100644 index 91d3e661c..000000000 --- a/src/handlers/comment/handlers/issue/assignee-scoring.ts +++ /dev/null @@ -1,74 +0,0 @@ -import Decimal from "decimal.js"; -import { Context } from "../../../../types/context"; -import { Issue, User } from "../../../../types/payload"; -import { ContributorView } from "./contribution-style-types"; -import { UserScoreDetails } from "./issue-shared-types"; - -export async function assigneeScoring( - context: Context, - { - issue, - source, - view, - }: { - issue: Issue; - source: User[]; - view: ContributorView; - } -): Promise { - // get the price label - const priceLabels = issue.labels.filter((label) => label.name.startsWith("Price: ")); - if (!priceLabels) throw context.logger.error("Price label is undefined"); - - // get the smallest price label - const priceLabel = priceLabels - .sort((a, b) => { - const priceA = parseFloat(a.name.replace("Price: ", "")); - const priceB = parseFloat(b.name.replace("Price: ", "")); - return priceA - priceB; - })[0] - .name.match(/\d+(\.\d+)?/) - ?.shift(); - - if (!priceLabel) { - throw context.logger.error("Price label is undefined"); - } - - // get the price - const price = new Decimal(priceLabel); - - // get the number of assignees - const numberOfAssignees = source.length; - - const assigneeRewards = source.map((assignee) => { - // get the assignee multiplier - const assigneeMultiplier = new Decimal(1); // TODO: get the assignee multiplier from the database - - // calculate the total - const splitReward = price.div(numberOfAssignees).times(assigneeMultiplier); - - // return the total - const details: UserScoreDetails = { - score: splitReward, - - view: view, - role: "Assignee", - contribution: "Task", - - scoring: { - issueComments: null, - reviewComments: null, - specification: null, - task: price, - }, - source: { - issue: issue, - user: assignee, - }, - }; - - return details; - }); - - return assigneeRewards; -} diff --git a/src/handlers/comment/handlers/issue/calculate-quality-score.test.ts b/src/handlers/comment/handlers/issue/calculate-quality-score.test.ts deleted file mode 100644 index c32e3a47b..000000000 --- a/src/handlers/comment/handlers/issue/calculate-quality-score.test.ts +++ /dev/null @@ -1,111 +0,0 @@ -import OpenAI from "openai"; -import { Context } from "../../../../types/context"; -import { Comment, Issue, User, UserType } from "../../../../types/payload"; -import { countTokensOfConversation, estimateOptimalModel, gptRelevance, relevanceScoring } from "./relevance-scoring"; - -const context = { - openAi: new OpenAI(), -} as unknown as Context; - -// Do not run real API calls inside of VSCode because it keeps running the tests in the background -if (process.env.NODE_ENV !== "test") { - describe("*** Real OpenAI API Call *** relevanceScoring", () => { - it("should calculate quality score", async () => { - const issue = { body: "my topic is about apples" } as Issue; - const comments: Comment[] = [ - { body: "the apple is red", user: { type: UserType.User } as User } as Comment, - { body: "it is juicy", user: { type: UserType.User } as User } as Comment, - { body: "bananas are great", user: { type: UserType.User } as User } as Comment, - ]; - const result = await relevanceScoring(context, issue, comments); - expect(result).toBeDefined(); - expect(result.score).toBeDefined(); - expect(Array.isArray(result.score)).toBe(true); - expect(typeof result.tokens).toBe("number"); - expect(typeof result.model).toBe("string"); - }); - }); - - describe("*** Real OpenAI API Call *** gptRelevance", () => { - it("should calculate gpt relevance", async () => { - const result = await gptRelevance(context, "gpt-3.5-turbo", "my topic is about apples", [ - "the apple is red", - "it is juicy", - "bananas are great", - ]); - expect(result[0]).toBeGreaterThan(0); - expect(result[1]).toBeGreaterThan(0); - expect(result[result.length - 1]).toBe(0); - }); - }); -} - -describe("countTokensOfConversation", () => { - it("should count tokens of conversation", () => { - const issue = { body: "my topic is about apples" } as Issue; - const comments: Comment[] = [ - { body: "the apple is red", user: { type: UserType.User } as User } as Comment, - { body: "it is juicy", user: { type: UserType.User } as User } as Comment, - { body: "bananas are great", user: { type: UserType.User } as User } as Comment, - ]; - const result = countTokensOfConversation(issue, comments); - expect(result).toBeGreaterThan(0); - }); -}); - -describe("estimateOptimalModel", () => { - it("should estimate optimal model", () => { - const result = estimateOptimalModel(5000); - expect(result).toBe("gpt-3.5-turbo-16k"); - }); -}); - -jest.mock("openai", () => - // mock OPEN AI API - // the purpose of this is to test without real API calls in order to isolate issues - jest.fn().mockImplementation(() => ({ - chat: { - completions: { - create: jest.fn().mockResolvedValue({ - choices: [ - { - message: { - content: "[1, 1, 0]", - }, - }, - ], - }), - }, - }, - })) -); - -describe("relevanceScoring", () => { - it("should calculate quality score", async () => { - const issue = { body: "issue body" } as Issue; - const comment = { body: "comment body", user: { type: "User" } } as Comment; - const comments = [comment, comment, comment] as Comment[]; - const result = await relevanceScoring(context, issue, comments); - expect(result).toBeDefined(); - expect(result.score).toBeDefined(); - expect(Array.isArray(result.score)).toBe(true); - expect(typeof result.tokens).toBe("number"); - expect(typeof result.model).toBe("string"); - }); -}); - -// describe("countTokensOfConversation", () => { -// it("should count tokens of conversation", () => { -// const issue = { body: "issue body" } as Issue; -// const comments = [{ body: "comment body", user: { type: "User" } }] as Comment[]; -// const result = countTokensOfConversation(issue, comments); -// expect(result).toBeGreaterThan(0); -// }); -// }); - -describe("gptRelevance", () => { - it("should calculate gpt relevance", async () => { - const result = await gptRelevance(context, "gpt-3.5-turbo", "issue body", ["comment body"]); - expect(result).toEqual([1, 1, 0]); - }); -}); diff --git a/src/handlers/comment/handlers/issue/comment-scoring-by-contribution-style.ts b/src/handlers/comment/handlers/issue/comment-scoring-by-contribution-style.ts deleted file mode 100644 index 963bb3a52..000000000 --- a/src/handlers/comment/handlers/issue/comment-scoring-by-contribution-style.ts +++ /dev/null @@ -1,110 +0,0 @@ -import { CommentScoring } from "./comment-scoring-rubric"; -import { ContributorClasses } from "./contribution-style-types"; - -export const commentScoringByContributionClass = { - // TODO: make this configurable - - "Issue Assignee Task": () => - new CommentScoring({ - contributionClass: "Issue Assignee Task", - formattingMultiplier: 0, - wordValue: 0, - }), - - "Issue Issuer Comment": () => - new CommentScoring({ - contributionClass: "Issue Issuer Comment", - formattingMultiplier: 1, - wordValue: 0.2, - }), - "Issue Assignee Comment": () => - new CommentScoring({ - contributionClass: "Issue Assignee Comment", - formattingMultiplier: 0, - wordValue: 0, - }), - - "Issue Collaborator Comment": () => - new CommentScoring({ - contributionClass: "Issue Collaborator Comment", - formattingMultiplier: 1, - wordValue: 0.1, - }), - "Issue Contributor Comment": () => - new CommentScoring({ - contributionClass: "Issue Contributor Comment", - formattingMultiplier: 0.25, - wordValue: 0.1, - }), - "Review Issuer Comment": () => - new CommentScoring({ - contributionClass: "Review Issuer Comment", - formattingMultiplier: 2, - wordValue: 0.2, - }), - "Review Assignee Comment": () => - new CommentScoring({ - contributionClass: "Review Assignee Comment", - formattingMultiplier: 1, - wordValue: 0.1, - }), - "Review Collaborator Comment": () => - new CommentScoring({ - contributionClass: "Review Collaborator Comment", - formattingMultiplier: 1, - wordValue: 0.1, - }), - "Review Contributor Comment": () => - new CommentScoring({ - contributionClass: "Review Contributor Comment", - formattingMultiplier: 0.25, - wordValue: 0.1, - }), - // end comments - // "Issue Issuer Specification": new CommentScoring({ - // contributionClass: "Issue Issuer Specification", - // formattingMultiplier: 3, - // wordValue: 0.2, - // }), - - // // // start reviews - // "Review Issuer Approval": new CommentScoring({ - // contributionClass: "Review Issuer Approval", - // formattingMultiplier: 1, - // wordValue: 2, - // }), - // "Review Issuer Rejection": new CommentScoring({ - // contributionClass: "Review Issuer Rejection", - // formattingMultiplier: 1, - // wordValue: 2, - // }), - // "Review Collaborator Approval": new CommentScoring({ - // contributionClass: "Review Collaborator Approval", - // formattingMultiplier: 1, - // wordValue: 1, - // }), - // "Review Collaborator Rejection": new CommentScoring({ - // contributionClass: "Review Collaborator Rejection", - // formattingMultiplier: 1, - // wordValue: 1, - // }), - // // // end reviews - // // // start code - // "Review Issuer Code": new CommentScoring({ - // contributionClass: "Review Issuer Code", - // formattingMultiplier: 1, - // wordValue: 1, - // }), - // "Review Assignee Code": new CommentScoring({ - // contributionClass: "Review Assignee Code", - // formattingMultiplier: 0, - // wordValue: 0, - // }), - // "Review Collaborator Code": new CommentScoring({ - // contributionClass: "Review Collaborator Code", - // formattingMultiplier: 1, - // wordValue: 1, - // }), -} as { - [key in keyof ContributorClasses]: () => CommentScoring; -}; diff --git a/src/handlers/comment/handlers/issue/comment-scoring-rubric.ts b/src/handlers/comment/handlers/issue/comment-scoring-rubric.ts deleted file mode 100644 index 2b6830f81..000000000 --- a/src/handlers/comment/handlers/issue/comment-scoring-rubric.ts +++ /dev/null @@ -1,281 +0,0 @@ -import { JSDOM } from "jsdom"; -// TODO: should be inherited from default config. This is a temporary solution. -import Decimal from "decimal.js"; -import _ from "lodash"; -import MarkdownIt from "markdown-it"; -import { Comment } from "../../../../types/payload"; -import { ContributorClassesKeys } from "./contribution-style-types"; -import { FormatScoreConfig, FormatScoreConfigParams } from "./element-score-config"; -import { Context } from "../../../../types/context"; - -type Tags = keyof HTMLElementTagNameMap; - -const md = new MarkdownIt(); -const ZERO = new Decimal(0); -const ONE = new Decimal(1); - -type CommentScoringConstructor = { - contributionClass: ContributorClassesKeys; - formattingMultiplier: number; - wordValue: number; -}; - -export class CommentScoring { - public contributionClass: ContributorClassesKeys; // This instance is used to calculate the score for this contribution `[view][role] "Comment"` class - // public viewWordScore: Decimal; // TODO: implement - // public viewWordScoreMultiplier!: number; // TODO: implement - public roleWordScore: Decimal; - public roleWordScoreMultiplier!: number; - public commentScores: { - [userId: string]: { - totalScoreTotal: Decimal; - wordScoreTotal: Decimal; - formatScoreTotal: Decimal; - details: { - [commentId: string]: { - totalScoreComment: Decimal; - - relevanceScoreComment: Decimal; // nullable because this is handled elsewhere in the program logic - // clarityScoreComment: null | Decimal; // TODO: implement - wordScoreComment: Decimal; - wordScoreCommentDetails: { [word: string]: Decimal }; - formatScoreComment: Decimal; - formatScoreCommentDetails: { - [tagName in Tags]?: { - count: number; - score: Decimal; - words: number; - }; - }; - comment: Comment; - }; - }; - }; - } = {}; - - private _formatConfig: { [tagName in Tags]?: FormatScoreConfigParams } = { - img: new FormatScoreConfig({ element: "img", disabled: true }), // disabled - blockquote: new FormatScoreConfig({ element: "blockquote", disabled: true }), // disabled - em: new FormatScoreConfig({ element: "em", disabled: true }), // disabled - strong: new FormatScoreConfig({ element: "strong", disabled: true }), // disabled - - h1: new FormatScoreConfig({ element: "h1", value: ONE }), - h2: new FormatScoreConfig({ element: "h2", value: ONE }), - h3: new FormatScoreConfig({ element: "h3", value: ONE }), - h4: new FormatScoreConfig({ element: "h4", value: ONE }), - h5: new FormatScoreConfig({ element: "h5", value: ONE }), - h6: new FormatScoreConfig({ element: "h6", value: ONE }), - a: new FormatScoreConfig({ element: "a", value: ONE }), - // ul: new ElementScoreConfig({ element: "ul", value: ONE }), - li: new FormatScoreConfig({ element: "li", value: ONE }), - // p: new ElementScoreConfig({ element: "p", value: ZERO }), - code: new FormatScoreConfig({ element: "code", value: ONE }), - // table: new ElementScoreConfig({ element: "table", value: ONE }), - td: new FormatScoreConfig({ element: "td", value: ONE }), - // tr: new ElementScoreConfig({ element: "tr", value: ONE }), - br: new FormatScoreConfig({ element: "br", value: ONE }), - hr: new FormatScoreConfig({ element: "hr", value: ONE }), - // del: new ElementScoreConfig({ element: "del", value: ONE }), - // pre: new ElementScoreConfig({ element: "pre", value: ONE }), - // ol: new ElementScoreConfig({ element: "ol", value: ONE }), - }; - - private _renderCache: { [commentId: number]: string } = {}; - - constructor({ contributionClass, formattingMultiplier = 1, wordValue = 0 }: CommentScoringConstructor) { - this.contributionClass = contributionClass; - this._applyRoleMultiplier(formattingMultiplier); - this.roleWordScore = new Decimal(wordValue); - } - - private _getRenderedCommentBody(comment: Comment): string { - if (!this._renderCache[comment.id]) { - this._renderCache[comment.id] = md.render(comment.body); - } - return this._renderCache[comment.id]; - } - - public compileTotalUserScores(): void { - for (const userId in this.commentScores) { - const userCommentScore = this.commentScores[userId]; - const wordScores = []; - const formatScores = []; - for (const commentId in userCommentScore.details) { - const commentScoreDetails = userCommentScore.details[commentId]; - const formatScoreComment = commentScoreDetails.formatScoreComment; - const wordScoreComment = commentScoreDetails.wordScoreComment; - - commentScoreDetails.totalScoreComment = formatScoreComment.plus(wordScoreComment); - - wordScores.push(wordScoreComment); - formatScores.push(formatScoreComment); - } - userCommentScore.wordScoreTotal = wordScores.reduce((total, score) => total.plus(score), ZERO); - userCommentScore.formatScoreTotal = formatScores.reduce((total, score) => total.plus(score), ZERO); - userCommentScore.totalScoreTotal = userCommentScore.wordScoreTotal.plus(userCommentScore.formatScoreTotal); - } - } - - public getTotalScorePerId(userId: number): Decimal { - const score = this.commentScores[userId].totalScoreTotal; - if (!score) { - throw new Error(`No score for id ${userId}`); - } - return score; - } - - private _getWordsNotInDisabledElements(comment: Comment): string[] { - const htmlString = this._getRenderedCommentBody(comment); - const dom = new JSDOM(htmlString); - const doc = dom.window.document; - const disabledElements = Object.entries(this._formatConfig) - .filter(([, config]) => config.disabled) - .map(([elementName]) => elementName); - - disabledElements.forEach((elementName) => { - const elements = doc.getElementsByTagName(elementName); - for (let i = 0; i < elements.length; i++) { - this._removeTextContent(elements[i]); // Recursively remove text content - } - }); - - // Provide a default value when textContent is null - return (doc.body.textContent || "").match(/\w+/g) || []; - } - - private _removeTextContent(element: Element): void { - if (element.hasChildNodes()) { - for (const child of Array.from(element.childNodes)) { - this._removeTextContent(child as Element); - } - } - element.textContent = ""; // Remove the text content of the element - } - - private _calculateWordScores( - words: string[] - ): (typeof CommentScoring.prototype.commentScores)[number]["details"][number]["wordScoreCommentDetails"] { - const wordScoreCommentDetails: { [key: string]: Decimal } = {}; - - for (const word of words) { - let counter = wordScoreCommentDetails[word] || ZERO; - counter = counter.plus(this.roleWordScore); - wordScoreCommentDetails[word] = counter; - } - - return wordScoreCommentDetails; - } - - private _calculateWordScoresTotals( - wordScoreCommentDetails: (typeof CommentScoring.prototype.commentScores)[number]["details"][number]["wordScoreCommentDetails"] - ): Decimal { - let totalScore = ZERO; - for (const score of Object.values(wordScoreCommentDetails)) { - totalScore = totalScore.plus(score); - } - return totalScore; - } - - private _countWordsInTag(html: string, tag: string): number { - const regex = new RegExp(`<${tag}[^>]*>(.*?)`, "g"); - let match; - let wordCount = 0; - while ((match = regex.exec(html)) !== null) { - const content = match[1]; - const words = content.match(/\w+/g) || []; - wordCount += words.length; - } - return wordCount; - } - - public computeElementScore(context: Context, comment: Comment, userId: number) { - const htmlString = this._getRenderedCommentBody(comment); - const formatStatistics = _.mapValues(_.cloneDeep(this._formatConfig), () => ({ - count: 0, - score: ZERO, - words: 0, - })); - - let totalElementScore = ZERO; - - for (const _elementName in formatStatistics) { - const elementName = _elementName as Tags; - const tag = formatStatistics[elementName]; - if (!tag) continue; - - tag.count = this._countTags(htmlString, elementName); - const value = this._formatConfig[elementName]?.value; - if (value) tag.score = value.times(tag.count); - - tag.words = this._countWordsInTag(htmlString, elementName); - if (tag.count !== 0 || !tag.score.isZero()) { - totalElementScore = totalElementScore.plus(tag.score); - } else { - delete formatStatistics[elementName]; // Delete the element if count and score are both zero - } - } - - this._initialize(context, comment, userId); - // Store the element score for the comment - this.commentScores[userId].details[comment.id].formatScoreComment = totalElementScore; - this.commentScores[userId].details[comment.id].formatScoreCommentDetails = formatStatistics; - - return htmlString; - } - - private _initialize(context: Context, comment: Comment, userId: number) { - if (!this.commentScores[userId]) { - context.logger.debug("good thing we initialized, was unsure if necessary"); - const initialCommentScoreValue = { - totalScoreTotal: ZERO, - wordScoreTotal: ZERO, - formatScoreTotal: ZERO, - details: {}, - }; - this.commentScores[userId] = { ...initialCommentScoreValue }; - } - if (!this.commentScores[userId].details[comment.id]) { - context.logger.debug("good thing we initialized, was unsure if necessary"); - this.commentScores[userId].details[comment.id] = { - totalScoreComment: ZERO, - relevanceScoreComment: ZERO, - wordScoreComment: ZERO, - formatScoreComment: ZERO, - formatScoreCommentDetails: {}, - wordScoreCommentDetails: {}, - comment, - }; - } - } - - public computeWordScore(context: Context, comment: Comment, userId: number) { - const words = this._getWordsNotInDisabledElements(comment); - const wordScoreDetails = this._calculateWordScores(words); - - this._initialize(context, comment, userId); - this.commentScores[userId].details[comment.id].comment = comment; - this.commentScores[userId].details[comment.id].wordScoreComment = this._calculateWordScoresTotals(wordScoreDetails); - this.commentScores[userId].details[comment.id].wordScoreCommentDetails = wordScoreDetails; - - return wordScoreDetails; - } - private _applyRoleMultiplier(multiplier: number) { - for (const tag in this._formatConfig) { - const selection = this._formatConfig[tag as Tags]; - const value = selection?.value; - if (value) { - selection.value = value.times(multiplier); - } - } - this.roleWordScoreMultiplier = multiplier; - } - - private _countTags(html: string, tag: Tags) { - if (this._formatConfig[tag]?.disabled) { - return 0; - } - - const regex = new RegExp(`<${tag}[^>]*>`, "g"); - return (html.match(regex) || []).length; - } -} diff --git a/src/handlers/comment/handlers/issue/contribution-style-types.ts b/src/handlers/comment/handlers/issue/contribution-style-types.ts deleted file mode 100644 index 18271e6f9..000000000 --- a/src/handlers/comment/handlers/issue/contribution-style-types.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { User } from "../../../../types/payload"; - -type All = User[] | User | null; - -type Assignee = All; -type Issuer = User; -type Collaborator = All; -type Contributor = All; - -// [VIEW] [ROLE] [CONTRIBUTION] - -export type ContributorClasses = { - // start comments - "Issue Issuer Comment": Issuer; - "Issue Assignee Comment": Assignee; - "Issue Collaborator Comment": Collaborator; - "Issue Contributor Comment": Contributor; - // end comments - - // start specification - "Issue Issuer Specification": Issuer; - // end specification - - // start code - "Issue Assignee Task": Assignee; - // end code - - // start comments - "Review Issuer Comment": Issuer; - "Review Assignee Comment": Assignee; - "Review Collaborator Comment": Collaborator; - "Review Contributor Comment": Contributor; - // end comments - - // start reviews - "Review Issuer Approval": Issuer; - "Review Issuer Rejection": Issuer; - "Review Collaborator Approval": Collaborator; - "Review Collaborator Rejection": Collaborator; - // end reviews - - // start code - "Review Issuer Code": Issuer; - "Review Assignee Code": Assignee; - "Review Collaborator Code": Collaborator; - // end code -}; -export type ContributorClassesKeys = keyof ContributorClasses; -export type ContributorView = "Issue" | "Review"; -export type ContributorRole = "Issuer" | "Assignee" | "Collaborator" | "Contributor"; -export type ContributorContribution = "Comment" | "Approval" | "Rejection" | "Code" | "Specification" | "Task"; diff --git a/src/handlers/comment/handlers/issue/element-score-config.ts b/src/handlers/comment/handlers/issue/element-score-config.ts deleted file mode 100644 index 93dbbbf57..000000000 --- a/src/handlers/comment/handlers/issue/element-score-config.ts +++ /dev/null @@ -1,25 +0,0 @@ -import Decimal from "decimal.js"; -import { validHTMLElements } from "./valid-html-elements"; -export interface FormatScoreConfigParams { - element: string; - value?: null | Decimal; - disabled?: boolean; -} -export class FormatScoreConfig { - public element: keyof HTMLElementTagNameMap; - public value: null | Decimal = null; - public disabled: boolean; - - constructor({ element, value = null, disabled = false }: FormatScoreConfigParams) { - if (!this._isHTMLElement(element)) { - throw new Error(`Invalid HTML element: ${element}`); - } - this.element = element; - this.disabled = disabled; - this.value = value; - } - - private _isHTMLElement(element: string): element is keyof HTMLElementTagNameMap { - return validHTMLElements.includes(element as keyof HTMLElementTagNameMap); - } -} diff --git a/src/handlers/comment/handlers/issue/evaluate-comments.ts b/src/handlers/comment/handlers/issue/evaluate-comments.ts deleted file mode 100644 index c6ff78a8b..000000000 --- a/src/handlers/comment/handlers/issue/evaluate-comments.ts +++ /dev/null @@ -1,72 +0,0 @@ -import Decimal from "decimal.js"; - -import { Context } from "../../../../types/context"; -import { Comment, Issue, User } from "../../../../types/payload"; -import { allCommentScoring } from "./all-comment-scoring"; -import { CommentScoring } from "./comment-scoring-rubric"; -import { ContributorView } from "./contribution-style-types"; -import { UserScoreDetails } from "./issue-shared-types"; -import { addRelevanceAndFormatScoring } from "./relevance-format-scoring"; -import { relevanceScoring } from "./relevance-scoring"; - -export async function commentsScoring({ - context, - issue, - source, - view, -}: { - context: Context; - issue: Issue; - source: Comment[]; - view: ContributorView; -}): Promise { - const relevance = await relevanceScoring(context, issue, source); - const relevanceWithMetaData = relevance.score.map(enrichRelevanceData(source)); - - const formatting: CommentScoring[] = await allCommentScoring({ context, issue, comments: source, view }); - const formattingWithRelevance: CommentScoring[] = addRelevanceAndFormatScoring(relevanceWithMetaData, formatting); - - const userScoreDetails = formattingWithRelevance.reduce((acc, commentScoring) => { - for (const userId in commentScoring.commentScores) { - const userScore = commentScoring.commentScores[userId]; - - const userScoreDetail: UserScoreDetails = { - score: userScore.totalScoreTotal, - view, - role: null, - contribution: "Comment", - scoring: { - issueComments: view === "Issue" ? commentScoring : null, - reviewComments: view === "Review" ? commentScoring : null, - specification: null, - task: null, - }, - source: { - issue, - user: Object.values(userScore.details)[0].comment.user, - }, - }; - - acc.push(userScoreDetail); - } - return acc; - }, [] as UserScoreDetails[]); - - return userScoreDetails; -} - -export interface EnrichedRelevance { - comment: Comment; - user: User; - score: Decimal; -} - -function enrichRelevanceData( - contributorComments: Comment[] -): (value: Decimal, index: number, array: Decimal[]) => EnrichedRelevance { - return (score, index) => ({ - comment: contributorComments[index], - user: contributorComments[index].user, - score, - }); -} diff --git a/src/handlers/comment/handlers/issue/filter-comments-by-contribution-type.ts b/src/handlers/comment/handlers/issue/filter-comments-by-contribution-type.ts deleted file mode 100644 index cbb632752..000000000 --- a/src/handlers/comment/handlers/issue/filter-comments-by-contribution-type.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { Comment, User } from "../../../../types/payload"; -import { ContributorClasses, ContributorClassesKeys, ContributorView } from "./contribution-style-types"; -type CommentsSortedByClass = { - [className in keyof ContributorClasses]: null | Comment[]; -}; - -export function sortCommentsByClass( - usersByClass: ContributorClasses, - contributorComments: Comment[], - view: ContributorView -): CommentsSortedByClass { - const result = {} as CommentsSortedByClass; - - for (const role of Object.keys(usersByClass)) { - if (role.startsWith(view)) { - const key = role as ContributorClassesKeys; - if (key in usersByClass) { - result[key] = filterComments(key, usersByClass, contributorComments); - } - } - } - - return result; -} - -function filterComments( - role: ContributorClassesKeys, - usersOfCommentsByRole: ContributorClasses, - contributorComments: Comment[] -): Comment[] | null { - const users = usersOfCommentsByRole[role]; - if (!users) return null; - if (Array.isArray(users)) { - return contributorComments.filter((comment: Comment) => users.some((user: User) => user.id == comment.user.id)); - } else { - return contributorComments.filter((comment: Comment) => comment.user.id === users.id); - } -} diff --git a/src/handlers/comment/handlers/issue/generate-permit-2-signature.ts b/src/handlers/comment/handlers/issue/generate-permit-2-signature.ts deleted file mode 100644 index 711eeb605..000000000 --- a/src/handlers/comment/handlers/issue/generate-permit-2-signature.ts +++ /dev/null @@ -1,119 +0,0 @@ -import { MaxUint256, PERMIT2_ADDRESS, PermitTransferFrom, SignatureTransfer } from "@uniswap/permit2-sdk"; -import Decimal from "decimal.js"; -import { BigNumber, ethers } from "ethers"; -import { keccak256, toUtf8Bytes } from "ethers/lib/utils"; -import { getPayoutConfigByNetworkId } from "../../../../helpers/payout"; - -import { Context } from "../../../../types/context"; -import { decryptKeys } from "../../../../utils/private"; - -export async function generatePermit2Signature( - context: Context, - { beneficiary, amount, userId }: GeneratePermit2SignatureParams -) { - const logger = context.logger; - const { - payments: { evmNetworkId }, - keys: { evmPrivateEncrypted }, - } = context.config; - - if (!evmPrivateEncrypted) throw logger.error("No bot wallet private key defined"); - const { rpc, paymentToken } = getPayoutConfigByNetworkId(evmNetworkId); - const { privateKey } = await decryptKeys(evmPrivateEncrypted); - - if (!rpc) throw logger.fatal("RPC is not defined"); - if (!privateKey) throw logger.fatal("Private key is not defined"); - if (!paymentToken) throw logger.fatal("Payment token is not defined"); - - let provider; - let adminWallet; - try { - provider = new ethers.providers.JsonRpcProvider(rpc); - } catch (error) { - throw logger.debug("Failed to instantiate provider", error); - } - - try { - adminWallet = new ethers.Wallet(privateKey, provider); - } catch (error) { - throw logger.debug("Failed to instantiate wallet", error); - } - - const permitTransferFromData: PermitTransferFrom = { - permitted: { - token: paymentToken, - amount: ethers.utils.parseUnits(amount.toString(), 18), - }, - spender: beneficiary, - nonce: BigNumber.from(keccak256(toUtf8Bytes(userId))), - deadline: MaxUint256, - }; - - const { domain, types, values } = SignatureTransfer.getPermitData( - permitTransferFromData, - PERMIT2_ADDRESS, - evmNetworkId - ); - - const signature = await adminWallet._signTypedData(domain, types, values).catch((error) => { - throw logger.debug("Failed to sign typed data", error); - }); - - const transactionData: PermitTransactionData = { - permit: { - permitted: { - token: permitTransferFromData.permitted.token, - amount: permitTransferFromData.permitted.amount.toString(), - }, - nonce: permitTransferFromData.nonce.toString(), - deadline: permitTransferFromData.deadline.toString(), - }, - transferDetails: { - to: permitTransferFromData.spender, - requestedAmount: permitTransferFromData.permitted.amount.toString(), - }, - owner: adminWallet.address, - signature: signature, - }; - - // const transactionDataV2 = { - // token: permitTransferFromData.permitted.token, - // nonce: permitTransferFromData.nonce.toString(), - // deadline: permitTransferFromData.deadline.toString(), - // beneficiary: permitTransferFromData.spender, - // amount: permitTransferFromData.permitted.amount.toString(), - // }; - - const base64encodedTxData = Buffer.from(JSON.stringify(transactionData)).toString("base64"); - - const url = new URL("https://pay.ubq.fi/"); - url.searchParams.append("claim", base64encodedTxData); - url.searchParams.append("network", evmNetworkId.toString()); - - logger.info("Generated permit2 signature", { transactionData, url: url.toString() }); - - return { transactionData, url }; -} -interface GeneratePermit2SignatureParams { - beneficiary: string; - amount: Decimal; - - userId: string; -} - -export interface PermitTransactionData { - permit: { - permitted: { - token: string; - amount: string; - }; - nonce: string; - deadline: string; - }; - transferDetails: { - to: string; - requestedAmount: string; - }; - owner: string; - signature: string; -} diff --git a/src/handlers/comment/handlers/issue/generate-permits.ts b/src/handlers/comment/handlers/issue/generate-permits.ts deleted file mode 100644 index e4ddb16ac..000000000 --- a/src/handlers/comment/handlers/issue/generate-permits.ts +++ /dev/null @@ -1,249 +0,0 @@ -import Decimal from "decimal.js"; -import { stringify } from "yaml"; - -import Runtime from "../../../../bindings/bot-runtime"; -import { getTokenSymbol } from "../../../../helpers/contracts"; -import { getPayoutConfigByNetworkId } from "../../../../helpers/payout"; -import { Context } from "../../../../types/context"; -import { Issue, Payload } from "../../../../types/payload"; - -import structuredMetadata from "../../../shared/structured-metadata"; -import { generatePermit2Signature } from "./generate-permit-2-signature"; -import { UserScoreTotals } from "./issue-shared-types"; - -type TotalsById = { [userId: string]: UserScoreTotals }; - -export async function generatePermits(context: Context, totals: TotalsById) { - const { html: comment, permits } = await generateComment(context, totals); - const metadata = structuredMetadata.create("Permits", { permits, totals }); - return comment.concat("\n", metadata); -} - -async function generateComment(context: Context, totals: TotalsById) { - const runtime = Runtime.getState(); - const { - keys: { evmPrivateEncrypted }, - } = context.config; - const payload = context.event.payload as Payload; - const issue = payload.issue as Issue; - const { rpc, paymentToken } = getPayoutConfigByNetworkId(context.config.payments.evmNetworkId); - - const tokenSymbol = await getTokenSymbol(paymentToken, rpc); - const HTML = [] as string[]; - - const permits = []; - - for (const userId in totals) { - const userTotals = totals[userId]; - const contributionsOverviewTable = generateContributionsOverview({ [userId]: userTotals }, issue); - const conversationIncentivesTable = generateDetailsTable({ [userId]: userTotals }); - - const tokenAmount = userTotals.total; - - const contributorName = userTotals.user.login; - // const contributionClassName = userTotals.details[0].contribution as ContributorClassNames; - - if (!evmPrivateEncrypted) throw context.logger.error("No bot wallet private key defined"); - - const beneficiaryAddress = await runtime.adapters.supabase.wallet.getAddress(parseInt(userId)); - - const permit = await generatePermit2Signature(context, { - beneficiary: beneficiaryAddress, - amount: tokenAmount, - userId: userId, - }); - - permits.push(permit); - - const html = generateHtml({ - permit: permit.url, - tokenAmount, - tokenSymbol, - contributorName, - contributionsOverviewTable, - detailsTable: conversationIncentivesTable, - }); - HTML.push(html); - } - return { html: HTML.join("\n"), permits }; -} -function generateHtml({ - permit, - tokenAmount, - tokenSymbol, - contributorName, - contributionsOverviewTable, - detailsTable, -}: GenerateHtmlParams) { - return ` -
- -

- - [ ${tokenAmount} ${tokenSymbol} ] -

-
@${contributorName}
-
- ${contributionsOverviewTable} - ${detailsTable} -
- `; -} - -function generateContributionsOverview(userScoreDetails: TotalsById, issue: Issue) { - const buffer = [ - "
Contributions Overview
", - "", - "", - "", - ]; - - function newRow(view: string, contribution: string, count: string, reward: string) { - return ``; - } - - for (const entries of Object.entries(userScoreDetails)) { - const userId = Number(entries[0]); - const userScore = entries[1]; - for (const detail of userScore.details) { - const { specification, issueComments, reviewComments, task } = detail.scoring; - - if (specification) { - buffer.push( - newRow( - "Issue", - "Specification", - Object.keys(specification.commentScores[userId].details).length.toString() || "-", - specification.commentScores[userId].totalScoreTotal.toString() || "-" - ) - ); - } - if (issueComments) { - buffer.push( - newRow( - "Issue", - "Comment", - Object.keys(issueComments.commentScores[userId].details).length.toString() || "-", - issueComments.commentScores[userId].totalScoreTotal.toString() || "-" - ) - ); - } - if (reviewComments) { - buffer.push( - newRow( - "Review", - "Comment", - Object.keys(reviewComments.commentScores[userId].details).length.toString() || "-", - reviewComments.commentScores[userId].totalScoreTotal.toString() || "-" - ) - ); - } - if (task) { - buffer.push( - newRow( - "Issue", - "Task", - issue.assignees.length === 0 ? "-" : `${(1 / issue.assignees.length).toFixed(2)}`, - task?.toString() || "-" - ) - ); - } - } - } - /** - * Example - * - * Contributions Overview - * | View | Contribution | Count | Reward | - * | --- | --- | --- | --- | - * | Issue | Specification | 1 | 1 | - * | Issue | Comment | 6 | 1 | - * | Review | Comment | 4 | 1 | - * | Review | Approval | 1 | 1 | - * | Review | Rejection | 3 | 1 | - */ - buffer.push("
ViewContributionCountReward
${view}${contribution}${count}${reward}
"); - return buffer.join("\n"); -} - -function generateDetailsTable(totals: TotalsById) { - let tableRows = ""; - - for (const user of Object.values(totals)) { - for (const detail of user.details) { - const userId = detail.source.user.id; - - const commentSources = []; - const specificationComments = detail.scoring.specification?.commentScores[userId].details; - const issueComments = detail.scoring.issueComments?.commentScores[userId].details; - const reviewComments = detail.scoring.reviewComments?.commentScores[userId].details; - if (specificationComments) commentSources.push(...Object.values(specificationComments)); - if (issueComments) commentSources.push(...Object.values(issueComments)); - if (reviewComments) commentSources.push(...Object.values(reviewComments)); - - const commentScores = []; - const specificationCommentScores = detail.scoring.specification?.commentScores[userId].details; - const issueCommentScores = detail.scoring.issueComments?.commentScores[userId].details; - const reviewCommentScores = detail.scoring.reviewComments?.commentScores[userId].details; - if (specificationCommentScores) commentScores.push(...Object.values(specificationCommentScores)); - if (issueCommentScores) commentScores.push(...Object.values(issueCommentScores)); - if (reviewCommentScores) commentScores.push(...Object.values(reviewCommentScores)); - - if (!commentSources) continue; - if (!commentScores) continue; - - for (const index in commentSources) { - // - const commentSource = commentSources[index]; - const commentScore = commentScores[index]; - - const commentUrl = commentSource.comment.html_url; - const truncatedBody = commentSource ? commentSource.comment.body.substring(0, 64).concat("...") : ""; - const formatScoreDetails = commentScore.formatScoreCommentDetails; - - let formatDetailsStr = ""; - if (formatScoreDetails && Object.keys(formatScoreDetails).length > 0) { - const ymlElementScores = stringify(formatScoreDetails); - formatDetailsStr = ["", `
${ymlElementScores}
`, ""].join("\n"); // weird rendering quirk with pre that needs breaks - } else { - formatDetailsStr = "-"; - } - - const formatScore = zeroToHyphen(commentScore.wordScoreComment.plus(commentScore.formatScoreComment)); - const relevanceScore = zeroToHyphen(commentScore.relevanceScoreComment); - const totalScore = zeroToHyphen(commentScore.totalScoreComment); - let formatScoreCell; - if (formatDetailsStr != "-") { - formatScoreCell = `
${formatScore}${formatDetailsStr}
`; - } else { - formatScoreCell = formatScore; - } - tableRows += `
${truncatedBody}
${formatScoreCell}${relevanceScore}${totalScore}`; - } - } - } - if (tableRows === "") return ""; - return `
Conversation Incentives
${tableRows}
CommentFormattingRelevanceReward
`; -} - -function zeroToHyphen(value: number | Decimal) { - if (value instanceof Decimal ? value.isZero() : value === 0) { - return "-"; - } else { - return value.toString(); - } -} - -interface GenerateHtmlParams { - permit: URL; - tokenAmount: Decimal; - tokenSymbol: string; - contributorName: string; - contributionsOverviewTable: string; - detailsTable: string; -} diff --git a/src/handlers/comment/handlers/issue/get-pull-request-comments.ts b/src/handlers/comment/handlers/issue/get-pull-request-comments.ts index 58354a011..724356c04 100644 --- a/src/handlers/comment/handlers/issue/get-pull-request-comments.ts +++ b/src/handlers/comment/handlers/issue/get-pull-request-comments.ts @@ -1,5 +1,5 @@ +import { getLinkedPullRequests } from "../../../../helpers/get-linked-issues-and-pull-requests"; import { getAllIssueComments } from "../../../../helpers/issue"; -import { getLinkedPullRequests } from "../../../../helpers/parser"; import { Context } from "../../../../types/context"; import { Comment } from "../../../../types/payload"; diff --git a/src/handlers/comment/handlers/issue/identify-user-ids.ts b/src/handlers/comment/handlers/issue/identify-user-ids.ts deleted file mode 100644 index 17c12271a..000000000 --- a/src/handlers/comment/handlers/issue/identify-user-ids.ts +++ /dev/null @@ -1,69 +0,0 @@ -import { Context } from "../../../../types/context"; -import { Comment, Issue, User } from "../../../../types/payload"; -import { ContributorClasses } from "./contribution-style-types"; -import { getCollaboratorsForRepo } from "./get-collaborator-ids-for-repo"; - -export async function sortUsersByClass( - context: Context, - issue: Issue, - contributorComments: Comment[] -): Promise { - const { issuer, assignees, collaborators, contributors } = await filterUsers(context, issue, contributorComments); - - return returnValues(issuer, assignees, collaborators, contributors); -} - -async function filterUsers(context: Context, issue: Issue, contributorComments: Comment[]) { - const issuer = issue.user; - const assignees = issue.assignees.filter((assignee): assignee is User => assignee !== null); - const collaborators = await getCollaboratorsForRepo(context); - - const allRoleUsers: User[] = [ - issuer, - ...assignees.filter((user): user is User => user !== null), - ...collaborators.filter((user): user is User => user !== null), - ]; - const humanUsersWhoCommented = contributorComments - .filter((comment) => comment.user.type === "User") - .map((comment) => comment.user); - - const contributors = humanUsersWhoCommented.filter( - (user: User) => !allRoleUsers.some((roleUser) => roleUser?.id === user.id) - ); - const uniqueContributors = Array.from(new Map(contributors.map((user) => [user.id, user])).values()); - return { - issuer, - assignees, - collaborators: collaborators.filter((collaborator) => collaborator.id !== issuer.id), - contributors: uniqueContributors, - }; -} - -function returnValues( - issuer: User, - assignees: User[], - collaborators: User[], - contributors: User[] -): ContributorClasses { - return { - "Issue Issuer Comment": issuer, - "Issue Assignee Comment": assignees, - "Issue Collaborator Comment": collaborators, - "Issue Contributor Comment": contributors, - - "Issue Issuer Specification": issuer, - "Issue Assignee Task": assignees, - - "Review Issuer Comment": issuer, - "Review Assignee Comment": assignees, - "Review Collaborator Comment": collaborators, - "Review Contributor Comment": contributors, - "Review Issuer Approval": issuer, - "Review Issuer Rejection": issuer, - "Review Collaborator Approval": collaborators, - "Review Collaborator Rejection": collaborators, - "Review Issuer Code": issuer, - "Review Assignee Code": assignees, - "Review Collaborator Code": collaborators, - }; -} diff --git a/src/handlers/comment/handlers/issue/issue-shared-types.ts b/src/handlers/comment/handlers/issue/issue-shared-types.ts deleted file mode 100644 index dbd55ac7c..000000000 --- a/src/handlers/comment/handlers/issue/issue-shared-types.ts +++ /dev/null @@ -1,39 +0,0 @@ -import Decimal from "decimal.js"; -import { Issue, User } from "../../../../types/payload"; -import { CommentScoring } from "./comment-scoring-rubric"; -import { ContributorContribution, ContributorRole, ContributorView } from "./contribution-style-types"; - -export interface UserScoreTotals { - // class: ContributorClassNames; - - // view: ContributorView; - // role: ContributorRole; - // contribution: ContributorContribution; - - total: Decimal; - details: UserScoreDetails[]; - user: User; -} - -export interface UserScoreDetails { - score: Decimal; - - view: null | ContributorView; - role: null | ContributorRole; - contribution: null | ContributorContribution; - - scoring: { - issueComments: null | CommentScoring; - reviewComments: null | CommentScoring; - specification: null | CommentScoring; - task: null | Decimal; - // approvals: unknown; - // rejections: unknown; - // code: unknown; - }; - source: { - // comments: null | Comment[]; - issue: Issue; - user: User; - }; -} diff --git a/src/handlers/comment/handlers/issue/mockup/issuer-rewards.md b/src/handlers/comment/handlers/issue/mockup/issuer-rewards.md deleted file mode 100644 index 583b40e5b..000000000 --- a/src/handlers/comment/handlers/issue/mockup/issuer-rewards.md +++ /dev/null @@ -1,257 +0,0 @@ -
- -

- - [ 14.282 WXDAI ] -

-
Issuer @pavlovcik
-
- -
Contributions Overview
- - - - - - - -
ViewContributionCountReward
IssueSpecification1-
IssueComment6-
ReviewComment4-
ReviewApproval1-
ReviewRejection3-
- - - - - - -
Conversation Incentives
- - - -
CommentFormattingRelevanceReward
My issue specification
10110
apples are green...
0.60.710.426
bananas are yellow...
0.60.010.006
> this is my block quote which i should not get credit for. APPL...
-0.01-
# here is my header - -- list item one -- list item two -- list ...
7.8 -
h1:
-    count: 1
-    score: 1
-    words: 4
-  li:
-    count: 4
-    score: 4
-    words: 10
-  
-
0.564.368
# apples - -- apples are red -- apples are green -- apples are t...
6.6 -
h1:
-    count: 1
-    score: 1
-    words: 1
-  li:
-    count: 3
-    score: 3
-    words: 9
-  
-
0.634.158
# this is a comment about apples - -- here is a test apple 🍎 - -...
5.8 -
h1:
-    count: 1
-    score: 1
-    words: 6
-  li:
-    count: 2
-    score: 2
-    words: 8
-  
-
0.834.814
-
- - diff --git a/src/handlers/comment/handlers/issue/per-user-comment-scoring.ts b/src/handlers/comment/handlers/issue/per-user-comment-scoring.ts deleted file mode 100644 index cfeeee1e6..000000000 --- a/src/handlers/comment/handlers/issue/per-user-comment-scoring.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { Context } from "../../../../types/context"; -import { Comment, User } from "../../../../types/payload"; -import { CommentScoring } from "./comment-scoring-rubric"; - -export function perUserCommentScoring( - context: Context, - user: User, - comments: Comment[], - scoringRubric: CommentScoring -): CommentScoring { - for (const comment of comments) { - scoringRubric.computeWordScore(context, comment, user.id); - scoringRubric.computeElementScore(context, comment, user.id); - } - scoringRubric.compileTotalUserScores(); - return scoringRubric; -} diff --git a/src/handlers/comment/handlers/issue/relevance-format-scoring.ts b/src/handlers/comment/handlers/issue/relevance-format-scoring.ts deleted file mode 100644 index 97e5205ce..000000000 --- a/src/handlers/comment/handlers/issue/relevance-format-scoring.ts +++ /dev/null @@ -1,70 +0,0 @@ -import { CommentScoring } from "./comment-scoring-rubric"; -import { EnrichedRelevance } from "./evaluate-comments"; - -// this can be used in two contexts: -// 1. to score an array of comments -// 2. to score an issue specification - -export function addRelevanceAndFormatScoring( - relevanceScore: EnrichedRelevance[], - formatScore: CommentScoring[] -): CommentScoring[] { - // this only needs to associate the relevance score with the format score - - // const details = [] as UserScoreDetails[]; - - for (let i = 0; i < formatScore.length; i++) { - const userScore = formatScore[i]; - for (const userId in userScore.commentScores) { - const userCommentScores = userScore.commentScores[userId]; - for (const commentId in userCommentScores.details) { - const commentDetails = userCommentScores.details[commentId]; - const relevance = relevanceScore.find( - (r) => r.comment.id === parseInt(commentId) && r.user.id === parseInt(userId) - ); - if (relevance) { - commentDetails.relevanceScoreComment = relevance.score; - } - } - } - } - - return formatScore; -} - -/* -relevanceScore { - comment: Comment; - user: User; - score: Decimal; -} -*/ - -/* -formatScore { -contributionClass: ContributorClassNames; - roleWordScore: Decimal; - roleWordScoreMultiplier!: number; - commentScores: { - [userId: number]: { - totalScore: Decimal; - wordScoreTotal: Decimal; - formatScoreTotal: Decimal; - details: { - [commentId: number]: { - wordScoreComment: Decimal; - wordScoreCommentDetails: { [word: string]: Decimal }; - formatScoreComment: Decimal; - formatScoreCommentDetails: { - [tagName in Tags]?: { - count: number; - score: Decimal; - words: number; - }; - }; - }; - }; - }; - } -} -*/ diff --git a/src/handlers/comment/handlers/issue/relevance-scoring.ts b/src/handlers/comment/handlers/issue/relevance-scoring.ts deleted file mode 100644 index 8c0377c68..000000000 --- a/src/handlers/comment/handlers/issue/relevance-scoring.ts +++ /dev/null @@ -1,159 +0,0 @@ -import Decimal from "decimal.js"; -import { encodingForModel } from "js-tiktoken"; -import OpenAI from "openai"; -import { Context } from "../../../../types/context"; - -import { Comment, Issue } from "../../../../types/payload"; - -export async function relevanceScoring(context: Context, issue: Issue, contributorComments: Comment[]) { - const tokens = countTokensOfConversation(issue, contributorComments); - const estimatedOptimalModel = estimateOptimalModel(tokens); - const score = await sampleRelevanceScores(context, contributorComments, estimatedOptimalModel, issue); - return { score, tokens, model: estimatedOptimalModel }; -} - -export function estimateOptimalModel(sumOfTokens: number) { - // we used the gpt-3.5-turbo encoder to estimate the amount of tokens. - // this also doesn't include the overhead of the prompting etc so this is expected to be a slight underestimate - if (sumOfTokens <= 4097) { - return "gpt-3.5-turbo"; - } else if (sumOfTokens <= 16385) { - // TODO: maybe use gpt-3.5-turbo-16k encoder to recalculate tokens - return "gpt-3.5-turbo-16k"; - } else { - // TODO: maybe use gpt-4-32k encoder to recalculate tokens - console.warn("Backup plan for development purposes only, but using gpt-4-32k due to huge context size"); - return "gpt-4-32k"; - } -} - -export function countTokensOfConversation(issue: Issue, comments: Comment[]) { - const specificationComment = issue.body; - if (!specificationComment) { - throw new Error("Issue specification comment is missing"); - } - - const gpt3TurboEncoder = encodingForModel("gpt-3.5-turbo"); - const contributorCommentsWithTokens = comments.map((comment) => ({ - tokens: gpt3TurboEncoder.encode(comment.body), - comment, - })); - - const sumOfContributorTokens = contributorCommentsWithTokens.reduce((acc, { tokens }) => acc + tokens.length, 0); - const specificationTokens = gpt3TurboEncoder.encode(specificationComment); - const sumOfSpecificationTokens = specificationTokens.length; - const totalSumOfTokens = sumOfSpecificationTokens + sumOfContributorTokens; - - return totalSumOfTokens; -} - -export async function gptRelevance( - context: Context, - model: string, - issueSpecificationBody: string, - conversation: string[], - conversationLength = conversation.length -) { - const openAi = context.openAi; - if (!openAi) throw new Error("OpenAI adapter is not defined"); - const PROMPT = `I need to evaluate the relevance of GitHub contributors' comments to a specific issue specification. Specifically, I'm interested in how much each comment helps to further define the issue specification or contributes new information or research relevant to the issue. Please provide a float between 0 and 1 to represent the degree of relevance. A score of 1 indicates that the comment is entirely relevant and adds significant value to the issue, whereas a score of 0 indicates no relevance or added value. Each contributor's comment is on a new line.\n\nIssue Specification:\n\`\`\`\n${issueSpecificationBody}\n\`\`\`\n\nConversation:\n\`\`\`\n${conversation.join( - "\n" - )}\n\`\`\`\n\n\nTo what degree are each of the comments in the conversation relevant and valuable to further defining the issue specification? Please reply with an array of float numbers between 0 and 1, corresponding to each comment in the order they appear. Each float should represent the degree of relevance and added value of the comment to the issue. The total length of the array in your response should equal exactly ${conversationLength} elements.`; - const response: OpenAI.Chat.ChatCompletion = await openAi.chat.completions.create({ - model: model, - messages: [ - { - role: "system", - content: PROMPT, - }, - ], - temperature: 1, - max_tokens: 64, - top_p: 1, - frequency_penalty: 0, - presence_penalty: 0, - }); - - try { - const parsedResponse = JSON.parse(response.choices[0].message.content as "[1, 1, 0.5, 0]") as number[]; - return parsedResponse; - } catch (error) { - return []; - } -} - -async function sampleRelevanceScores( - context: Context, - contributorComments: Comment[], - estimatedOptimalModel: ReturnType, - issue: Issue -) { - const BATCH_SIZE = 10; - const BATCHES = 1; - const correctLength = contributorComments.length; - const batchSamples = [] as Decimal[][]; - - for (let attempt = 0; attempt < BATCHES; attempt++) { - const fetchedSamples = await fetchSamples(context, { - contributorComments, - estimatedOptimalModel, - issue, - maxConcurrency: BATCH_SIZE, - }); - const filteredSamples = filterSamples(context, fetchedSamples, correctLength); - const averagedSample = averageSamples(filteredSamples, 10); - batchSamples.push(averagedSample); - } - const average = averageSamples(batchSamples, 4); - - return average; -} - -async function fetchSamples( - context: Context, - { contributorComments, estimatedOptimalModel, issue, maxConcurrency }: InEachRequestParams -) { - const commentsSerialized = contributorComments.map((comment) => comment.body); - const batchPromises = []; - for (let i = 0; i < maxConcurrency; i++) { - const requestPromise = gptRelevance(context, estimatedOptimalModel, issue.body, commentsSerialized); - batchPromises.push(requestPromise); - } - const batchResults = await Promise.all(batchPromises); - return batchResults; -} - -interface InEachRequestParams { - contributorComments: Comment[]; - estimatedOptimalModel: ReturnType; - issue: Issue; - maxConcurrency: number; -} - -function filterSamples(context: Context, batchResults: number[][], correctLength: number) { - return batchResults.filter((result) => { - if (result.length != correctLength) { - context.logger.error("Correct length is not defined", { - batchResultsLength: batchResults.length, - result, - }); - return false; - } else { - return true; - } - }); -} - -function averageSamples(batchResults: (number | Decimal)[][], precision: number) { - const averageScores = batchResults[0] - .map((_, columnIndex) => { - let sum = new Decimal(0); - batchResults.forEach((row) => { - sum = sum.plus(row[columnIndex]); - }); - return sum.dividedBy(batchResults.length); - }) - .map((score) => score.toDecimalPlaces(precision)); - - return averageScores; -} diff --git a/src/handlers/comment/handlers/issue/score-sources.ts b/src/handlers/comment/handlers/issue/score-sources.ts deleted file mode 100644 index 120a7b154..000000000 --- a/src/handlers/comment/handlers/issue/score-sources.ts +++ /dev/null @@ -1,123 +0,0 @@ -import { Context } from "../../../../types/context"; -import { Comment, Issue, User } from "../../../../types/payload"; -import { assigneeScoring as assigneeTaskScoring } from "./assignee-scoring"; -import { commentsScoring } from "./evaluate-comments"; -import { getPullRequestComments } from "./get-pull-request-comments"; -import { UserScoreDetails } from "./issue-shared-types"; -import { specificationScoring as issuerSpecificationScoring } from "./specification-scoring"; - -export async function aggregateAndScoreContributions({ - context, - issue, - issueComments, - owner, - repository, - issueNumber, -}: ScoreParams): Promise { - const issueIssuerSpecification = await issuerSpecificationScoring({ context, issue, view: "Issue" }); - - const issueAssigneeTask = await assigneeTaskScoring(context, { - issue, - source: issue.assignees.filter((assignee): assignee is User => Boolean(assignee)), - view: "Issue", - }); - - const issueContributorComments = await commentsScoring({ - context, - issue, - source: issueComments.filter(botCommandsAndHumanCommentsFilter), - view: "Issue", - }); - - const reviewContributorComments = await commentsScoring({ - context, - issue, - source: ( - await getPullRequestComments(context, owner, repository, issueNumber) - ).filter(botCommandsAndHumanCommentsFilter), - view: "Review", - }); - - // TODO: review pull request scoring - // TODO: code contribution scoring - - return [...issueIssuerSpecification, ...issueAssigneeTask, ...issueContributorComments, ...reviewContributorComments]; -} - -interface ScoreParams { - context: Context; - issue: Issue; - issueComments: Comment[]; - owner: string; - repository: string; - issueNumber: number; -} - -// different ways to earn: - -/** - * - * 1. write a specification - * - things to collect: - * - - author (User) - * - - issue (Issue) - * - scoring: - * - - formatting - * - - word count - * - - clarity - * - * 2. be assigned a task and complete it - * - things to collect: - * - - assignees (User[]) - * - - issue (Issue) - * - scoring: - * - - just take the price of the issue, divide by amount of assignees - * - * 3. comment on the issue - * - things to collect: - * - - author (User) - * - - issue (Issue) - * - - comments (Comment[]) - * - scoring: - * - - formatting - * - - word count - * - - relevance - * - * 4. comment on the pull request - * - things to collect: - * - - author (User) - * - - issue (Issue) - * - - comments (Comment[]) - * - scoring: - * - - formatting - * - - word count - * - - relevance - * - * 5. review the pull request - * - things to collect: - * - - reviewer (User) - * - - issue (Issue) - * - - comments (Comment[]) - * - - pull request (PullRequest) - * - - review (Review) - * - - review comments (Comment[]) - * - scoring: - * - - formatting - * - - word count - * - - relevance - * - * 6. contribute code - * - things to collect: - * - - author (User) - * - - issue (Issue) - * - - pull request (PullRequest) - * - - commits (Commit[]) - * - - files (File[]) - * - scoring: - * - - ??? - * - */ - -function botCommandsAndHumanCommentsFilter(comment: Comment) { - return !comment.body.startsWith("/") /* No Commands */ && comment.user.type === "User"; -} /* No Bots */ diff --git a/src/handlers/comment/handlers/issue/specification-scoring.ts b/src/handlers/comment/handlers/issue/specification-scoring.ts deleted file mode 100644 index 3700c59b6..000000000 --- a/src/handlers/comment/handlers/issue/specification-scoring.ts +++ /dev/null @@ -1,67 +0,0 @@ -import Decimal from "decimal.js"; - -import { Comment, Issue } from "../../../../types/payload"; -import { allCommentScoring } from "./all-comment-scoring"; -import { UserScoreDetails } from "./issue-shared-types"; -import { addRelevanceAndFormatScoring } from "./relevance-format-scoring"; - -// import Runtime from "../../../../bindings/bot-runtime"; -import { Context } from "../../../../types/context"; -import { ContributorView } from "./contribution-style-types"; - -export type ContextIssue = { context: Context; issue: Issue }; - -export async function specificationScoring({ - context, - issue, - view, -}: ContextIssue & { view: ContributorView }): Promise { - // const logger = Runtime.getState().logger; - const userScoreDetails = [] as UserScoreDetails[]; - - const issueAsComment = castIssueAsComment(issue); - - // synthetic relevance score - const RELEVANT = [{ comment: issueAsComment, user: issue.user, score: new Decimal(1) }]; - - const formatting = await allCommentScoring({ context, issue, comments: [issueAsComment], view }); - const scoreDetails = addRelevanceAndFormatScoring(RELEVANT, formatting); - for (const user in scoreDetails) { - const userScore = scoreDetails[user]; - if (userScore.contributionClass !== "Issue Issuer Comment") continue; - - const userScoreDetail: UserScoreDetails = { - score: userScore.commentScores[issue.user.id].totalScoreTotal, - view: view, - role: "Issuer", - contribution: "Specification", - scoring: { - specification: userScore, - issueComments: null, - reviewComments: null, - task: null, - }, - source: { - user: issue.user, - issue: issue, - }, - }; - - userScoreDetails.push(userScoreDetail); - } - return userScoreDetails; -} - -function castIssueAsComment(issue: Issue): Comment { - return { - body: issue.body, - user: issue.user, - created_at: issue.created_at, - updated_at: issue.updated_at, - id: issue.id, - node_id: issue.node_id, - author_association: issue.author_association, - html_url: issue.html_url, - url: issue.url, - } as Comment; -} diff --git a/src/handlers/comment/handlers/issue/sum-total-scores-per-contributor.ts b/src/handlers/comment/handlers/issue/sum-total-scores-per-contributor.ts deleted file mode 100644 index b74398d98..000000000 --- a/src/handlers/comment/handlers/issue/sum-total-scores-per-contributor.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { UserScoreDetails, UserScoreTotals } from "./issue-shared-types"; - -export function sumTotalScores(allSourceScores: UserScoreDetails[]): { [userId: string]: UserScoreTotals } { - const totals = allSourceScores.reduce((accumulator, currentScore) => { - const { score, source } = currentScore; - const userId = source.user.id; - // const username = source.user.login; - if (!accumulator[userId]) { - accumulator[userId] = { - total: score, - details: [currentScore], - user: source.user, - } as UserScoreTotals; - } else { - accumulator[userId].total = accumulator[userId].total.plus(score); - accumulator[userId].details.push(currentScore); - } - return accumulator; - }, {} as { [userId: string]: UserScoreTotals }); - return totals; -} diff --git a/src/handlers/processors.ts b/src/handlers/processors.ts index 058825bea..5046fc592 100644 --- a/src/handlers/processors.ts +++ b/src/handlers/processors.ts @@ -4,7 +4,7 @@ import { GitHubEvent } from "../types/payload"; import { closePullRequestForAnIssue, startCommandHandler } from "./assign/action"; import { checkPullRequests } from "./assign/auto"; import { commentCreatedOrEdited } from "./comment/action"; -import { issueClosed } from "./comment/handlers/issue/issue-closed"; +import { issueClosed } from "./comment/handlers/issue-closed"; import { watchLabelChange } from "./label/label"; import { syncPriceLabelsToConfig } from "./pricing/pre"; import { onLabelChangeSetPricing } from "./pricing/pricing-label"; diff --git a/src/handlers/wildcard/unassign/unassign.ts b/src/handlers/wildcard/unassign/unassign.ts index c1bc15b56..1d4b2032d 100644 --- a/src/handlers/wildcard/unassign/unassign.ts +++ b/src/handlers/wildcard/unassign/unassign.ts @@ -1,6 +1,6 @@ import { RestEndpointMethodTypes } from "@octokit/rest"; +import { getLinkedPullRequests } from "../../../helpers/get-linked-issues-and-pull-requests"; import { listAllIssuesAndPullsForRepo } from "../../../helpers/issue"; -import { getLinkedPullRequests } from "../../../helpers/parser"; import { Commit } from "../../../types/commit"; import { Context } from "../../../types/context"; import { Issue, IssueType, Payload, User } from "../../../types/payload"; diff --git a/src/helpers/parser.ts b/src/helpers/get-linked-issues-and-pull-requests.ts similarity index 100% rename from src/helpers/parser.ts rename to src/helpers/get-linked-issues-and-pull-requests.ts diff --git a/src/types/configuration-types.ts b/src/types/configuration-types.ts index 0246c7000..e2e113744 100644 --- a/src/types/configuration-types.ts +++ b/src/types/configuration-types.ts @@ -2,8 +2,9 @@ import { ObjectOptions, Static, StaticDecode, StringOptions, TProperties, Type a import ms from "ms"; import { LogLevel } from "../adapters/supabase/helpers/pretty-logs"; import { userCommands } from "../handlers/comment/handlers/comment-handler-main"; -import { validHTMLElements } from "../handlers/comment/handlers/issue/valid-html-elements"; + import { ajv } from "../utils/ajv"; +import { validHTMLElements } from "./valid-html-elements"; const promotionComment = "###### If you enjoy the DevPool experience, please follow [Ubiquity on GitHub](https://github.com/ubiquity) and star [this repo](https://github.com/ubiquity/devpool-directory) to show your support. It helps a lot!"; diff --git a/src/handlers/comment/handlers/issue/valid-html-elements.ts b/src/types/valid-html-elements.ts similarity index 100% rename from src/handlers/comment/handlers/issue/valid-html-elements.ts rename to src/types/valid-html-elements.ts