From 8035852d101ff3df790c15f7e79afed24f372cfe Mon Sep 17 00:00:00 2001 From: Zoltan Date: Thu, 28 Nov 2024 22:54:04 +0100 Subject: [PATCH] draft --- package-lock.json | 1751 +++++++++++++++++++++++++-------------------- package.json | 7 +- src/bot.ts | 165 +++-- src/main.ts | 2 +- src/prompts.ts | 53 +- src/review.ts | 978 +++++-------------------- src/review_old.ts | 871 ++++++++++++++++++++++ src/types.ts | 47 ++ 8 files changed, 2217 insertions(+), 1657 deletions(-) create mode 100644 src/review_old.ts create mode 100644 src/types.ts diff --git a/package-lock.json b/package-lock.json index d23b6fa9..c64729f7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,6 +12,8 @@ "@actions/core": "^1.10.0", "@actions/github": "^5.1.1", "@dqbd/tiktoken": "^1.0.7", + "@langchain/core": "^0.3.19", + "@langchain/google-genai": "^0.1.4", "@octokit/action": "^6.0.4", "@octokit/plugin-retry": "^4.1.3", "@octokit/plugin-throttling": "^6.1.0", @@ -22,7 +24,6 @@ "p-retry": "^5.1.2" }, "devDependencies": { - "@jest/globals": "^29.5.0", "@types/node": "^20.4.2", "@typescript-eslint/eslint-plugin": "^5.59.6", "@typescript-eslint/parser": "^5.59.6", @@ -37,8 +38,8 @@ "jest": "^27.2.5", "js-yaml": "^4.1.0", "prettier": "2.8.8", - "ts-jest": "^27.1.2", - "typescript": "^4.9.5" + "tsx": "^4.19.2", + "typescript": "^5.7.2" } }, "node_modules/@actions/core": { @@ -251,9 +252,9 @@ } }, "node_modules/@babel/core/node_modules/semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, "bin": { "semver": "bin/semver.js" @@ -308,9 +309,9 @@ } }, "node_modules/@babel/helper-compilation-targets/node_modules/semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, "bin": { "semver": "bin/semver.js" @@ -616,21 +617,6 @@ "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-syntax-jsx": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.18.6.tgz", - "integrity": "sha512-6mmljtAedFGTWu2p/8WIORGwy+61PLgOMPOdazc7YoJ9ZCWUyFy3A6CpPkRKLKD1ToAesxX8KGEViAiLo9N+7Q==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.18.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, "node_modules/@babel/plugin-syntax-logical-assignment-operators": { "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", @@ -809,11 +795,421 @@ "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", "dev": true }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@cspotcode/source-map-support/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, "node_modules/@dqbd/tiktoken": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/@dqbd/tiktoken/-/tiktoken-1.0.7.tgz", "integrity": "sha512-bhR5k5W+8GLzysjk8zTMVygQZsgvf7W1F0IlL4ZQ5ugjo5rCyiwGM5d8DYriXspytfu98tv59niang3/T+FoDw==" }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.23.1.tgz", + "integrity": "sha512-6VhYk1diRqrhBAqpJEdjASR/+WVRtfjpqKuNw11cLiaWpAT/Uu+nokB+UJnevzy/P9C/ty6AOe0dwueMrGh/iQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.23.1.tgz", + "integrity": "sha512-uz6/tEy2IFm9RYOyvKl88zdzZfwEfKZmnX9Cj1BHjeSGNuGLuMD1kR8y5bteYmwqKm1tj8m4cb/aKEorr6fHWQ==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.23.1.tgz", + "integrity": "sha512-xw50ipykXcLstLeWH7WRdQuysJqejuAGPd30vd1i5zSyKK3WE+ijzHmLKxdiCMtH1pHz78rOg0BKSYOSB/2Khw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.23.1.tgz", + "integrity": "sha512-nlN9B69St9BwUoB+jkyU090bru8L0NA3yFvAd7k8dNsVH8bi9a8cUAUSEcEEgTp2z3dbEDGJGfP6VUnkQnlReg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.23.1.tgz", + "integrity": "sha512-YsS2e3Wtgnw7Wq53XXBLcV6JhRsEq8hkfg91ESVadIrzr9wO6jJDMZnCQbHm1Guc5t/CdDiFSSfWP58FNuvT3Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.23.1.tgz", + "integrity": "sha512-aClqdgTDVPSEGgoCS8QDG37Gu8yc9lTHNAQlsztQ6ENetKEO//b8y31MMu2ZaPbn4kVsIABzVLXYLhCGekGDqw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.23.1.tgz", + "integrity": "sha512-h1k6yS8/pN/NHlMl5+v4XPfikhJulk4G+tKGFIOwURBSFzE8bixw1ebjluLOjfwtLqY0kewfjLSrO6tN2MgIhA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.23.1.tgz", + "integrity": "sha512-lK1eJeyk1ZX8UklqFd/3A60UuZ/6UVfGT2LuGo3Wp4/z7eRTRYY+0xOu2kpClP+vMTi9wKOfXi2vjUpO1Ro76g==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.23.1.tgz", + "integrity": "sha512-CXXkzgn+dXAPs3WBwE+Kvnrf4WECwBdfjfeYHpMeVxWE0EceB6vhWGShs6wi0IYEqMSIzdOF1XjQ/Mkm5d7ZdQ==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.23.1.tgz", + "integrity": "sha512-/93bf2yxencYDnItMYV/v116zff6UyTjo4EtEQjUBeGiVpMmffDNUyD9UN2zV+V3LRV3/on4xdZ26NKzn6754g==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.23.1.tgz", + "integrity": "sha512-VTN4EuOHwXEkXzX5nTvVY4s7E/Krz7COC8xkftbbKRYAl96vPiUssGkeMELQMOnLOJ8k3BY1+ZY52tttZnHcXQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.23.1.tgz", + "integrity": "sha512-Vx09LzEoBa5zDnieH8LSMRToj7ir/Jeq0Gu6qJ/1GcBq9GkfoEAoXvLiW1U9J1qE/Y/Oyaq33w5p2ZWrNNHNEw==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.23.1.tgz", + "integrity": "sha512-nrFzzMQ7W4WRLNUOU5dlWAqa6yVeI0P78WKGUo7lg2HShq/yx+UYkeNSE0SSfSure0SqgnsxPvmAUu/vu0E+3Q==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.23.1.tgz", + "integrity": "sha512-dKN8fgVqd0vUIjxuJI6P/9SSSe/mB9rvA98CSH2sJnlZ/OCZWO1DJvxj8jvKTfYUdGfcq2dDxoKaC6bHuTlgcw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.23.1.tgz", + "integrity": "sha512-5AV4Pzp80fhHL83JM6LoA6pTQVWgB1HovMBsLQ9OZWLDqVY8MVobBXNSmAJi//Csh6tcY7e7Lny2Hg1tElMjIA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.23.1.tgz", + "integrity": "sha512-9ygs73tuFCe6f6m/Tb+9LtYxWR4c9yg7zjt2cYkjDbDpV/xVn+68cQxMXCjUpYwEkze2RcU/rMnfIXNRFmSoDw==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.23.1.tgz", + "integrity": "sha512-EV6+ovTsEXCPAp58g2dD68LxoP/wK5pRvgy0J/HxPGB009omFPv3Yet0HiaqvrIrgPTBuC6wCH1LTOY91EO5hQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.23.1.tgz", + "integrity": "sha512-aevEkCNu7KlPRpYLjwmdcuNz6bDFiE7Z8XC4CPqExjTvrHugh28QzUXVOZtiYghciKUacNktqxdpymplil1beA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.23.1.tgz", + "integrity": "sha512-3x37szhLexNA4bXhLrCC/LImN/YtWis6WXr1VESlfVtVeoFJBRINPJ3f0a/6LV8zpikqoUg4hyXw0sFBt5Cr+Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.23.1.tgz", + "integrity": "sha512-aY2gMmKmPhxfU+0EdnN+XNtGbjfQgwZj43k8G3fyrDM/UdZww6xrWxmDkuz2eCZchqVeABjV5BpildOrUbBTqA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.23.1.tgz", + "integrity": "sha512-RBRT2gqEl0IKQABT4XTj78tpk9v7ehp+mazn2HbUeZl1YMdaGAQqhapjGTCe7uw7y0frDi4gS0uHzhvpFuI1sA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.23.1.tgz", + "integrity": "sha512-4O+gPR5rEBe2FpKOVyiJ7wNDPA8nGzDuJ6gN4okSA1gEOYZ67N8JPk58tkWtdtPeLz7lBnY6I5L3jdsr3S+A6A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.23.1.tgz", + "integrity": "sha512-BcaL0Vn6QwCwre3Y717nVHZbAa4UBEigzFm6VdsVdT/MbZ38xoj1X9HPkZhbmaBGUD1W8vxAfffbDe8bA6AKnQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.23.1.tgz", + "integrity": "sha512-BHpFFeslkWrXWyUPnbKm+xYYVYruCinGcftSBaa8zoF9hZO4BcSCFUvHVTtzpIY6YzUnYtuEhZ+C9iEXjxnasg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, "node_modules/@eslint-community/eslint-utils": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", @@ -920,6 +1316,14 @@ "integrity": "sha512-gIhjdJp/c2beaIWWIlsXdqXVRUz3r2BxBCpfz/F3JXHvSAQ1paMYjLH+maEATtENg+k5eLV7gA+9yPp762ieuw==", "dev": true }, + "node_modules/@google/generative-ai": { + "version": "0.21.0", + "resolved": "https://registry.npmjs.org/@google/generative-ai/-/generative-ai-0.21.0.tgz", + "integrity": "sha512-7XhUbtnlkSEZK15kN3t+tzIMxsbKm/dSkKBFalj+20NvPKe1kBY7mR2P7vuijEn+f06z5+A8bVGKO0v39cr6Wg==", + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/@humanwhocodes/config-array": { "version": "0.11.8", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.8.tgz", @@ -1367,78 +1771,6 @@ "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" } }, - "node_modules/@jest/environment": { - "version": "29.5.0", - "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-29.5.0.tgz", - "integrity": "sha512-5FXw2+wD29YU1d4I2htpRX7jYnAyTRjP2CsXQdo9SAM8g3ifxWPSV0HnClSn71xwctr0U3oZIIH+dtbfmnbXVQ==", - "dev": true, - "dependencies": { - "@jest/fake-timers": "^29.5.0", - "@jest/types": "^29.5.0", - "@types/node": "*", - "jest-mock": "^29.5.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@jest/expect": { - "version": "29.5.0", - "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-29.5.0.tgz", - "integrity": "sha512-PueDR2HGihN3ciUNGr4uelropW7rqUfTiOn+8u0leg/42UhblPxHkfoh0Ruu3I9Y1962P3u2DY4+h7GVTSVU6g==", - "dev": true, - "dependencies": { - "expect": "^29.5.0", - "jest-snapshot": "^29.5.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@jest/expect-utils": { - "version": "29.5.0", - "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.5.0.tgz", - "integrity": "sha512-fmKzsidoXQT2KwnrwE0SQq3uj8Z763vzR8LnLBwC2qYWEFpjX8daRsk6rHUM1QvNlEW/UJXNXm59ztmJJWs2Mg==", - "dev": true, - "dependencies": { - "jest-get-type": "^29.4.3" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@jest/fake-timers": { - "version": "29.5.0", - "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-29.5.0.tgz", - "integrity": "sha512-9ARvuAAQcBwDAqOnglWq2zwNIRUDtk/SCkp/ToGEhFv5r86K21l+VEs0qNTaXtyiY0lEePl3kylijSYJQqdbDg==", - "dev": true, - "dependencies": { - "@jest/types": "^29.5.0", - "@sinonjs/fake-timers": "^10.0.2", - "@types/node": "*", - "jest-message-util": "^29.5.0", - "jest-mock": "^29.5.0", - "jest-util": "^29.5.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@jest/globals": { - "version": "29.5.0", - "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-29.5.0.tgz", - "integrity": "sha512-S02y0qMWGihdzNbUiqSAiKSpSozSuHX5UYc7QbnHP+D9Lyw8DgGGCinrN9uSuHPeKgSSzvPom2q1nAtBvUsvPQ==", - "dev": true, - "dependencies": { - "@jest/environment": "^29.5.0", - "@jest/expect": "^29.5.0", - "@jest/types": "^29.5.0", - "jest-mock": "^29.5.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, "node_modules/@jest/reporters": { "version": "27.5.1", "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-27.5.1.tgz", @@ -1525,18 +1857,6 @@ "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" } }, - "node_modules/@jest/schemas": { - "version": "29.4.3", - "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.4.3.tgz", - "integrity": "sha512-VLYKXQmtmuEz6IxJsrZwzG9NvtkQsWNnWMsKxqWNu3+CnfzJQhp0WDDKWLVV9hLKr0l3SLLFRqcYHjhtyuDVxg==", - "dev": true, - "dependencies": { - "@sinclair/typebox": "^0.25.16" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, "node_modules/@jest/source-map": { "version": "27.5.1", "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-27.5.1.tgz", @@ -1674,23 +1994,6 @@ "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" } }, - "node_modules/@jest/types": { - "version": "29.5.0", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.5.0.tgz", - "integrity": "sha512-qbu7kN6czmVRc3xWFQcAN03RAUamgppVUdXrvl1Wr3jlNF93o9mJbGcDWrwGB6ht44u7efB1qCFgVQmca24Uog==", - "dev": true, - "dependencies": { - "@jest/schemas": "^29.4.3", - "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^3.0.0", - "@types/node": "*", - "@types/yargs": "^17.0.8", - "chalk": "^4.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, "node_modules/@jridgewell/gen-mapping": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.1.1.tgz", @@ -1738,6 +2041,93 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@langchain/core": { + "version": "0.3.19", + "resolved": "https://registry.npmjs.org/@langchain/core/-/core-0.3.19.tgz", + "integrity": "sha512-pJVOAHShefu1SRO8uhzUs0Pexah/Ib66WETLMScIC2w9vXlpwQy3DzXJPJ5X7ixry9N666jYO5cHtM2Z1DnQIQ==", + "dependencies": { + "ansi-styles": "^5.0.0", + "camelcase": "6", + "decamelize": "1.2.0", + "js-tiktoken": "^1.0.12", + "langsmith": "^0.2.0", + "mustache": "^4.2.0", + "p-queue": "^6.6.2", + "p-retry": "4", + "uuid": "^10.0.0", + "zod": "^3.22.4", + "zod-to-json-schema": "^3.22.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@langchain/core/node_modules/@types/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==" + }, + "node_modules/@langchain/core/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@langchain/core/node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@langchain/core/node_modules/p-retry": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-4.6.2.tgz", + "integrity": "sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ==", + "dependencies": { + "@types/retry": "0.12.0", + "retry": "^0.13.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@langchain/core/node_modules/uuid": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/@langchain/google-genai": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/@langchain/google-genai/-/google-genai-0.1.4.tgz", + "integrity": "sha512-b8qrqnHYbNseaAikrWyxuDTww6CUIse82F5/BmF2GtWVR25yJrNUWETfTp7o7iIMxhFR0PuQag4gEZOL74F5Tw==", + "dependencies": { + "@google/generative-ai": "^0.21.0", + "zod-to-json-schema": "^3.22.4" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@langchain/core": ">=0.3.17 <0.4.0" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -2113,30 +2503,6 @@ "@octokit/openapi-types": "^16.0.0" } }, - "node_modules/@sinclair/typebox": { - "version": "0.25.24", - "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.25.24.tgz", - "integrity": "sha512-XJfwUVUKDHF5ugKwIcxEgc9k8b7HbznCp6eUfWgu710hMPNIO4aw4/zB5RogDQz8nd6gyCDpU9O/m6qYEWY6yQ==", - "dev": true - }, - "node_modules/@sinonjs/commons": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-2.0.0.tgz", - "integrity": "sha512-uLa0j859mMrg2slwQYdO/AkrOfmH+X6LTVmNTS9CqexuE2IvVORIkSpJLqePAbEnKJ77aMmCwr1NUZ57120Xcg==", - "dev": true, - "dependencies": { - "type-detect": "4.0.8" - } - }, - "node_modules/@sinonjs/fake-timers": { - "version": "10.0.2", - "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.0.2.tgz", - "integrity": "sha512-SwUDyjWnah1AaNl7kxsa7cfLhlTYoiyhDAIgyh+El30YvXs/o7OLXpYH88Zdhyx9JExKrmHDJ+10bwIcY80Jmw==", - "dev": true, - "dependencies": { - "@sinonjs/commons": "^2.0.0" - } - }, "node_modules/@tootallnate/once": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz", @@ -2146,6 +2512,38 @@ "node": ">= 6" } }, + "node_modules/@tsconfig/node10": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz", + "integrity": "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==", + "dev": true, + "optional": true, + "peer": true + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true, + "optional": true, + "peer": true + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true, + "optional": true, + "peer": true + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "dev": true, + "optional": true, + "peer": true + }, "node_modules/@types/babel__core": { "version": "7.20.0", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.0.tgz", @@ -2261,14 +2659,10 @@ "integrity": "sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw==", "dev": true }, - "node_modules/@types/yargs": { - "version": "17.0.23", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.23.tgz", - "integrity": "sha512-yuogunc04OnzGQCrfHx+Kk883Q4X0aSwmYZhKjI21m+SVYzjIbrWl8dOOwSv5hf2Um2pdCOXWo9isteZTNXUZQ==", - "dev": true, - "dependencies": { - "@types/yargs-parser": "*" - } + "node_modules/@types/uuid": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==" }, "node_modules/@types/yargs-parser": { "version": "21.0.0", @@ -2502,9 +2896,9 @@ "dev": true }, "node_modules/acorn": { - "version": "8.8.2", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.2.tgz", - "integrity": "sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw==", + "version": "8.14.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz", + "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==", "dev": true, "bin": { "acorn": "bin/acorn" @@ -2629,6 +3023,14 @@ "node": ">= 8" } }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true, + "optional": true, + "peer": true + }, "node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", @@ -2908,6 +3310,25 @@ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, "node_modules/before-after-hook": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-2.2.3.tgz", @@ -2972,18 +3393,6 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, - "node_modules/bs-logger": { - "version": "0.2.6", - "resolved": "https://registry.npmjs.org/bs-logger/-/bs-logger-0.2.6.tgz", - "integrity": "sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==", - "dev": true, - "dependencies": { - "fast-json-stable-stringify": "2.x" - }, - "engines": { - "node": ">= 6" - } - }, "node_modules/bser": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", @@ -3157,6 +3566,14 @@ "node": ">= 0.8" } }, + "node_modules/commander": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz", + "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==", + "engines": { + "node": ">=14" + } + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -3169,10 +3586,18 @@ "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", "dev": true }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true, + "optional": true, + "peer": true + }, "node_modules/cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "dev": true, "dependencies": { "path-key": "^3.1.0", @@ -3251,6 +3676,14 @@ } } }, + "node_modules/decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/decimal.js": { "version": "10.4.3", "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.4.3.tgz", @@ -3344,13 +3777,15 @@ "node": ">=8" } }, - "node_modules/diff-sequences": { - "version": "29.4.3", - "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.4.3.tgz", - "integrity": "sha512-ofrBgwpPhCD85kMKtE9RYFFq6OC1A89oW2vvgWZNCwxrUpRUILopY7lsYyMDSjc8g6U6aiO0Qubg6r4Wgt5ZnA==", + "node_modules/diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", "dev": true, + "optional": true, + "peer": true, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": ">=0.3.1" } }, "node_modules/dir-glob": { @@ -3539,6 +3974,45 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/esbuild": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.23.1.tgz", + "integrity": "sha512-VVNz/9Sa0bs5SELtn3f7qhJCDPCF5oMEl5cO9/SSinpE9hbPVvxbd572HH5AKiP7WD8INO53GgfDDhRjkylHEg==", + "dev": true, + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.23.1", + "@esbuild/android-arm": "0.23.1", + "@esbuild/android-arm64": "0.23.1", + "@esbuild/android-x64": "0.23.1", + "@esbuild/darwin-arm64": "0.23.1", + "@esbuild/darwin-x64": "0.23.1", + "@esbuild/freebsd-arm64": "0.23.1", + "@esbuild/freebsd-x64": "0.23.1", + "@esbuild/linux-arm": "0.23.1", + "@esbuild/linux-arm64": "0.23.1", + "@esbuild/linux-ia32": "0.23.1", + "@esbuild/linux-loong64": "0.23.1", + "@esbuild/linux-mips64el": "0.23.1", + "@esbuild/linux-ppc64": "0.23.1", + "@esbuild/linux-riscv64": "0.23.1", + "@esbuild/linux-s390x": "0.23.1", + "@esbuild/linux-x64": "0.23.1", + "@esbuild/netbsd-x64": "0.23.1", + "@esbuild/openbsd-arm64": "0.23.1", + "@esbuild/openbsd-x64": "0.23.1", + "@esbuild/sunos-x64": "0.23.1", + "@esbuild/win32-arm64": "0.23.1", + "@esbuild/win32-ia32": "0.23.1", + "@esbuild/win32-x64": "0.23.1" + } + }, "node_modules/escalade": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", @@ -4002,9 +4476,9 @@ } }, "node_modules/eslint-plugin-import/node_modules/semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, "bin": { "semver": "bin/semver.js" @@ -4087,9 +4561,9 @@ } }, "node_modules/eslint-plugin-jsx-a11y/node_modules/semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, "bin": { "semver": "bin/semver.js" @@ -4364,6 +4838,11 @@ "node": ">=0.10.0" } }, + "node_modules/eventemitter3": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==" + }, "node_modules/execa": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", @@ -4396,22 +4875,6 @@ "node": ">= 0.8.0" } }, - "node_modules/expect": { - "version": "29.5.0", - "resolved": "https://registry.npmjs.org/expect/-/expect-29.5.0.tgz", - "integrity": "sha512-yM7xqUrCO2JdpFo4XpM82t+PJBFybdqoQuJLDGeDX2ij8NZzqRHyu3Hp188/JX7SWqud+7t4MUdvcgGBICMHZg==", - "dev": true, - "dependencies": { - "@jest/expect-utils": "^29.5.0", - "jest-get-type": "^29.4.3", - "jest-matcher-utils": "^29.5.0", - "jest-message-util": "^29.5.0", - "jest-util": "^29.5.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -4623,9 +5086,9 @@ "dev": true }, "node_modules/fsevents": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", - "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", "dev": true, "hasInstallScript": true, "optional": true, @@ -4738,6 +5201,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/get-tsconfig": { + "version": "4.8.1", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.8.1.tgz", + "integrity": "sha512-k9PN+cFBmaLWtVz29SkUoqU5O0slLuHJXt/2P+tMVFT+phsSGXGkp9t3rQIqdz0e+06EHNGs3oM6ZX1s2zHxRg==", + "dev": true, + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, "node_modules/glob": { "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", @@ -5494,9 +5969,9 @@ } }, "node_modules/istanbul-lib-instrument/node_modules/semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, "bin": { "semver": "bin/semver.js" @@ -6028,53 +6503,6 @@ "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" } }, - "node_modules/jest-diff": { - "version": "29.5.0", - "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.5.0.tgz", - "integrity": "sha512-LtxijLLZBduXnHSniy0WMdaHjmQnt3g5sa16W4p0HqukYTTsyTW3GD1q41TyGl5YFXj/5B2U6dlh5FM1LIMgxw==", - "dev": true, - "dependencies": { - "chalk": "^4.0.0", - "diff-sequences": "^29.4.3", - "jest-get-type": "^29.4.3", - "pretty-format": "^29.5.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-diff/node_modules/ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/jest-diff/node_modules/pretty-format": { - "version": "29.5.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.5.0.tgz", - "integrity": "sha512-V2mGkI31qdttvTFX7Mt4efOqHXqJWMu4/r66Xh3Z3BwZaPfPJgp6/gbwoujRpPUtfEF6AUUWx3Jim3GCw5g/Qw==", - "dev": true, - "dependencies": { - "@jest/schemas": "^29.4.3", - "ansi-styles": "^5.0.0", - "react-is": "^18.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-diff/node_modules/react-is": { - "version": "18.2.0", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", - "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", - "dev": true - }, "node_modules/jest-docblock": { "version": "27.5.1", "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-27.5.1.tgz", @@ -6439,15 +6867,6 @@ "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" } }, - "node_modules/jest-get-type": { - "version": "29.4.3", - "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.4.3.tgz", - "integrity": "sha512-J5Xez4nRRMjk8emnTpWrlkyb9pfRQQanDrvWHhsR1+VUfbwxi30eVcZFlcdGInRibU4G5LwHXpI7IRHU0CY+gg==", - "dev": true, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, "node_modules/jest-haste-map": { "version": "27.5.1", "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-27.5.1.tgz", @@ -6787,119 +7206,6 @@ "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" } }, - "node_modules/jest-matcher-utils": { - "version": "29.5.0", - "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.5.0.tgz", - "integrity": "sha512-lecRtgm/rjIK0CQ7LPQwzCs2VwW6WAahA55YBuI+xqmhm7LAaxokSB8C97yJeYyT+HvQkH741StzpU41wohhWw==", - "dev": true, - "dependencies": { - "chalk": "^4.0.0", - "jest-diff": "^29.5.0", - "jest-get-type": "^29.4.3", - "pretty-format": "^29.5.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-matcher-utils/node_modules/ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/jest-matcher-utils/node_modules/pretty-format": { - "version": "29.5.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.5.0.tgz", - "integrity": "sha512-V2mGkI31qdttvTFX7Mt4efOqHXqJWMu4/r66Xh3Z3BwZaPfPJgp6/gbwoujRpPUtfEF6AUUWx3Jim3GCw5g/Qw==", - "dev": true, - "dependencies": { - "@jest/schemas": "^29.4.3", - "ansi-styles": "^5.0.0", - "react-is": "^18.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-matcher-utils/node_modules/react-is": { - "version": "18.2.0", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", - "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", - "dev": true - }, - "node_modules/jest-message-util": { - "version": "29.5.0", - "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.5.0.tgz", - "integrity": "sha512-Kijeg9Dag6CKtIDA7O21zNTACqD5MD/8HfIV8pdD94vFyFuer52SigdC3IQMhab3vACxXMiFk+yMHNdbqtyTGA==", - "dev": true, - "dependencies": { - "@babel/code-frame": "^7.12.13", - "@jest/types": "^29.5.0", - "@types/stack-utils": "^2.0.0", - "chalk": "^4.0.0", - "graceful-fs": "^4.2.9", - "micromatch": "^4.0.4", - "pretty-format": "^29.5.0", - "slash": "^3.0.0", - "stack-utils": "^2.0.3" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-message-util/node_modules/ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/jest-message-util/node_modules/pretty-format": { - "version": "29.5.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.5.0.tgz", - "integrity": "sha512-V2mGkI31qdttvTFX7Mt4efOqHXqJWMu4/r66Xh3Z3BwZaPfPJgp6/gbwoujRpPUtfEF6AUUWx3Jim3GCw5g/Qw==", - "dev": true, - "dependencies": { - "@jest/schemas": "^29.4.3", - "ansi-styles": "^5.0.0", - "react-is": "^18.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-message-util/node_modules/react-is": { - "version": "18.2.0", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", - "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", - "dev": true - }, - "node_modules/jest-mock": { - "version": "29.5.0", - "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-29.5.0.tgz", - "integrity": "sha512-GqOzvdWDE4fAV2bWQLQCkujxYWL7RxjCnj71b5VhDAGOevB3qj3Ovg26A5NI84ZpODxyzaozXLOh2NCgkbvyaw==", - "dev": true, - "dependencies": { - "@jest/types": "^29.5.0", - "@types/node": "*", - "jest-util": "^29.5.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, "node_modules/jest-pnp-resolver": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", @@ -7476,311 +7782,106 @@ "pretty-format": "^27.5.1" }, "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/jest-runtime/node_modules/jest-get-type": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-27.5.1.tgz", - "integrity": "sha512-2KY95ksYSaK7DMBWQn6dQz3kqAf3BB64y2udeG+hv4KfSOb9qwcYQstTJc1KCbsix+wLZWZYN8t7nwX3GOBLRw==", - "dev": true, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/jest-runtime/node_modules/jest-matcher-utils": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-27.5.1.tgz", - "integrity": "sha512-z2uTx/T6LBaCoNWNFWwChLBKYxTMcGBRjAt+2SbP929/Fflb9aa5LGma654Rz8z9HLxsrUaYzxE9T/EFIL/PAw==", - "dev": true, - "dependencies": { - "chalk": "^4.0.0", - "jest-diff": "^27.5.1", - "jest-get-type": "^27.5.1", - "pretty-format": "^27.5.1" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/jest-runtime/node_modules/jest-message-util": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-27.5.1.tgz", - "integrity": "sha512-rMyFe1+jnyAAf+NHwTclDz0eAaLkVDdKVHHBFWsBWHnnh5YeJMNWWsv7AbFYXfK3oTqvL7VTWkhNLu1jX24D+g==", - "dev": true, - "dependencies": { - "@babel/code-frame": "^7.12.13", - "@jest/types": "^27.5.1", - "@types/stack-utils": "^2.0.0", - "chalk": "^4.0.0", - "graceful-fs": "^4.2.9", - "micromatch": "^4.0.4", - "pretty-format": "^27.5.1", - "slash": "^3.0.0", - "stack-utils": "^2.0.3" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/jest-runtime/node_modules/jest-mock": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-27.5.1.tgz", - "integrity": "sha512-K4jKbY1d4ENhbrG2zuPWaQBvDly+iZ2yAW+T1fATN78hc0sInwn7wZB8XtlNnvHug5RMwV897Xm4LqmPM4e2Og==", - "dev": true, - "dependencies": { - "@jest/types": "^27.5.1", - "@types/node": "*" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/jest-runtime/node_modules/jest-snapshot": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-27.5.1.tgz", - "integrity": "sha512-yYykXI5a0I31xX67mgeLw1DZ0bJB+gpq5IpSuCAoyDi0+BhgU/RIrL+RTzDmkNTchvDFWKP8lp+w/42Z3us5sA==", - "dev": true, - "dependencies": { - "@babel/core": "^7.7.2", - "@babel/generator": "^7.7.2", - "@babel/plugin-syntax-typescript": "^7.7.2", - "@babel/traverse": "^7.7.2", - "@babel/types": "^7.0.0", - "@jest/transform": "^27.5.1", - "@jest/types": "^27.5.1", - "@types/babel__traverse": "^7.0.4", - "@types/prettier": "^2.1.5", - "babel-preset-current-node-syntax": "^1.0.0", - "chalk": "^4.0.0", - "expect": "^27.5.1", - "graceful-fs": "^4.2.9", - "jest-diff": "^27.5.1", - "jest-get-type": "^27.5.1", - "jest-haste-map": "^27.5.1", - "jest-matcher-utils": "^27.5.1", - "jest-message-util": "^27.5.1", - "jest-util": "^27.5.1", - "natural-compare": "^1.4.0", - "pretty-format": "^27.5.1", - "semver": "^7.3.2" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/jest-runtime/node_modules/jest-util": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-27.5.1.tgz", - "integrity": "sha512-Kv2o/8jNvX1MQ0KGtw480E/w4fBCDOnH6+6DmeKi6LZUIlKA5kwY0YNdlzaWTiVgxqAqik11QyxDOKk543aKXw==", - "dev": true, - "dependencies": { - "@jest/types": "^27.5.1", - "@types/node": "*", - "chalk": "^4.0.0", - "ci-info": "^3.2.0", - "graceful-fs": "^4.2.9", - "picomatch": "^2.2.3" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/jest-serializer": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-serializer/-/jest-serializer-27.5.1.tgz", - "integrity": "sha512-jZCyo6iIxO1aqUxpuBlwTDMkzOAJS4a3eYz3YzgxxVQFwLeSA7Jfq5cbqCY+JLvTDrWirgusI/0KwxKMgrdf7w==", - "dev": true, - "dependencies": { - "@types/node": "*", - "graceful-fs": "^4.2.9" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/jest-snapshot": { - "version": "29.5.0", - "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-29.5.0.tgz", - "integrity": "sha512-x7Wolra5V0tt3wRs3/ts3S6ciSQVypgGQlJpz2rsdQYoUKxMxPNaoHMGJN6qAuPJqS+2iQ1ZUn5kl7HCyls84g==", - "dev": true, - "dependencies": { - "@babel/core": "^7.11.6", - "@babel/generator": "^7.7.2", - "@babel/plugin-syntax-jsx": "^7.7.2", - "@babel/plugin-syntax-typescript": "^7.7.2", - "@babel/traverse": "^7.7.2", - "@babel/types": "^7.3.3", - "@jest/expect-utils": "^29.5.0", - "@jest/transform": "^29.5.0", - "@jest/types": "^29.5.0", - "@types/babel__traverse": "^7.0.6", - "@types/prettier": "^2.1.5", - "babel-preset-current-node-syntax": "^1.0.0", - "chalk": "^4.0.0", - "expect": "^29.5.0", - "graceful-fs": "^4.2.9", - "jest-diff": "^29.5.0", - "jest-get-type": "^29.4.3", - "jest-matcher-utils": "^29.5.0", - "jest-message-util": "^29.5.0", - "jest-util": "^29.5.0", - "natural-compare": "^1.4.0", - "pretty-format": "^29.5.0", - "semver": "^7.3.5" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-snapshot/node_modules/@jest/transform": { - "version": "29.5.0", - "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.5.0.tgz", - "integrity": "sha512-8vbeZWqLJOvHaDfeMuoHITGKSz5qWc9u04lnWrQE3VyuSw604PzQM824ZeX9XSjUCeDiE3GuxZe5UKa8J61NQw==", - "dev": true, - "dependencies": { - "@babel/core": "^7.11.6", - "@jest/types": "^29.5.0", - "@jridgewell/trace-mapping": "^0.3.15", - "babel-plugin-istanbul": "^6.1.1", - "chalk": "^4.0.0", - "convert-source-map": "^2.0.0", - "fast-json-stable-stringify": "^2.1.0", - "graceful-fs": "^4.2.9", - "jest-haste-map": "^29.5.0", - "jest-regex-util": "^29.4.3", - "jest-util": "^29.5.0", - "micromatch": "^4.0.4", - "pirates": "^4.0.4", - "slash": "^3.0.0", - "write-file-atomic": "^4.0.2" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-snapshot/node_modules/ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/jest-snapshot/node_modules/convert-source-map": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", - "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", - "dev": true - }, - "node_modules/jest-snapshot/node_modules/jest-haste-map": { - "version": "29.5.0", - "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.5.0.tgz", - "integrity": "sha512-IspOPnnBro8YfVYSw6yDRKh/TiCdRngjxeacCps1cQ9cgVN6+10JUcuJ1EabrgYLOATsIAigxA0rLR9x/YlrSA==", - "dev": true, - "dependencies": { - "@jest/types": "^29.5.0", - "@types/graceful-fs": "^4.1.3", - "@types/node": "*", - "anymatch": "^3.0.3", - "fb-watchman": "^2.0.0", - "graceful-fs": "^4.2.9", - "jest-regex-util": "^29.4.3", - "jest-util": "^29.5.0", - "jest-worker": "^29.5.0", - "micromatch": "^4.0.4", - "walker": "^1.0.8" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - }, - "optionalDependencies": { - "fsevents": "^2.3.2" + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" } }, - "node_modules/jest-snapshot/node_modules/jest-regex-util": { - "version": "29.4.3", - "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.4.3.tgz", - "integrity": "sha512-O4FglZaMmWXbGHSQInfXewIsd1LMn9p3ZXB/6r4FOkyhX2/iP/soMG98jGvk/A3HAN78+5VWcBGO0BJAPRh4kg==", + "node_modules/jest-runtime/node_modules/jest-get-type": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-27.5.1.tgz", + "integrity": "sha512-2KY95ksYSaK7DMBWQn6dQz3kqAf3BB64y2udeG+hv4KfSOb9qwcYQstTJc1KCbsix+wLZWZYN8t7nwX3GOBLRw==", "dev": true, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" } }, - "node_modules/jest-snapshot/node_modules/jest-worker": { - "version": "29.5.0", - "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.5.0.tgz", - "integrity": "sha512-NcrQnevGoSp4b5kg+akIpthoAFHxPBcb5P6mYPY0fUNT+sSvmtu6jlkEle3anczUKIKEbMxFimk9oTP/tpIPgA==", + "node_modules/jest-runtime/node_modules/jest-matcher-utils": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-27.5.1.tgz", + "integrity": "sha512-z2uTx/T6LBaCoNWNFWwChLBKYxTMcGBRjAt+2SbP929/Fflb9aa5LGma654Rz8z9HLxsrUaYzxE9T/EFIL/PAw==", "dev": true, "dependencies": { - "@types/node": "*", - "jest-util": "^29.5.0", - "merge-stream": "^2.0.0", - "supports-color": "^8.0.0" + "chalk": "^4.0.0", + "jest-diff": "^27.5.1", + "jest-get-type": "^27.5.1", + "pretty-format": "^27.5.1" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" } }, - "node_modules/jest-snapshot/node_modules/pretty-format": { - "version": "29.5.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.5.0.tgz", - "integrity": "sha512-V2mGkI31qdttvTFX7Mt4efOqHXqJWMu4/r66Xh3Z3BwZaPfPJgp6/gbwoujRpPUtfEF6AUUWx3Jim3GCw5g/Qw==", + "node_modules/jest-runtime/node_modules/jest-message-util": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-27.5.1.tgz", + "integrity": "sha512-rMyFe1+jnyAAf+NHwTclDz0eAaLkVDdKVHHBFWsBWHnnh5YeJMNWWsv7AbFYXfK3oTqvL7VTWkhNLu1jX24D+g==", "dev": true, "dependencies": { - "@jest/schemas": "^29.4.3", - "ansi-styles": "^5.0.0", - "react-is": "^18.0.0" + "@babel/code-frame": "^7.12.13", + "@jest/types": "^27.5.1", + "@types/stack-utils": "^2.0.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "micromatch": "^4.0.4", + "pretty-format": "^27.5.1", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" } }, - "node_modules/jest-snapshot/node_modules/react-is": { - "version": "18.2.0", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", - "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", - "dev": true - }, - "node_modules/jest-snapshot/node_modules/supports-color": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", - "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "node_modules/jest-runtime/node_modules/jest-mock": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-27.5.1.tgz", + "integrity": "sha512-K4jKbY1d4ENhbrG2zuPWaQBvDly+iZ2yAW+T1fATN78hc0sInwn7wZB8XtlNnvHug5RMwV897Xm4LqmPM4e2Og==", "dev": true, "dependencies": { - "has-flag": "^4.0.0" + "@jest/types": "^27.5.1", + "@types/node": "*" }, "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/supports-color?sponsor=1" + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" } }, - "node_modules/jest-snapshot/node_modules/write-file-atomic": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.2.tgz", - "integrity": "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==", + "node_modules/jest-runtime/node_modules/jest-snapshot": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-27.5.1.tgz", + "integrity": "sha512-yYykXI5a0I31xX67mgeLw1DZ0bJB+gpq5IpSuCAoyDi0+BhgU/RIrL+RTzDmkNTchvDFWKP8lp+w/42Z3us5sA==", "dev": true, "dependencies": { - "imurmurhash": "^0.1.4", - "signal-exit": "^3.0.7" + "@babel/core": "^7.7.2", + "@babel/generator": "^7.7.2", + "@babel/plugin-syntax-typescript": "^7.7.2", + "@babel/traverse": "^7.7.2", + "@babel/types": "^7.0.0", + "@jest/transform": "^27.5.1", + "@jest/types": "^27.5.1", + "@types/babel__traverse": "^7.0.4", + "@types/prettier": "^2.1.5", + "babel-preset-current-node-syntax": "^1.0.0", + "chalk": "^4.0.0", + "expect": "^27.5.1", + "graceful-fs": "^4.2.9", + "jest-diff": "^27.5.1", + "jest-get-type": "^27.5.1", + "jest-haste-map": "^27.5.1", + "jest-matcher-utils": "^27.5.1", + "jest-message-util": "^27.5.1", + "jest-util": "^27.5.1", + "natural-compare": "^1.4.0", + "pretty-format": "^27.5.1", + "semver": "^7.3.2" }, "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" } }, - "node_modules/jest-util": { - "version": "29.5.0", - "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.5.0.tgz", - "integrity": "sha512-RYMgG/MTadOr5t8KdhejfvUU82MxsCu5MF6KuDUHl+NuwzUt+Sm6jJWxTJVrDR1j5M/gJVCPKQEpWXY+yIQ6lQ==", + "node_modules/jest-runtime/node_modules/jest-util": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-27.5.1.tgz", + "integrity": "sha512-Kv2o/8jNvX1MQ0KGtw480E/w4fBCDOnH6+6DmeKi6LZUIlKA5kwY0YNdlzaWTiVgxqAqik11QyxDOKk543aKXw==", "dev": true, "dependencies": { - "@jest/types": "^29.5.0", + "@jest/types": "^27.5.1", "@types/node": "*", "chalk": "^4.0.0", "ci-info": "^3.2.0", @@ -7788,7 +7889,20 @@ "picomatch": "^2.2.3" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/jest-serializer": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-serializer/-/jest-serializer-27.5.1.tgz", + "integrity": "sha512-jZCyo6iIxO1aqUxpuBlwTDMkzOAJS4a3eYz3YzgxxVQFwLeSA7Jfq5cbqCY+JLvTDrWirgusI/0KwxKMgrdf7w==", + "dev": true, + "dependencies": { + "@types/node": "*", + "graceful-fs": "^4.2.9" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" } }, "node_modules/jest-validate": { @@ -7943,6 +8057,14 @@ "url": "https://github.com/chalk/supports-color?sponsor=1" } }, + "node_modules/js-tiktoken": { + "version": "1.0.15", + "resolved": "https://registry.npmjs.org/js-tiktoken/-/js-tiktoken-1.0.15.tgz", + "integrity": "sha512-65ruOWWXDEZHHbAo7EjOcNxOGasQKbL4Fq3jEr2xsCqSsoOo6VVSqzWQb6PRIqypFSDcma4jO90YP0w5X8qVXQ==", + "dependencies": { + "base64-js": "^1.5.1" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -8065,6 +8187,56 @@ "node": ">=6" } }, + "node_modules/langsmith": { + "version": "0.2.8", + "resolved": "https://registry.npmjs.org/langsmith/-/langsmith-0.2.8.tgz", + "integrity": "sha512-wKVNZoYtd8EqQWUEsfDZlZ77rH7vVqgNtONXRwynUp7ZFMFUIPhSlqE9pXqrmYPE8ZTBFj7diag2lFgUuaOEKw==", + "dependencies": { + "@types/uuid": "^10.0.0", + "commander": "^10.0.1", + "p-queue": "^6.6.2", + "p-retry": "4", + "semver": "^7.6.3", + "uuid": "^10.0.0" + }, + "peerDependencies": { + "openai": "*" + }, + "peerDependenciesMeta": { + "openai": { + "optional": true + } + } + }, + "node_modules/langsmith/node_modules/@types/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==" + }, + "node_modules/langsmith/node_modules/p-retry": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-4.6.2.tgz", + "integrity": "sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ==", + "dependencies": { + "@types/retry": "0.12.0", + "retry": "^0.13.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/langsmith/node_modules/uuid": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/language-subtag-registry": { "version": "0.3.22", "resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.22.tgz", @@ -8141,12 +8313,6 @@ "integrity": "sha512-N8XRTIMMqqDgSy4VLKPnJ/+hpGZN+PHQiJnSenYqPaVV/NCqEogTnAdZLQiGKhxX+JCs8waWq2t1XHWKOmlY8g==", "dev": true }, - "node_modules/lodash.memoize": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", - "integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==", - "dev": true - }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", @@ -8190,9 +8356,9 @@ } }, "node_modules/make-dir/node_modules/semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, "bin": { "semver": "bin/semver.js" @@ -8202,7 +8368,9 @@ "version": "1.3.6", "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", - "dev": true + "dev": true, + "optional": true, + "peer": true }, "node_modules/makeerror": { "version": "1.0.12", @@ -8229,12 +8397,12 @@ } }, "node_modules/micromatch": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", - "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", "dev": true, "dependencies": { - "braces": "^3.0.2", + "braces": "^3.0.3", "picomatch": "^2.3.1" }, "engines": { @@ -8288,6 +8456,14 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, + "node_modules/mustache": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/mustache/-/mustache-4.2.0.tgz", + "integrity": "sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==", + "bin": { + "mustache": "bin/mustache" + } + }, "node_modules/natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", @@ -8523,6 +8699,14 @@ "node": ">= 0.8.0" } }, + "node_modules/p-finally": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", + "integrity": "sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==", + "engines": { + "node": ">=4" + } + }, "node_modules/p-limit": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-4.0.0.tgz", @@ -8579,6 +8763,21 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/p-queue": { + "version": "6.6.2", + "resolved": "https://registry.npmjs.org/p-queue/-/p-queue-6.6.2.tgz", + "integrity": "sha512-RwFpb72c/BhQLEXIZ5K2e+AhgNVmIejGlTgiB9MzZ0e93GRvqZ7uSi0dvRF7/XIXDeNkra2fNHBxTyPDGySpjQ==", + "dependencies": { + "eventemitter3": "^4.0.4", + "p-timeout": "^3.2.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/p-retry": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-5.1.2.tgz", @@ -8594,6 +8793,17 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/p-timeout": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-3.2.0.tgz", + "integrity": "sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg==", + "dependencies": { + "p-finally": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/p-try": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", @@ -8996,6 +9206,15 @@ "node": ">=4" } }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, "node_modules/resolve.exports": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-1.1.1.tgz", @@ -9094,13 +9313,9 @@ } }, "node_modules/semver": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", - "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", - "dev": true, - "dependencies": { - "lru-cache": "^6.0.0" - }, + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", "bin": { "semver": "bin/semver.js" }, @@ -9108,24 +9323,6 @@ "node": ">=10" } }, - "node_modules/semver/node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/semver/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true - }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -9531,89 +9728,63 @@ "node": ">=8" } }, - "node_modules/ts-jest": { - "version": "27.1.5", - "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-27.1.5.tgz", - "integrity": "sha512-Xv6jBQPoBEvBq/5i2TeSG9tt/nqkbpcurrEG1b+2yfBrcJelOZF9Ml6dmyMh7bcW9JyFbRYpR5rxROSlBLTZHA==", + "node_modules/ts-node": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", "dev": true, - "dependencies": { - "bs-logger": "0.x", - "fast-json-stable-stringify": "2.x", - "jest-util": "^27.0.0", - "json5": "2.x", - "lodash.memoize": "4.x", - "make-error": "1.x", - "semver": "7.x", - "yargs-parser": "20.x" + "optional": true, + "peer": true, + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" }, "bin": { - "ts-jest": "cli.js" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" }, "peerDependencies": { - "@babel/core": ">=7.0.0-beta.0 <8", - "@types/jest": "^27.0.0", - "babel-jest": ">=27.0.0 <28", - "jest": "^27.0.0", - "typescript": ">=3.8 <5.0" + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" }, "peerDependenciesMeta": { - "@babel/core": { - "optional": true - }, - "@types/jest": { - "optional": true - }, - "babel-jest": { + "@swc/core": { "optional": true }, - "esbuild": { + "@swc/wasm": { "optional": true } } }, - "node_modules/ts-jest/node_modules/@jest/types": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-27.5.1.tgz", - "integrity": "sha512-Cx46iJ9QpwQTjIdq5VJu2QTMMs3QlEjI0x1QbBP5W1+nMzyc2XmimiRR/CbX9TO0cPTeUlxWMOu8mslYsJ8DEw==", - "dev": true, - "dependencies": { - "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^3.0.0", - "@types/node": "*", - "@types/yargs": "^16.0.0", - "chalk": "^4.0.0" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/ts-jest/node_modules/@types/yargs": { - "version": "16.0.5", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-16.0.5.tgz", - "integrity": "sha512-AxO/ADJOBFJScHbWhq2xAhlWP24rY4aCEG/NFaMvbT3X2MgRsLjhjQwsn0Zi5zn0LG9jUhCCZMeX9Dkuw6k+vQ==", - "dev": true, - "dependencies": { - "@types/yargs-parser": "*" - } - }, - "node_modules/ts-jest/node_modules/jest-util": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-27.5.1.tgz", - "integrity": "sha512-Kv2o/8jNvX1MQ0KGtw480E/w4fBCDOnH6+6DmeKi6LZUIlKA5kwY0YNdlzaWTiVgxqAqik11QyxDOKk543aKXw==", + "node_modules/ts-node/node_modules/acorn-walk": { + "version": "8.3.4", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", + "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", "dev": true, + "optional": true, + "peer": true, "dependencies": { - "@jest/types": "^27.5.1", - "@types/node": "*", - "chalk": "^4.0.0", - "ci-info": "^3.2.0", - "graceful-fs": "^4.2.9", - "picomatch": "^2.2.3" + "acorn": "^8.11.0" }, "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + "node": ">=0.4.0" } }, "node_modules/tsconfig-paths": { @@ -9670,6 +9841,25 @@ "typescript": ">=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta" } }, + "node_modules/tsx": { + "version": "4.19.2", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.19.2.tgz", + "integrity": "sha512-pOUl6Vo2LUq/bSa8S5q7b91cgNSjctn9ugq/+Mvow99qW6x/UZYwzxy/3NmqoT66eHYfCVvFvACC58UBPFf28g==", + "dev": true, + "dependencies": { + "esbuild": "~0.23.0", + "get-tsconfig": "^4.7.5" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, "node_modules/tunnel": { "version": "0.0.6", "resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz", @@ -9723,16 +9913,16 @@ } }, "node_modules/typescript": { - "version": "4.9.5", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", - "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.2.tgz", + "integrity": "sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg==", "dev": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" }, "engines": { - "node": ">=4.2.0" + "node": ">=14.17" } }, "node_modules/unbox-primitive": { @@ -9817,6 +10007,14 @@ "uuid": "dist/bin/uuid" } }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true, + "optional": true, + "peer": true + }, "node_modules/v8-to-istanbul": { "version": "8.1.1", "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-8.1.1.tgz", @@ -9984,9 +10182,9 @@ } }, "node_modules/word-wrap": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", - "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", "dev": true, "engines": { "node": ">=0.10.0" @@ -10101,6 +10299,17 @@ "node": ">=10" } }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true, + "optional": true, + "peer": true, + "engines": { + "node": ">=6" + } + }, "node_modules/yocto-queue": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.0.0.tgz", @@ -10111,6 +10320,22 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "node_modules/zod": { + "version": "3.23.8", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.23.8.tgz", + "integrity": "sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-to-json-schema": { + "version": "3.23.5", + "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.23.5.tgz", + "integrity": "sha512-5wlSS0bXfF/BrL4jPAbz9da5hDlDptdEppYfe+x4eIJ7jioqKG9uUxOwPzqof09u/XeVdrgFu29lZi+8XNDJtA==", + "peerDependencies": { + "zod": "^3.23.3" + } } } } diff --git a/package.json b/package.json index 954ad015..bbd81973 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,8 @@ "@actions/core": "^1.10.0", "@actions/github": "^5.1.1", "@dqbd/tiktoken": "^1.0.7", + "@langchain/core": "^0.3.19", + "@langchain/google-genai": "^0.1.4", "@octokit/action": "^6.0.4", "@octokit/plugin-retry": "^4.1.3", "@octokit/plugin-throttling": "^6.1.0", @@ -38,7 +40,6 @@ "p-retry": "^5.1.2" }, "devDependencies": { - "@jest/globals": "^29.5.0", "@types/node": "^20.4.2", "@typescript-eslint/eslint-plugin": "^5.59.6", "@typescript-eslint/parser": "^5.59.6", @@ -53,7 +54,7 @@ "jest": "^27.2.5", "js-yaml": "^4.1.0", "prettier": "2.8.8", - "ts-jest": "^27.1.2", - "typescript": "^4.9.5" + "tsx": "^4.19.2", + "typescript": "^5.7.2" } } diff --git a/src/bot.ts b/src/bot.ts index 4323abcf..f0d90a30 100644 --- a/src/bot.ts +++ b/src/bot.ts @@ -1,70 +1,133 @@ -import './fetch-polyfill' - -import {info, error, setFailed, warning} from '@actions/core' -import pRetry from 'p-retry' -import {Options} from './options' +import {info, warning, error} from '@actions/core' import {EragAPI} from './erag' +import {type Options} from './options' +import {ChatGoogleGenerativeAI} from '@langchain/google-genai' +import {PromptTemplate} from '@langchain/core/prompts' +import {StringOutputParser} from '@langchain/core/output_parsers' +import {Finding, FileInfo, PRSummaryInput} from './types' -export class Bot { - private readonly api: EragAPI | null = null +const PR_SUMMARY_TEMPLATE = `You are a code review assistant. Analyze this pull request and provide a clear, concise summary. This summary will be provided as a comment on the pull request and also used for context during the code review. - private readonly options: Options +Title: {title} +Description: {description} - constructor(options: Options) { - this.options = options - if (process.env.ERAG_ACCESS_TOKEN) { - this.api = new EragAPI(options.eragBaseUrl, options.model, options.eragProjectName, process.env.ERAG_ACCESS_TOKEN) - } else { - const err = "Unable to initialize the ERAG API, 'ERAG_ACCESS_TOKEN' environment variable is not available" - throw new Error(err) - } +Changes: +{changes} + +Provide a summary covering: +1. The main purpose of the changes +2. Key components modified +3. Notable implementation details +4. Anything specific that the code reviewer should pay special attention to + +Keep the summary focused and technical. Don't include code snippets, pleasantries, or unnecessary text.` + +const REVIEW_TEMPLATE = `You are a code review assistant. Analyze the following changes and provide specific, actionable feedback. + +Title: {title} +Description: {description} + +PR Summary: +{summary} + +Changes to Review: +{files} + +For each potential issue found, provide: +1. A clear description of the concern. Don't include code snippets, only explain the issue and a potential solution in a few sentences. +2. The specific file and location +3. Any symbols (functions, classes, variables) that are relevant to the issue + +IMPORTANT: Your response MUST be a valid JSON array of findings, each with these fields: +- description: string (clear explanation of the issue) +- file: string (full file path as appears in the input) +- lines?: { start: number, end: number } (location of the issue in the updated file) + +Example response format: +[ + { + "description": "The error handling could be improved by logging the error message.", + "file": "src/handler.ts", + "lines": { "start": 15, "end": 20 } } +] - chat = async (message: string): Promise => { - let res: string = '' - try { - res = await this.chat_(message) - return res - } catch (e: any) { - warning(`Failed to chat: ${e.message}, backtrace: ${e.stack}`) - return res +If there are no issues found, return an empty array. + +Focus on: +- Potential bugs, regressions +- Maintainability issues +- Performance issues +- Simplification opportunities (less code lines the better) + +Keep suggestions specific and actionable. Don't include general comments about coding style unless they impact maintainability.` + +export class Bot { + private readonly api: EragAPI + private readonly gemini: ChatGoogleGenerativeAI + + constructor(options: Options) { + // Initialize Gemini + if (!process.env.GOOGLE_API_KEY) { + throw new Error("Unable to initialize Gemini, 'GOOGLE_API_KEY' environment variable is not available") + } + if (!process.env.ERAG_ACCESS_TOKEN) { + throw new Error("Unable to initialize the ERAG API, 'ERAG_ACCESS_TOKEN' environment variable is not available") } + + this.gemini = new ChatGoogleGenerativeAI({ + modelName: 'gemini-1.5-flash-latest', + maxOutputTokens: 1000, + apiKey: process.env.GOOGLE_API_KEY, + temperature: 0.4 + }) + + // Initialize ERAG + this.api = new EragAPI(options.eragBaseUrl, 'bedrock-claude3.5-sonnet', options.eragProjectName, process.env.ERAG_ACCESS_TOKEN) } - private readonly chat_ = async (message: string): Promise => { - if (!message) { - return '' - } + async reviewBatch(summary: string, files: FileInfo[]): Promise { + try { + const prompt = REVIEW_TEMPLATE.replace('{summary}', summary).replace( + '{files}', + files.map(file => `File: ${file.filename}\nPatch:\n${file.patch}`).join('\n---\n') + ) - if (!this.api) { - setFailed('The ERAG API is not initialized') - return '' + const response = await this.api.sendMessage(prompt) + try { + // Extract JSON array from response using regex + const match = response.match(/\[.*\]/s) + if (!match) { + error(`No JSON array found in response: ${response}`) + throw new Error('No JSON array found in response') + } + return JSON.parse(match[0]) as Finding[] + } catch (parseError) { + error(`Failed to parse response as JSON: ${response}`) + throw new Error(`Invalid JSON response: ${parseError}`) + } + } catch (e) { + throw new Error(`Failed to review batch: ${e}`) } + } - const start = Date.now() - let response: string = '' + async summarizePR(input: PRSummaryInput): Promise { try { - if (this.options.debug) { - info('::group::Sending message to erag') - info(`\n\n ${message}\n\n`) - info('::endgroup::') - } + // Create and run the chain + const prompt = PromptTemplate.fromTemplate(PR_SUMMARY_TEMPLATE) + const chain = prompt.pipe(this.gemini).pipe(new StringOutputParser()) - response = await pRetry(() => this.api!.sendMessage(message), { - retries: this.options.eragRetries + const summary = await chain.invoke({ + title: input.title, + description: input.description, + changes: input.changes.map(change => `File: ${change.filename}\nPatch:\n${change.patch}`).join('\n---\n') }) - if (this.options.debug) { - info('::group::Received response from erag') - info(`\n\n ${response}\n\n`) - info('::endgroup::') - } - } catch (err: any) { - error(`Failed to send message to erag: ${err}`) + info(`Generated PR Summary: ${summary}`) + return summary + } catch (e: any) { + warning(`Failed to generate PR summary: ${e.message}, backtrace: ${e.stack}`) + throw e } - const end = Date.now() - info(`erag sendMessage (including retries) response time: ${end - start} ms`) - - return response } } diff --git a/src/main.ts b/src/main.ts index f2486b5c..76999c7e 100644 --- a/src/main.ts +++ b/src/main.ts @@ -2,7 +2,7 @@ import {info, setFailed, warning} from '@actions/core' import {Bot} from './bot' import {Options} from './options' import {Prompts} from './prompts' -import {codeReview} from './review' +import {codeReview} from './review_old' import {handleReviewComment} from './review-comment' async function run(): Promise { diff --git a/src/prompts.ts b/src/prompts.ts index 8f9aeac1..58c11adb 100644 --- a/src/prompts.ts +++ b/src/prompts.ts @@ -1,9 +1,7 @@ import {type Inputs} from './inputs' export class Prompts { - systemMessage = `$system_message - -` + systemMessage = `$system_message\n` summarize = ` Provide your final response in markdown with the following content: @@ -307,38 +305,19 @@ $comment \`\`\` ` - renderSummarizeFileDiff(inputs: Inputs): string { - const prompt = this.systemMessage + this.summarizeFileDiff - return inputs.render(prompt) - } - - renderSummarizeChangesets(inputs: Inputs): string { - const prompt = this.systemMessage + this.summarizeChangesets - return inputs.render(prompt) - } - - renderSummarize(inputs: Inputs): string { - const prompt = this.systemMessage + this.summarizePrefix + this.summarize - return inputs.render(prompt) - } - - renderSummarizeShort(inputs: Inputs): string { - const prompt = this.systemMessage + this.summarizePrefix + this.summarizeShort - return inputs.render(prompt) - } - - renderSummarizeReleaseNotes(inputs: Inputs): string { - const prompt = this.systemMessage + this.summarizePrefix + this.summarizeReleaseNotes - return inputs.render(prompt) - } - - renderComment(inputs: Inputs): string { - const prompt = this.systemMessage + this.comment - return inputs.render(prompt) - } - - renderReviewFileDiff(inputs: Inputs): string { - const prompt = this.systemMessage + this.reviewFileDiff - return inputs.render(prompt) - } + renderComment = (inputs: Inputs) => ` +You are a code review assistant. Continue this code review discussion thread. + +Context: +${inputs.diff} + +Previous comments: +${inputs.commentChain} + +Latest comment: +${inputs.comment} + +Provide a helpful, technical response that moves the discussion forward. If the discussion seems resolved, say so. +If you need more context, mention specific files or symbols that would help you understand better. +` } diff --git a/src/review.ts b/src/review.ts index a5fbea77..01424b44 100644 --- a/src/review.ts +++ b/src/review.ts @@ -1,871 +1,245 @@ -import {error, info, warning} from '@actions/core' -// eslint-disable-next-line camelcase -import {context as github_context} from '@actions/github' +import {Bot} from './bot' +import {Commenter} from './commenter' +import {info, warning, error} from '@actions/core' import pLimit from 'p-limit' -import {type Bot} from './bot' -import { - Commenter, - COMMENT_REPLY_TAG, - RAW_SUMMARY_END_TAG, - RAW_SUMMARY_START_TAG, - SHORT_SUMMARY_END_TAG, - SHORT_SUMMARY_START_TAG, - SUMMARIZE_TAG -} from './commenter' -import {Inputs} from './inputs' +import {Finding, PRContext, FileInfo, PullRequest, PRSummaryInput, FileBatch} from './types' +import {context as githubContext} from '@actions/github' import {octokit} from './octokit' -import {type Options} from './options' -import {type Prompts} from './prompts' -import {getTokenCount} from './tokenizer' -import {execFile} from 'child_process' -import {promisify} from 'util' -import path from 'path' +import {Options} from './options' -// eslint-disable-next-line camelcase -const context = github_context -const repo = context.repo +const repo = githubContext.repo -const rgPath = path.join(__dirname, './rg') -const execFileAsync = promisify(execFile) +const EXCLUDED_FILES = [ + // Package management + 'package-lock.json', + 'yarn.lock', + 'pnpm-lock.yaml', + // Generated files + '*.generated.*', + '*.g.*', + // Other + '.DS_Store' +] -const ignoreKeyword = '@erag: ignore' +const MIN_BATCH_LINES = 100 -export async function codeReview(reviewBot: Bot, options: Options, prompts: Prompts): Promise { - const commenter: Commenter = new Commenter() +// Main entry point +export async function codeReview(reviewBot: Bot): Promise { + const commenter = new Commenter() - const eragConcurrencyLimit = pLimit(options.eragConcurrencyLimit) + if (!isPullRequestEvent()) return + const pullRequest = githubContext.payload.pull_request! - if (!isPullRequestEvent()) { - return - } - const pullRequest = context.payload.pull_request! - - const inputs = initializeInputs(pullRequest, options, commenter) - if (inputs.description.includes(ignoreKeyword)) { - info('Skipped: description contains ignore_keyword') - return - } - - const {existingSummarizeCmtBody, existingCommitIdsBlock} = await getExistingSummarizeComment(pullRequest, commenter, inputs) - - const highestReviewedCommitId = await determineHighestReviewedCommitId(existingCommitIdsBlock, pullRequest, commenter) - - const {files, commits} = await fetchDiffFiles(highestReviewedCommitId, pullRequest) - if (!files) { - warning('Skipped: files is null') - return - } + // Step 1: Get PR changes and generate summary + const files = await getChangedFiles(pullRequest) + const prContext = await generatePRContext(reviewBot, files, pullRequest) - const {filterSelectedFiles, filterIgnoredFiles} = filterFilesByPath(files, options) - if (filterSelectedFiles.length === 0) { - warning('Skipped: filterSelectedFiles is null') - return - } + // Step 2: Generate initial findings + const findings = await generateFindings(reviewBot, prContext) - const filesAndChanges = await getFilesAndChanges(filterSelectedFiles, pullRequest, options) - if (filesAndChanges.length === 0) { - error('Skipped: no files to review') + // For testing locally + if (process.env.NODE_ENV === 'test') { + info(`Generated Findings:${JSON.stringify(findings, null, 2)}`) return } - let statusMsg = `
-Commits -Files that changed from the base of the PR and between ${highestReviewedCommitId} and ${pullRequest.head.sha} commits. -
-${ - filesAndChanges.length > 0 - ? ` -
-Files selected (${filesAndChanges.length}) - -* ${filesAndChanges.map(([filename, , , patches]) => `${filename} (${patches.length})`).join('\n* ')} -
-` - : '' -} -${ - filterIgnoredFiles.length > 0 - ? ` -
-Files ignored due to filter (${filterIgnoredFiles.length}) + // Step 3: Validate and enhance findings + const validatedFindings = await validateFindings(reviewBot, findings, prContext) -* ${filterIgnoredFiles.map(file => file.filename).join('\n* ')} + // Step 4: Check for duplicates and existing discussions + const newFindings = await filterDuplicateFindings(validatedFindings, pullRequest, commenter) -
-` - : '' + // Step 5: Post review comments + await postReviewComments(newFindings, pullRequest, commenter) } -` - - // update the existing comment with in progress status - const inProgressSummarizeCmt = commenter.addInProgressStatus(existingSummarizeCmtBody, statusMsg) - - // add in progress status to the summarize comment - await commenter.comment(`${inProgressSummarizeCmt}`, SUMMARIZE_TAG, 'replace') - const summariesFailed: string[] = [] - - const doSummary = async (filename: string, fileContent: string, fileDiff: string): Promise<[string, string, boolean, string[]] | null> => { - info(`summarize: ${filename}`) - const ins = inputs.clone() - if (fileDiff.length === 0) { - warning(`summarize: file_diff is empty, skip ${filename}`) - summariesFailed.push(`${filename} (empty diff)`) - return null +function isExcludedFile(filename: string): boolean { + return EXCLUDED_FILES.some(pattern => { + if (pattern.endsWith('/')) { + return filename.startsWith(pattern) } - - ins.filename = filename - ins.fileDiff = fileDiff - - // render prompt based on inputs so far - const summarizePrompt = prompts.renderSummarizeFileDiff(ins) - const tokens = getTokenCount(summarizePrompt) - - if (tokens > options.tokenLimits.requestTokens) { - info(`summarize: diff tokens exceeds limit, skip ${filename}`) - summariesFailed.push(`${filename} (diff tokens exceeds limit)`) - return null + if (pattern.includes('*')) { + const regex = new RegExp(`^${pattern.replace(/\*/g, '.*')}$`) + return regex.test(filename) } + return filename === pattern + }) +} - // summarize content - try { - const summarizeResp = await reviewBot.chat(summarizePrompt) - - if (summarizeResp === '') { - info('summarize: nothing obtained from erag') - summariesFailed.push(`${filename} (nothing obtained from erag)`) - return null - } else { - // parse the comment to look for triage classification - // Format is : [TRIAGE]: - // if the change needs review return true, else false - let needsReview = false - let summary = summarizeResp - let symbols: string[] = [] - - const triageRegex = /\[TRIAGE\]:\s*(NEEDS_REVIEW|APPROVED)/ - const triageMatch = summarizeResp.match(triageRegex) - if (triageMatch != null) { - const triage = triageMatch[1] - needsReview = triage === 'NEEDS_REVIEW' - // remove this line from the comment - summary = summarizeResp.replace(triageRegex, '').trim() - info(`filename: ${filename}, triage: ${triage}`) - } - - // Symbols to search for in the codebase to give more context to the LLM - const symbolsRegex = /SYMBOLS:\s*(\[.*?\])/ - const symbolsMatch = summarizeResp.match(symbolsRegex) - if (symbolsMatch != null) { - const symbolsStr = symbolsMatch[1] - symbols = symbolsStr - .replace('[', '') - .replace(']', '') - .split(',') - .map(symbol => symbol.trim()) - .filter(symbol => symbol !== '') - - summary = summary.replace(symbolsRegex, '').trim() - } - - return [filename, summary, needsReview, symbols] - } - } catch (e: any) { - warning(`summarize: error from erag: ${e as string}`) - summariesFailed.push(`${filename} (error from erag: ${e as string})})`) - return null - } - } +async function getChangedFiles(pullRequest: any): Promise { + const {data: files} = await octokit.pulls.listFiles({ + owner: repo.owner, + repo: repo.repo, + // eslint-disable-next-line camelcase + pull_number: pullRequest.number + }) - const summaryPromises = [] - const skippedFiles = [] - for (const [filename, fileContent, fileDiff] of filesAndChanges) { - if (options.maxFiles <= 0 || summaryPromises.length < options.maxFiles) { - summaryPromises.push(eragConcurrencyLimit(async () => await doSummary(filename, fileContent, fileDiff))) - } else { - skippedFiles.push(filename) - } - } + return files + .filter(file => !isExcludedFile(file.filename)) + .map(file => ({ + filename: file.filename, + patch: file.patch + })) +} - const summaries = (await Promise.all(summaryPromises)).filter(summary => summary !== null) as Array<[string, string, boolean, string[]]> - - if (summaries.length > 0) { - const batchSize = 10 - // join summaries into one in the batches of batchSize - // and ask the bot to summarize the summaries - for (let i = 0; i < summaries.length; i += batchSize) { - const summariesBatch = summaries.slice(i, i + batchSize) - for (const [filename, summary] of summariesBatch) { - inputs.rawSummary += `--- -${filename}: ${summary} -` - } - // ask chatgpt to summarize the summaries - const summarizeResp = await reviewBot.chat(prompts.renderSummarizeChangesets(inputs)) - if (summarizeResp === '') { - warning('summarize: nothing obtained from erag') - } else { - inputs.rawSummary = summarizeResp - } - } +async function generatePRContext(bot: Bot, files: FileInfo[], pullRequest: any): Promise { + // Prepare summary input + const summaryInput: PRSummaryInput = { + title: pullRequest.title, + description: pullRequest.body || '', + changes: files.map(file => ({ + filename: file.filename, + patch: file.patch || '', + additions: file.patch?.match(/^\+/gm)?.length || 0, + deletions: file.patch?.match(/^-/gm)?.length || 0 + })) } - // final summary - const summarizeFinalResponse = await reviewBot.chat(prompts.renderSummarize(inputs)) - if (summarizeFinalResponse === '') { - info('summarize: nothing obtained from erag') - } + // Generate summary using Gemini + const summary = await bot.summarizePR(summaryInput) - if (options.disableReleaseNotes === false) { - // final release notes - const releaseNotesResponse = await reviewBot.chat(prompts.renderSummarizeReleaseNotes(inputs)) - if (releaseNotesResponse === '') { - info('release notes: nothing obtained from erag') - } else { - let message = '### Summary by Erag Reviewer\n\n' - message += releaseNotesResponse - try { - await commenter.updateDescription(pullRequest.number, message) - } catch (e: any) { - warning(`release notes: error from github: ${e.message as string}`) - } - } + return { + title: pullRequest.title, + description: pullRequest.body || '', + summary, + pullRequest, + files } - - // generate a short summary as well - const summarizeShortResponse = await reviewBot.chat(prompts.renderSummarizeShort(inputs)) - inputs.shortSummary = summarizeShortResponse - - let summarizeComment = `${summarizeFinalResponse} -${RAW_SUMMARY_START_TAG} -${inputs.rawSummary} -${RAW_SUMMARY_END_TAG} -${SHORT_SUMMARY_START_TAG} -${inputs.shortSummary} -${SHORT_SUMMARY_END_TAG}` - - statusMsg += ` -${ - skippedFiles.length > 0 - ? ` -
-Files not processed due to max files limit (${skippedFiles.length}) - -* ${skippedFiles.join('\n* ')} - -
-` - : '' } -${ - summariesFailed.length > 0 - ? ` -
-Files not summarized due to errors (${summariesFailed.length}) -* ${summariesFailed.join('\n* ')} +function createFileBatches(files: FileInfo[]): FileBatch[] { + const batches: FileBatch[] = [] + let currentBatch: FileBatch = {files: [], totalLines: 0} -
-` - : '' -} -` - - if (!options.disableReview) { - const filesAndChangesReview = filesAndChanges - .filter(([filename]) => { - const needsReview = summaries.find(([summaryFilename]) => summaryFilename === filename)?.[2] ?? true - return needsReview - }) - .map(([filename, fileContent, fileDiff, patches]) => { - const summaryEntry = summaries.find(([summaryFilename]) => summaryFilename === filename) - const symbols = summaryEntry ? summaryEntry[3] : [] - return [filename, fileContent, fileDiff, patches, symbols] as [string, string, string, [number, number, string][], string[]] - }) - - const reviewsSkipped = filesAndChanges - .filter(([filename]) => !filesAndChangesReview.some(([reviewFilename]) => reviewFilename === filename)) - .map(([filename]) => filename) - - // failed reviews array - const reviewsFailed: string[] = [] - let lgtmCount = 0 - let reviewCount = 0 - const doReview = async (filename: string, fileContent: string, patches: Array<[number, number, string]>, symbols: string[]): Promise => { - if (options.debug) { - info(`reviewing ${filename}`) - info(`symbols: ${symbols} - size: ${symbols.length}`) - } - - const symbolSearchResults = await searchSymbols(symbols) - info(`symbolSearchResults: ${symbolSearchResults}`) - - // make a copy of inputs - const ins: Inputs = inputs.clone() - ins.filename = filename - ins.symbolSearchResults = symbolSearchResults - - // calculate tokens based on inputs so far - let tokens = getTokenCount(prompts.renderReviewFileDiff(ins)) - // loop to calculate total patch tokens - let patchesToPack = 0 - for (const [, , patch] of patches) { - const patchTokens = getTokenCount(patch) - if (tokens + patchTokens > options.tokenLimits.requestTokens) { - info(`only packing ${patchesToPack} / ${patches.length} patches, tokens: ${tokens} / ${options.tokenLimits.requestTokens}`) - break - } - tokens += patchTokens - patchesToPack += 1 - } - - let patchesPacked = 0 - for (const [startLine, endLine, patch] of patches) { - // see if we can pack more patches into this request - if (patchesPacked >= patchesToPack) { - info(`unable to pack more patches into this request, packed: ${patchesPacked}, total patches: ${patches.length}, skipping.`) - if (options.debug) { - info(`prompt so far: ${prompts.renderReviewFileDiff(ins)}`) - } - break - } - patchesPacked += 1 - - let commentChain = '' - try { - const allChains = await commenter.getCommentChainsWithinRange(pullRequest.number, filename, startLine, endLine, COMMENT_REPLY_TAG) - - if (allChains.length > 0) { - info(`Found comment chains: ${allChains} for ${filename}`) - commentChain = allChains - } - } catch (e: any) { - warning(`Failed to get comments: ${e as string}, skipping. backtrace: ${e.stack as string}`) - } - // try packing comment_chain into this request - const commentChainTokens = getTokenCount(commentChain) - if (tokens + commentChainTokens > options.tokenLimits.requestTokens) { - commentChain = '' - } else { - tokens += commentChainTokens - } - - ins.patches += ` -${patch} -` - if (commentChain !== '') { - ins.patches += ` ----comment_chains--- -\`\`\` -${commentChain} -\`\`\` -` - } - - ins.patches += ` ----end_change_section--- -` - } - - if (patchesPacked > 0) { - // perform review - try { - const response = await reviewBot.chat(prompts.renderReviewFileDiff(ins)) - if (response === '') { - info('review: nothing obtained from erag') - reviewsFailed.push(`${filename} (no response)`) - return - } - // parse review - const reviews = parseReview(response, patches, options.debug) - for (const review of reviews) { - // check for LGTM - if (review.comment.includes('LGTM') || review.comment.includes('looks good to me')) { - lgtmCount += 1 - continue - } - - try { - reviewCount += 1 - await commenter.bufferReviewComment(filename, review.startLine, review.endLine, `${review.comment}`) - } catch (e: any) { - reviewsFailed.push(`${filename} comment failed (${e as string})`) - } - } - } catch (e: any) { - warning(`Failed to review: ${e as string}, skipping. backtrace: ${e.stack as string}`) - reviewsFailed.push(`${filename} (${e as string})`) - } - } else { - reviewsSkipped.push(`${filename} (diff too large)`) - } - } - - const reviewPromises = [] - for (const [filename, fileContent, , patches, symbols] of filesAndChangesReview) { - if (options.maxFiles <= 0 || reviewPromises.length < options.maxFiles) { - reviewPromises.push( - eragConcurrencyLimit(async () => { - await doReview(filename, fileContent, patches, symbols) - }) - ) - } else { - skippedFiles.push(filename) - } + for (const file of files) { + currentBatch.files.push(file) + currentBatch.totalLines += file.patch?.split('\n').length || 0 + // Start a new batch if current batch exceeds MIN_BATCH_LINES + if (currentBatch.totalLines > MIN_BATCH_LINES && currentBatch.files.length > 0) { + batches.push(currentBatch) + currentBatch = {files: [], totalLines: 0} } + } - await Promise.all(reviewPromises) - - statusMsg += ` -${ - reviewsFailed.length > 0 - ? `
-Files not reviewed due to errors (${reviewsFailed.length}) - -* ${reviewsFailed.join('\n* ')} - -
-` - : '' -} -${ - reviewsSkipped.length > 0 - ? `
-Files skipped from review due to trivial changes (${reviewsSkipped.length}) - -* ${reviewsSkipped.join('\n* ')} + // Add the last batch if it has any files + if (currentBatch.files.length > 0) { + batches.push(currentBatch) + } -
-` - : '' + return batches } -
-Review comments generated (${reviewCount + lgtmCount}) - -* Review: ${reviewCount} -* LGTM: ${lgtmCount} -
+async function generateFindings(bot: Bot, context: PRContext): Promise { + const files = await getChangedFiles(context.pullRequest) + const batches = createFileBatches(files) ---- + // Use p-limit to run batches in parallel with a concurrency limit + const limit = pLimit(10) // Process up to 10 batches at a time -
-Tips + const batchPromises = batches.map(batch => limit(async () => bot.reviewBatch(context.summary, batch.files))) -### Chat with Image description ERAG Reviewer (\`@erag\`) -- Reply on review comments left by this bot to ask follow-up questions. A review comment is a comment on a diff or a file. -- Invite the bot into a review comment chain by tagging \`@erag\` in a reply. - -### Code suggestions -- The bot may make code suggestions, but please review them carefully before committing since the line number ranges may be misaligned. -- You can edit the comment made by the bot and manually tweak the suggestion if it is slightly off. - -### Pausing incremental reviews -- Add \`@erag: ignore\` anywhere in the PR description to pause further reviews from the bot. - -
-` - // add existing_comment_ids_block with latest head sha - summarizeComment += `\n${commenter.addReviewedCommitId(existingCommitIdsBlock, pullRequest.head.sha)}` - - // post the review - await commenter.submitReview(pullRequest.number, commits[commits.length - 1].sha, statusMsg) - } - - // post the final summary comment - await commenter.comment(`${summarizeComment}`, SUMMARIZE_TAG, 'replace') + const batchResults = await Promise.all(batchPromises) + return batchResults.flat() } -async function searchSymbols(symbols: string[]): Promise { - // Uses ripgrep to search for the symbols in the codebase - let searchResults = '' - - for (const symbol of symbols) { - try { - info(`Searching for symbol ${symbol}`) - searchResults += `---${symbol}---\n` - const {stdout} = await execFileAsync(rgPath, [symbol, '-n', '-w', '.'], {maxBuffer: 1024 * 1024}) - searchResults += `${stdout.trim()}\n` - } catch (err: any) { - if (err.code === 1) { - // Ripgrep exits with code 1 if no matches are found - info(`No matches found for symbol ${symbol}`) - searchResults += `No matches found for symbol ${symbol}\n` - } else { - warning(`Error searching for symbol ${symbol}: ${err.message as string}`) - } - } - } - - return searchResults +async function validateFindings(bot: Bot, findings: Finding[], context: PRContext): Promise { + // TODO: Implement validation logic + // For now, just return the findings as-is + return findings } -async function determineHighestReviewedCommitId(existingCommitIdsBlock: string, pullRequest: any, commenter: Commenter) { - const allCommitIds = await commenter.getAllCommitIds() - let highestReviewedCommitId = '' - if (existingCommitIdsBlock !== '') { - highestReviewedCommitId = commenter.getHighestReviewedCommitId(allCommitIds, commenter.getReviewedCommitIds(existingCommitIdsBlock)) - } +async function analyzeSymbols(symbols: string[]): Promise { + // TODO: Implement symbol analysis using codebase search + return '' +} - if (highestReviewedCommitId === '' || highestReviewedCommitId === pullRequest.head.sha) { - info(`Will review from the base commit: ${pullRequest.base.sha as string}`) - highestReviewedCommitId = pullRequest.base.sha - } else { - info(`Will review from commit: ${highestReviewedCommitId}`) - } - return highestReviewedCommitId +async function validateFinding(bot: Bot, finding: Finding, symbolContext: string): Promise { + // TODO: Implement finding validation with symbol context + return true } -async function fetchDiffFiles(highestReviewedCommitId: string, pullRequest: any) { - // Fetch the diff between the highest reviewed commit and the latest commit of the PR branch - const incrementalDiff = await octokit.repos.compareCommits({ - owner: repo.owner, - repo: repo.repo, - base: highestReviewedCommitId, - head: pullRequest.head.sha - }) +async function filterDuplicateFindings(findings: Finding[], pullRequest: PullRequest, commenter: Commenter): Promise { + const existingComments = await commenter.getExistingComments(pullRequest.number) - // Fetch the diff between the target branch's base commit and the latest commit of the PR branch - const targetBranchDiff = await octokit.repos.compareCommits({ - owner: repo.owner, - repo: repo.repo, - base: pullRequest.base.sha, - head: pullRequest.head.sha + return findings.filter(finding => { + const key = `${finding.file}:${finding.lines?.start}-${finding.lines?.end}` + return !existingComments.some(comment => comment.body.includes(finding.description) || comment.path === finding.file) }) - - const incrementalFiles = incrementalDiff.data.files - const targetBranchFiles = targetBranchDiff.data.files - const commits = incrementalDiff.data.commits - - if (!incrementalFiles || !targetBranchFiles || !commits) { - warning('Skipped: files data is missing') - return {files: [], commits: []} - } - - // Get files that were changed in the last commit which are also changed compared to the PR base commit - const files = targetBranchFiles.filter(targetBranchFile => - incrementalFiles.some(incrementalFile => incrementalFile.filename === targetBranchFile.filename) - ) - - return {files, commits} -} - -async function getFilesAndChanges(filterSelectedFiles: any, pullRequest: any, options: Options) { - const githubConcurrencyLimit = pLimit(options.githubConcurrencyLimit) - // find hunks to review - const filteredFiles: Array<[string, string, string, Array<[number, number, string]>] | null> = await Promise.all( - filterSelectedFiles.map((file: any) => - githubConcurrencyLimit(async () => { - // retrieve file contents - let fileContent = '' - try { - const contents = await octokit.repos.getContent({ - owner: repo.owner, - repo: repo.repo, - path: file.filename, - ref: pullRequest.base.sha - }) - if (contents.data != null) { - if (!Array.isArray(contents.data)) { - if (contents.data.type === 'file' && contents.data.content != null) { - fileContent = Buffer.from(contents.data.content, 'base64').toString() - } - } - } - } catch (e: any) { - warning(`Failed to get file contents: ${e as string}. This is OK if it's a new file.`) - } - - let fileDiff = '' - if (file.patch != null) { - fileDiff = file.patch - } - - const patches: Array<[number, number, string]> = [] - for (const patch of splitPatch(file.patch)) { - const patchLines = patchStartEndLine(patch) - if (patchLines == null) { - continue - } - const hunks = parsePatch(patch) - if (hunks == null) { - continue - } - const hunksStr = ` ----new_hunk--- -\`\`\` -${hunks.newHunk} -\`\`\` - ----old_hunk--- -\`\`\` -${hunks.oldHunk} -\`\`\` -` - patches.push([patchLines.newHunk.startLine, patchLines.newHunk.endLine, hunksStr]) - } - if (patches.length > 0) { - return [file.filename, fileContent, fileDiff, patches] as [string, string, string, Array<[number, number, string]>] - } else { - return null - } - }) - ) - ) - - // Filter out any null results - return filteredFiles.filter(file => file !== null) as Array<[string, string, string, Array<[number, number, string]>]> } -function filterFilesByPath(files: any, options: Options) { - const filterSelectedFiles = [] - const filterIgnoredFiles = [] - for (const file of files) { - if (!options.pathFilters.check(file.filename)) { - info(`skip for excluded path: ${file.filename}`) - filterIgnoredFiles.push(file) +async function postReviewComments(findings: Finding[], pullRequest: PullRequest, commenter: Commenter): Promise { + for (const finding of findings) { + const comment = formatFindingComment(finding) + if (finding.lines) { + await commenter.createReviewComment(pullRequest.number, comment, finding.file, finding.lines.start) } else { - filterSelectedFiles.push(file) + await commenter.createIssueComment(pullRequest.number, comment) } } - return {filterSelectedFiles, filterIgnoredFiles} } -async function getExistingSummarizeComment(pullRequest: any, commenter: Commenter, inputs: Inputs) { - const existingSummarizeCmt = await commenter.findCommentWithTag(SUMMARIZE_TAG, pullRequest.number) - if (existingSummarizeCmt) { - const existingSummarizeCmtBody = existingSummarizeCmt.body - const existingCommitIdsBlock = commenter.getReviewedCommitIdsBlock(existingSummarizeCmtBody) - inputs.rawSummary = commenter.getRawSummary(existingSummarizeCmtBody) - inputs.shortSummary = commenter.getShortSummary(existingSummarizeCmtBody) - return {existingSummarizeCmtBody, existingCommitIdsBlock} +function formatFindingComment(finding: Finding): string { + const severity = { + high: '🔴', + medium: '🟡', + low: '🟢' + }[finding.severity] + + let comment = `${severity} **Code Review Finding**\n\n${finding.description}\n\n` + + if (finding.suggestion) { + comment += `**Suggestion:**\n${finding.suggestion}\n\n` } - return {existingSummarizeCmtBody: '', existingCommitIdsBlock: ''} -} -function initializeInputs(pullRequest: any, options: Options, commenter: Commenter) { - const inputs: Inputs = new Inputs() - inputs.systemMessage = options.systemMessage - inputs.title = pullRequest.title - if (pullRequest.body) { - inputs.description = commenter.getDescription(pullRequest.body) + if (finding.symbolsUsed?.length) { + comment += `**Related Symbols:**\n${finding.symbolsUsed.join(', ')}\n\n` } - return inputs + + return comment } function isPullRequestEvent(): boolean { - if (context.eventName !== 'pull_request' && context.eventName !== 'pull_request_target') { - warning(`Skipped: current event is ${context.eventName}, only support pull_request event`) + if (githubContext.eventName !== 'pull_request' && githubContext.eventName !== 'pull_request_target') { + warning(`Skipped: current event is ${githubContext.eventName}, only support pull_request event`) return false } - if (context.payload.pull_request == null) { + if (githubContext.payload.pull_request == null) { warning('Skipped: context.payload.pull_request is null') return false } return true } -function splitPatch(patch: string | null | undefined): string[] { - if (patch == null) { - return [] - } - - const pattern = /(^@@ -(\d+),(\d+) \+(\d+),(\d+) @@).*$/gm - - const result: string[] = [] - let last = -1 - let match: RegExpExecArray | null - while ((match = pattern.exec(patch)) !== null) { - if (last === -1) { - last = match.index - } else { - result.push(patch.substring(last, match.index)) - last = match.index - } - } - if (last !== -1) { - result.push(patch.substring(last)) - } - return result -} - -function patchStartEndLine(patch: string): { - oldHunk: {startLine: number; endLine: number} - newHunk: {startLine: number; endLine: number} -} | null { - const pattern = /(^@@ -(\d+),(\d+) \+(\d+),(\d+) @@)/gm - const match = pattern.exec(patch) - if (match != null) { - const oldBegin = parseInt(match[2]) - const oldDiff = parseInt(match[3]) - const newBegin = parseInt(match[4]) - const newDiff = parseInt(match[5]) - return { - oldHunk: { - startLine: oldBegin, - endLine: oldBegin + oldDiff - 1 - }, - newHunk: { - startLine: newBegin, - endLine: newBegin + newDiff - 1 +// Configure GitHub context for local testing +if (process.env.NODE_ENV === 'test') { + Object.assign(githubContext, { + eventName: 'pull_request', + payload: { + // eslint-disable-next-line camelcase + pull_request: { + number: 801, + title: 'Test PR', + body: 'Test PR description' } } - } else { - return null - } -} - -function parsePatch(patch: string): {oldHunk: string; newHunk: string} | null { - const hunkInfo = patchStartEndLine(patch) - if (hunkInfo == null) { - return null - } - - const oldHunkLines: string[] = [] - const newHunkLines: string[] = [] - - let newLine = hunkInfo.newHunk.startLine - - const lines = patch.split('\n').slice(1) // Skip the @@ line - - // Remove the last line if it's empty - if (lines[lines.length - 1] === '') { - lines.pop() - } - - // Skip annotations for the first 3 and last 3 lines - const skipStart = 3 - const skipEnd = 3 - - let currentLine = 0 - - const removalOnly = !lines.some(line => line.startsWith('+')) - - for (const line of lines) { - currentLine++ - if (line.startsWith('-')) { - oldHunkLines.push(`${line.substring(1)}`) - } else if (line.startsWith('+')) { - newHunkLines.push(`${newLine}: ${line.substring(1)}`) - newLine++ - } else { - // context line - oldHunkLines.push(`${line}`) - if (removalOnly || (currentLine > skipStart && currentLine <= lines.length - skipEnd)) { - newHunkLines.push(`${newLine}: ${line}`) - } else { - newHunkLines.push(`${line}`) - } - newLine++ - } - } - - return { - oldHunk: oldHunkLines.join('\n'), - newHunk: newHunkLines.join('\n') - } -} - -interface Review { - startLine: number - endLine: number - comment: string -} - -function parseReview(response: string, patches: Array<[number, number, string]>, debug = false): Review[] { - const reviews: Review[] = [] - - response = sanitizeResponse(response.trim()) - - const lines = response.split('\n') - const lineNumberRangeRegex = /(?:^|\s)(\d+)-(\d+):\s*$/ - const commentSeparator = '---' - - let currentStartLine: number | null = null - let currentEndLine: number | null = null - let currentComment = '' - function storeReview(): void { - if (currentStartLine !== null && currentEndLine !== null) { - const review: Review = { - startLine: currentStartLine, - endLine: currentEndLine, - comment: currentComment - } - - for (const [startLine, endLine] of patches) { - const intersectionStart = Math.max(review.startLine, startLine) - const intersectionEnd = Math.min(review.endLine, endLine) - const intersectionLength = Math.max(0, intersectionEnd - intersectionStart + 1) - - if (intersectionLength > 0) { - reviews.push(review) - info(`Stored comment for line range ${currentStartLine}-${currentEndLine}: ${currentComment.trim()}`) - break - } - } - } - } - - function sanitizeCodeBlock(comment: string, codeBlockLabel: string): string { - const codeBlockStart = `\`\`\`${codeBlockLabel}` - const codeBlockEnd = '```' - const lineNumberRegex = /^ *(\d+): /gm - - let codeBlockStartIndex = comment.indexOf(codeBlockStart) - - while (codeBlockStartIndex !== -1) { - const codeBlockEndIndex = comment.indexOf(codeBlockEnd, codeBlockStartIndex + codeBlockStart.length) - - if (codeBlockEndIndex === -1) break - - const codeBlock = comment.substring(codeBlockStartIndex + codeBlockStart.length, codeBlockEndIndex) - const sanitizedBlock = codeBlock.replace(lineNumberRegex, '') - - comment = comment.slice(0, codeBlockStartIndex + codeBlockStart.length) + sanitizedBlock + comment.slice(codeBlockEndIndex) - - codeBlockStartIndex = comment.indexOf(codeBlockStart, codeBlockStartIndex + codeBlockStart.length + sanitizedBlock.length + codeBlockEnd.length) - } - - return comment - } - - function sanitizeResponse(comment: string): string { - comment = sanitizeCodeBlock(comment, 'suggestion') - comment = sanitizeCodeBlock(comment, 'diff') - return comment - } - - for (const line of lines) { - const lineNumberRangeMatch = line.match(lineNumberRangeRegex) - - if (lineNumberRangeMatch != null) { - storeReview() - currentStartLine = parseInt(lineNumberRangeMatch[1], 10) - currentEndLine = parseInt(lineNumberRangeMatch[2], 10) - currentComment = '' - if (debug) { - info(`Found line number range: ${currentStartLine}-${currentEndLine}`) - } - continue - } - - if (line.trim() === commentSeparator) { - storeReview() - currentStartLine = null - currentEndLine = null - currentComment = '' - if (debug) { - info('Found comment separator') - } - continue - } - - if (currentStartLine !== null && currentEndLine !== null) { - currentComment += `${line}\n` - } - } - - storeReview() - - return reviews -} + }) + // For local testing with ts-node + if (require.main === module) { + const bot = new Bot({ + eragBaseUrl: 'https://erag.trilogy.com/api/v2', + eragProjectName: 'STUDYREEL' + } as Options) + // eslint-disable-next-line github/no-then + codeReview(bot).catch(error) + } +} +/* +local run: +$env:NODE_ENV="test" +$env:GITHUB_ACTION="testaction" +$env:GITHUB_REPOSITORY="trilogy-group/crossover-client-extension" +$env:GITHUB_TOKEN="..." +$env:GOOGLE_API_KEY="..." +$env:ERAG_ACCESS_TOKEN="..." +npx tsx src/review.ts +*/ diff --git a/src/review_old.ts b/src/review_old.ts new file mode 100644 index 00000000..a5fbea77 --- /dev/null +++ b/src/review_old.ts @@ -0,0 +1,871 @@ +import {error, info, warning} from '@actions/core' +// eslint-disable-next-line camelcase +import {context as github_context} from '@actions/github' +import pLimit from 'p-limit' +import {type Bot} from './bot' +import { + Commenter, + COMMENT_REPLY_TAG, + RAW_SUMMARY_END_TAG, + RAW_SUMMARY_START_TAG, + SHORT_SUMMARY_END_TAG, + SHORT_SUMMARY_START_TAG, + SUMMARIZE_TAG +} from './commenter' +import {Inputs} from './inputs' +import {octokit} from './octokit' +import {type Options} from './options' +import {type Prompts} from './prompts' +import {getTokenCount} from './tokenizer' +import {execFile} from 'child_process' +import {promisify} from 'util' +import path from 'path' + +// eslint-disable-next-line camelcase +const context = github_context +const repo = context.repo + +const rgPath = path.join(__dirname, './rg') +const execFileAsync = promisify(execFile) + +const ignoreKeyword = '@erag: ignore' + +export async function codeReview(reviewBot: Bot, options: Options, prompts: Prompts): Promise { + const commenter: Commenter = new Commenter() + + const eragConcurrencyLimit = pLimit(options.eragConcurrencyLimit) + + if (!isPullRequestEvent()) { + return + } + const pullRequest = context.payload.pull_request! + + const inputs = initializeInputs(pullRequest, options, commenter) + if (inputs.description.includes(ignoreKeyword)) { + info('Skipped: description contains ignore_keyword') + return + } + + const {existingSummarizeCmtBody, existingCommitIdsBlock} = await getExistingSummarizeComment(pullRequest, commenter, inputs) + + const highestReviewedCommitId = await determineHighestReviewedCommitId(existingCommitIdsBlock, pullRequest, commenter) + + const {files, commits} = await fetchDiffFiles(highestReviewedCommitId, pullRequest) + if (!files) { + warning('Skipped: files is null') + return + } + + const {filterSelectedFiles, filterIgnoredFiles} = filterFilesByPath(files, options) + if (filterSelectedFiles.length === 0) { + warning('Skipped: filterSelectedFiles is null') + return + } + + const filesAndChanges = await getFilesAndChanges(filterSelectedFiles, pullRequest, options) + if (filesAndChanges.length === 0) { + error('Skipped: no files to review') + return + } + + let statusMsg = `
+Commits +Files that changed from the base of the PR and between ${highestReviewedCommitId} and ${pullRequest.head.sha} commits. +
+${ + filesAndChanges.length > 0 + ? ` +
+Files selected (${filesAndChanges.length}) + +* ${filesAndChanges.map(([filename, , , patches]) => `${filename} (${patches.length})`).join('\n* ')} +
+` + : '' +} +${ + filterIgnoredFiles.length > 0 + ? ` +
+Files ignored due to filter (${filterIgnoredFiles.length}) + +* ${filterIgnoredFiles.map(file => file.filename).join('\n* ')} + +
+` + : '' +} +` + + // update the existing comment with in progress status + const inProgressSummarizeCmt = commenter.addInProgressStatus(existingSummarizeCmtBody, statusMsg) + + // add in progress status to the summarize comment + await commenter.comment(`${inProgressSummarizeCmt}`, SUMMARIZE_TAG, 'replace') + + const summariesFailed: string[] = [] + + const doSummary = async (filename: string, fileContent: string, fileDiff: string): Promise<[string, string, boolean, string[]] | null> => { + info(`summarize: ${filename}`) + const ins = inputs.clone() + if (fileDiff.length === 0) { + warning(`summarize: file_diff is empty, skip ${filename}`) + summariesFailed.push(`${filename} (empty diff)`) + return null + } + + ins.filename = filename + ins.fileDiff = fileDiff + + // render prompt based on inputs so far + const summarizePrompt = prompts.renderSummarizeFileDiff(ins) + const tokens = getTokenCount(summarizePrompt) + + if (tokens > options.tokenLimits.requestTokens) { + info(`summarize: diff tokens exceeds limit, skip ${filename}`) + summariesFailed.push(`${filename} (diff tokens exceeds limit)`) + return null + } + + // summarize content + try { + const summarizeResp = await reviewBot.chat(summarizePrompt) + + if (summarizeResp === '') { + info('summarize: nothing obtained from erag') + summariesFailed.push(`${filename} (nothing obtained from erag)`) + return null + } else { + // parse the comment to look for triage classification + // Format is : [TRIAGE]: + // if the change needs review return true, else false + let needsReview = false + let summary = summarizeResp + let symbols: string[] = [] + + const triageRegex = /\[TRIAGE\]:\s*(NEEDS_REVIEW|APPROVED)/ + const triageMatch = summarizeResp.match(triageRegex) + if (triageMatch != null) { + const triage = triageMatch[1] + needsReview = triage === 'NEEDS_REVIEW' + // remove this line from the comment + summary = summarizeResp.replace(triageRegex, '').trim() + info(`filename: ${filename}, triage: ${triage}`) + } + + // Symbols to search for in the codebase to give more context to the LLM + const symbolsRegex = /SYMBOLS:\s*(\[.*?\])/ + const symbolsMatch = summarizeResp.match(symbolsRegex) + if (symbolsMatch != null) { + const symbolsStr = symbolsMatch[1] + symbols = symbolsStr + .replace('[', '') + .replace(']', '') + .split(',') + .map(symbol => symbol.trim()) + .filter(symbol => symbol !== '') + + summary = summary.replace(symbolsRegex, '').trim() + } + + return [filename, summary, needsReview, symbols] + } + } catch (e: any) { + warning(`summarize: error from erag: ${e as string}`) + summariesFailed.push(`${filename} (error from erag: ${e as string})})`) + return null + } + } + + const summaryPromises = [] + const skippedFiles = [] + for (const [filename, fileContent, fileDiff] of filesAndChanges) { + if (options.maxFiles <= 0 || summaryPromises.length < options.maxFiles) { + summaryPromises.push(eragConcurrencyLimit(async () => await doSummary(filename, fileContent, fileDiff))) + } else { + skippedFiles.push(filename) + } + } + + const summaries = (await Promise.all(summaryPromises)).filter(summary => summary !== null) as Array<[string, string, boolean, string[]]> + + if (summaries.length > 0) { + const batchSize = 10 + // join summaries into one in the batches of batchSize + // and ask the bot to summarize the summaries + for (let i = 0; i < summaries.length; i += batchSize) { + const summariesBatch = summaries.slice(i, i + batchSize) + for (const [filename, summary] of summariesBatch) { + inputs.rawSummary += `--- +${filename}: ${summary} +` + } + // ask chatgpt to summarize the summaries + const summarizeResp = await reviewBot.chat(prompts.renderSummarizeChangesets(inputs)) + if (summarizeResp === '') { + warning('summarize: nothing obtained from erag') + } else { + inputs.rawSummary = summarizeResp + } + } + } + + // final summary + const summarizeFinalResponse = await reviewBot.chat(prompts.renderSummarize(inputs)) + if (summarizeFinalResponse === '') { + info('summarize: nothing obtained from erag') + } + + if (options.disableReleaseNotes === false) { + // final release notes + const releaseNotesResponse = await reviewBot.chat(prompts.renderSummarizeReleaseNotes(inputs)) + if (releaseNotesResponse === '') { + info('release notes: nothing obtained from erag') + } else { + let message = '### Summary by Erag Reviewer\n\n' + message += releaseNotesResponse + try { + await commenter.updateDescription(pullRequest.number, message) + } catch (e: any) { + warning(`release notes: error from github: ${e.message as string}`) + } + } + } + + // generate a short summary as well + const summarizeShortResponse = await reviewBot.chat(prompts.renderSummarizeShort(inputs)) + inputs.shortSummary = summarizeShortResponse + + let summarizeComment = `${summarizeFinalResponse} +${RAW_SUMMARY_START_TAG} +${inputs.rawSummary} +${RAW_SUMMARY_END_TAG} +${SHORT_SUMMARY_START_TAG} +${inputs.shortSummary} +${SHORT_SUMMARY_END_TAG}` + + statusMsg += ` +${ + skippedFiles.length > 0 + ? ` +
+Files not processed due to max files limit (${skippedFiles.length}) + +* ${skippedFiles.join('\n* ')} + +
+` + : '' +} +${ + summariesFailed.length > 0 + ? ` +
+Files not summarized due to errors (${summariesFailed.length}) + +* ${summariesFailed.join('\n* ')} + +
+` + : '' +} +` + + if (!options.disableReview) { + const filesAndChangesReview = filesAndChanges + .filter(([filename]) => { + const needsReview = summaries.find(([summaryFilename]) => summaryFilename === filename)?.[2] ?? true + return needsReview + }) + .map(([filename, fileContent, fileDiff, patches]) => { + const summaryEntry = summaries.find(([summaryFilename]) => summaryFilename === filename) + const symbols = summaryEntry ? summaryEntry[3] : [] + return [filename, fileContent, fileDiff, patches, symbols] as [string, string, string, [number, number, string][], string[]] + }) + + const reviewsSkipped = filesAndChanges + .filter(([filename]) => !filesAndChangesReview.some(([reviewFilename]) => reviewFilename === filename)) + .map(([filename]) => filename) + + // failed reviews array + const reviewsFailed: string[] = [] + let lgtmCount = 0 + let reviewCount = 0 + const doReview = async (filename: string, fileContent: string, patches: Array<[number, number, string]>, symbols: string[]): Promise => { + if (options.debug) { + info(`reviewing ${filename}`) + info(`symbols: ${symbols} - size: ${symbols.length}`) + } + + const symbolSearchResults = await searchSymbols(symbols) + info(`symbolSearchResults: ${symbolSearchResults}`) + + // make a copy of inputs + const ins: Inputs = inputs.clone() + ins.filename = filename + ins.symbolSearchResults = symbolSearchResults + + // calculate tokens based on inputs so far + let tokens = getTokenCount(prompts.renderReviewFileDiff(ins)) + // loop to calculate total patch tokens + let patchesToPack = 0 + for (const [, , patch] of patches) { + const patchTokens = getTokenCount(patch) + if (tokens + patchTokens > options.tokenLimits.requestTokens) { + info(`only packing ${patchesToPack} / ${patches.length} patches, tokens: ${tokens} / ${options.tokenLimits.requestTokens}`) + break + } + tokens += patchTokens + patchesToPack += 1 + } + + let patchesPacked = 0 + for (const [startLine, endLine, patch] of patches) { + // see if we can pack more patches into this request + if (patchesPacked >= patchesToPack) { + info(`unable to pack more patches into this request, packed: ${patchesPacked}, total patches: ${patches.length}, skipping.`) + if (options.debug) { + info(`prompt so far: ${prompts.renderReviewFileDiff(ins)}`) + } + break + } + patchesPacked += 1 + + let commentChain = '' + try { + const allChains = await commenter.getCommentChainsWithinRange(pullRequest.number, filename, startLine, endLine, COMMENT_REPLY_TAG) + + if (allChains.length > 0) { + info(`Found comment chains: ${allChains} for ${filename}`) + commentChain = allChains + } + } catch (e: any) { + warning(`Failed to get comments: ${e as string}, skipping. backtrace: ${e.stack as string}`) + } + // try packing comment_chain into this request + const commentChainTokens = getTokenCount(commentChain) + if (tokens + commentChainTokens > options.tokenLimits.requestTokens) { + commentChain = '' + } else { + tokens += commentChainTokens + } + + ins.patches += ` +${patch} +` + if (commentChain !== '') { + ins.patches += ` +---comment_chains--- +\`\`\` +${commentChain} +\`\`\` +` + } + + ins.patches += ` +---end_change_section--- +` + } + + if (patchesPacked > 0) { + // perform review + try { + const response = await reviewBot.chat(prompts.renderReviewFileDiff(ins)) + if (response === '') { + info('review: nothing obtained from erag') + reviewsFailed.push(`${filename} (no response)`) + return + } + // parse review + const reviews = parseReview(response, patches, options.debug) + for (const review of reviews) { + // check for LGTM + if (review.comment.includes('LGTM') || review.comment.includes('looks good to me')) { + lgtmCount += 1 + continue + } + + try { + reviewCount += 1 + await commenter.bufferReviewComment(filename, review.startLine, review.endLine, `${review.comment}`) + } catch (e: any) { + reviewsFailed.push(`${filename} comment failed (${e as string})`) + } + } + } catch (e: any) { + warning(`Failed to review: ${e as string}, skipping. backtrace: ${e.stack as string}`) + reviewsFailed.push(`${filename} (${e as string})`) + } + } else { + reviewsSkipped.push(`${filename} (diff too large)`) + } + } + + const reviewPromises = [] + for (const [filename, fileContent, , patches, symbols] of filesAndChangesReview) { + if (options.maxFiles <= 0 || reviewPromises.length < options.maxFiles) { + reviewPromises.push( + eragConcurrencyLimit(async () => { + await doReview(filename, fileContent, patches, symbols) + }) + ) + } else { + skippedFiles.push(filename) + } + } + + await Promise.all(reviewPromises) + + statusMsg += ` +${ + reviewsFailed.length > 0 + ? `
+Files not reviewed due to errors (${reviewsFailed.length}) + +* ${reviewsFailed.join('\n* ')} + +
+` + : '' +} +${ + reviewsSkipped.length > 0 + ? `
+Files skipped from review due to trivial changes (${reviewsSkipped.length}) + +* ${reviewsSkipped.join('\n* ')} + +
+` + : '' +} +
+Review comments generated (${reviewCount + lgtmCount}) + +* Review: ${reviewCount} +* LGTM: ${lgtmCount} + +
+ +--- + +
+Tips + +### Chat with Image description ERAG Reviewer (\`@erag\`) +- Reply on review comments left by this bot to ask follow-up questions. A review comment is a comment on a diff or a file. +- Invite the bot into a review comment chain by tagging \`@erag\` in a reply. + +### Code suggestions +- The bot may make code suggestions, but please review them carefully before committing since the line number ranges may be misaligned. +- You can edit the comment made by the bot and manually tweak the suggestion if it is slightly off. + +### Pausing incremental reviews +- Add \`@erag: ignore\` anywhere in the PR description to pause further reviews from the bot. + +
+` + // add existing_comment_ids_block with latest head sha + summarizeComment += `\n${commenter.addReviewedCommitId(existingCommitIdsBlock, pullRequest.head.sha)}` + + // post the review + await commenter.submitReview(pullRequest.number, commits[commits.length - 1].sha, statusMsg) + } + + // post the final summary comment + await commenter.comment(`${summarizeComment}`, SUMMARIZE_TAG, 'replace') +} + +async function searchSymbols(symbols: string[]): Promise { + // Uses ripgrep to search for the symbols in the codebase + let searchResults = '' + + for (const symbol of symbols) { + try { + info(`Searching for symbol ${symbol}`) + searchResults += `---${symbol}---\n` + const {stdout} = await execFileAsync(rgPath, [symbol, '-n', '-w', '.'], {maxBuffer: 1024 * 1024}) + searchResults += `${stdout.trim()}\n` + } catch (err: any) { + if (err.code === 1) { + // Ripgrep exits with code 1 if no matches are found + info(`No matches found for symbol ${symbol}`) + searchResults += `No matches found for symbol ${symbol}\n` + } else { + warning(`Error searching for symbol ${symbol}: ${err.message as string}`) + } + } + } + + return searchResults +} + +async function determineHighestReviewedCommitId(existingCommitIdsBlock: string, pullRequest: any, commenter: Commenter) { + const allCommitIds = await commenter.getAllCommitIds() + let highestReviewedCommitId = '' + if (existingCommitIdsBlock !== '') { + highestReviewedCommitId = commenter.getHighestReviewedCommitId(allCommitIds, commenter.getReviewedCommitIds(existingCommitIdsBlock)) + } + + if (highestReviewedCommitId === '' || highestReviewedCommitId === pullRequest.head.sha) { + info(`Will review from the base commit: ${pullRequest.base.sha as string}`) + highestReviewedCommitId = pullRequest.base.sha + } else { + info(`Will review from commit: ${highestReviewedCommitId}`) + } + return highestReviewedCommitId +} + +async function fetchDiffFiles(highestReviewedCommitId: string, pullRequest: any) { + // Fetch the diff between the highest reviewed commit and the latest commit of the PR branch + const incrementalDiff = await octokit.repos.compareCommits({ + owner: repo.owner, + repo: repo.repo, + base: highestReviewedCommitId, + head: pullRequest.head.sha + }) + + // Fetch the diff between the target branch's base commit and the latest commit of the PR branch + const targetBranchDiff = await octokit.repos.compareCommits({ + owner: repo.owner, + repo: repo.repo, + base: pullRequest.base.sha, + head: pullRequest.head.sha + }) + + const incrementalFiles = incrementalDiff.data.files + const targetBranchFiles = targetBranchDiff.data.files + const commits = incrementalDiff.data.commits + + if (!incrementalFiles || !targetBranchFiles || !commits) { + warning('Skipped: files data is missing') + return {files: [], commits: []} + } + + // Get files that were changed in the last commit which are also changed compared to the PR base commit + const files = targetBranchFiles.filter(targetBranchFile => + incrementalFiles.some(incrementalFile => incrementalFile.filename === targetBranchFile.filename) + ) + + return {files, commits} +} + +async function getFilesAndChanges(filterSelectedFiles: any, pullRequest: any, options: Options) { + const githubConcurrencyLimit = pLimit(options.githubConcurrencyLimit) + // find hunks to review + const filteredFiles: Array<[string, string, string, Array<[number, number, string]>] | null> = await Promise.all( + filterSelectedFiles.map((file: any) => + githubConcurrencyLimit(async () => { + // retrieve file contents + let fileContent = '' + try { + const contents = await octokit.repos.getContent({ + owner: repo.owner, + repo: repo.repo, + path: file.filename, + ref: pullRequest.base.sha + }) + if (contents.data != null) { + if (!Array.isArray(contents.data)) { + if (contents.data.type === 'file' && contents.data.content != null) { + fileContent = Buffer.from(contents.data.content, 'base64').toString() + } + } + } + } catch (e: any) { + warning(`Failed to get file contents: ${e as string}. This is OK if it's a new file.`) + } + + let fileDiff = '' + if (file.patch != null) { + fileDiff = file.patch + } + + const patches: Array<[number, number, string]> = [] + for (const patch of splitPatch(file.patch)) { + const patchLines = patchStartEndLine(patch) + if (patchLines == null) { + continue + } + const hunks = parsePatch(patch) + if (hunks == null) { + continue + } + const hunksStr = ` +---new_hunk--- +\`\`\` +${hunks.newHunk} +\`\`\` + +---old_hunk--- +\`\`\` +${hunks.oldHunk} +\`\`\` +` + patches.push([patchLines.newHunk.startLine, patchLines.newHunk.endLine, hunksStr]) + } + if (patches.length > 0) { + return [file.filename, fileContent, fileDiff, patches] as [string, string, string, Array<[number, number, string]>] + } else { + return null + } + }) + ) + ) + + // Filter out any null results + return filteredFiles.filter(file => file !== null) as Array<[string, string, string, Array<[number, number, string]>]> +} + +function filterFilesByPath(files: any, options: Options) { + const filterSelectedFiles = [] + const filterIgnoredFiles = [] + for (const file of files) { + if (!options.pathFilters.check(file.filename)) { + info(`skip for excluded path: ${file.filename}`) + filterIgnoredFiles.push(file) + } else { + filterSelectedFiles.push(file) + } + } + return {filterSelectedFiles, filterIgnoredFiles} +} + +async function getExistingSummarizeComment(pullRequest: any, commenter: Commenter, inputs: Inputs) { + const existingSummarizeCmt = await commenter.findCommentWithTag(SUMMARIZE_TAG, pullRequest.number) + if (existingSummarizeCmt) { + const existingSummarizeCmtBody = existingSummarizeCmt.body + const existingCommitIdsBlock = commenter.getReviewedCommitIdsBlock(existingSummarizeCmtBody) + inputs.rawSummary = commenter.getRawSummary(existingSummarizeCmtBody) + inputs.shortSummary = commenter.getShortSummary(existingSummarizeCmtBody) + return {existingSummarizeCmtBody, existingCommitIdsBlock} + } + return {existingSummarizeCmtBody: '', existingCommitIdsBlock: ''} +} + +function initializeInputs(pullRequest: any, options: Options, commenter: Commenter) { + const inputs: Inputs = new Inputs() + inputs.systemMessage = options.systemMessage + inputs.title = pullRequest.title + if (pullRequest.body) { + inputs.description = commenter.getDescription(pullRequest.body) + } + return inputs +} + +function isPullRequestEvent(): boolean { + if (context.eventName !== 'pull_request' && context.eventName !== 'pull_request_target') { + warning(`Skipped: current event is ${context.eventName}, only support pull_request event`) + return false + } + if (context.payload.pull_request == null) { + warning('Skipped: context.payload.pull_request is null') + return false + } + return true +} + +function splitPatch(patch: string | null | undefined): string[] { + if (patch == null) { + return [] + } + + const pattern = /(^@@ -(\d+),(\d+) \+(\d+),(\d+) @@).*$/gm + + const result: string[] = [] + let last = -1 + let match: RegExpExecArray | null + while ((match = pattern.exec(patch)) !== null) { + if (last === -1) { + last = match.index + } else { + result.push(patch.substring(last, match.index)) + last = match.index + } + } + if (last !== -1) { + result.push(patch.substring(last)) + } + return result +} + +function patchStartEndLine(patch: string): { + oldHunk: {startLine: number; endLine: number} + newHunk: {startLine: number; endLine: number} +} | null { + const pattern = /(^@@ -(\d+),(\d+) \+(\d+),(\d+) @@)/gm + const match = pattern.exec(patch) + if (match != null) { + const oldBegin = parseInt(match[2]) + const oldDiff = parseInt(match[3]) + const newBegin = parseInt(match[4]) + const newDiff = parseInt(match[5]) + return { + oldHunk: { + startLine: oldBegin, + endLine: oldBegin + oldDiff - 1 + }, + newHunk: { + startLine: newBegin, + endLine: newBegin + newDiff - 1 + } + } + } else { + return null + } +} + +function parsePatch(patch: string): {oldHunk: string; newHunk: string} | null { + const hunkInfo = patchStartEndLine(patch) + if (hunkInfo == null) { + return null + } + + const oldHunkLines: string[] = [] + const newHunkLines: string[] = [] + + let newLine = hunkInfo.newHunk.startLine + + const lines = patch.split('\n').slice(1) // Skip the @@ line + + // Remove the last line if it's empty + if (lines[lines.length - 1] === '') { + lines.pop() + } + + // Skip annotations for the first 3 and last 3 lines + const skipStart = 3 + const skipEnd = 3 + + let currentLine = 0 + + const removalOnly = !lines.some(line => line.startsWith('+')) + + for (const line of lines) { + currentLine++ + if (line.startsWith('-')) { + oldHunkLines.push(`${line.substring(1)}`) + } else if (line.startsWith('+')) { + newHunkLines.push(`${newLine}: ${line.substring(1)}`) + newLine++ + } else { + // context line + oldHunkLines.push(`${line}`) + if (removalOnly || (currentLine > skipStart && currentLine <= lines.length - skipEnd)) { + newHunkLines.push(`${newLine}: ${line}`) + } else { + newHunkLines.push(`${line}`) + } + newLine++ + } + } + + return { + oldHunk: oldHunkLines.join('\n'), + newHunk: newHunkLines.join('\n') + } +} + +interface Review { + startLine: number + endLine: number + comment: string +} + +function parseReview(response: string, patches: Array<[number, number, string]>, debug = false): Review[] { + const reviews: Review[] = [] + + response = sanitizeResponse(response.trim()) + + const lines = response.split('\n') + const lineNumberRangeRegex = /(?:^|\s)(\d+)-(\d+):\s*$/ + const commentSeparator = '---' + + let currentStartLine: number | null = null + let currentEndLine: number | null = null + let currentComment = '' + function storeReview(): void { + if (currentStartLine !== null && currentEndLine !== null) { + const review: Review = { + startLine: currentStartLine, + endLine: currentEndLine, + comment: currentComment + } + + for (const [startLine, endLine] of patches) { + const intersectionStart = Math.max(review.startLine, startLine) + const intersectionEnd = Math.min(review.endLine, endLine) + const intersectionLength = Math.max(0, intersectionEnd - intersectionStart + 1) + + if (intersectionLength > 0) { + reviews.push(review) + info(`Stored comment for line range ${currentStartLine}-${currentEndLine}: ${currentComment.trim()}`) + break + } + } + } + } + + function sanitizeCodeBlock(comment: string, codeBlockLabel: string): string { + const codeBlockStart = `\`\`\`${codeBlockLabel}` + const codeBlockEnd = '```' + const lineNumberRegex = /^ *(\d+): /gm + + let codeBlockStartIndex = comment.indexOf(codeBlockStart) + + while (codeBlockStartIndex !== -1) { + const codeBlockEndIndex = comment.indexOf(codeBlockEnd, codeBlockStartIndex + codeBlockStart.length) + + if (codeBlockEndIndex === -1) break + + const codeBlock = comment.substring(codeBlockStartIndex + codeBlockStart.length, codeBlockEndIndex) + const sanitizedBlock = codeBlock.replace(lineNumberRegex, '') + + comment = comment.slice(0, codeBlockStartIndex + codeBlockStart.length) + sanitizedBlock + comment.slice(codeBlockEndIndex) + + codeBlockStartIndex = comment.indexOf(codeBlockStart, codeBlockStartIndex + codeBlockStart.length + sanitizedBlock.length + codeBlockEnd.length) + } + + return comment + } + + function sanitizeResponse(comment: string): string { + comment = sanitizeCodeBlock(comment, 'suggestion') + comment = sanitizeCodeBlock(comment, 'diff') + return comment + } + + for (const line of lines) { + const lineNumberRangeMatch = line.match(lineNumberRangeRegex) + + if (lineNumberRangeMatch != null) { + storeReview() + currentStartLine = parseInt(lineNumberRangeMatch[1], 10) + currentEndLine = parseInt(lineNumberRangeMatch[2], 10) + currentComment = '' + if (debug) { + info(`Found line number range: ${currentStartLine}-${currentEndLine}`) + } + continue + } + + if (line.trim() === commentSeparator) { + storeReview() + currentStartLine = null + currentEndLine = null + currentComment = '' + if (debug) { + info('Found comment separator') + } + continue + } + + if (currentStartLine !== null && currentEndLine !== null) { + currentComment += `${line}\n` + } + } + + storeReview() + + return reviews +} diff --git a/src/types.ts b/src/types.ts new file mode 100644 index 00000000..8ef6f409 --- /dev/null +++ b/src/types.ts @@ -0,0 +1,47 @@ +export interface Finding { + file: string + message: string + suggestion?: string + lines?: { + start: number + end: number + } + severity: 'error' | 'warning' | 'info' +} + +export interface PRContext { + title: string + description: string + summary: string + pullRequest: any // TODO: Define proper type + files: FileInfo[] +} + +export interface FileInfo { + filename: string + patch?: string + additions?: number + deletions?: number +} + +export interface PullRequest { + number: number + title: string + body?: string +} + +export interface PRSummaryInput { + title: string + description: string + changes: { + filename: string + patch: string + additions: number + deletions: number + }[] +} + +export interface FileBatch { + files: FileInfo[] + totalLines: number +}