diff --git a/.github/dashboard/.gitignore b/.github/dashboard/.gitignore new file mode 100644 index 00000000..924a3dc0 --- /dev/null +++ b/.github/dashboard/.gitignore @@ -0,0 +1,2 @@ +site/ +__pycache__/ diff --git a/.github/dashboard/README.md b/.github/dashboard/README.md new file mode 100644 index 00000000..9a584d8c --- /dev/null +++ b/.github/dashboard/README.md @@ -0,0 +1,18 @@ +# GitHub Dashboard + +## Local Preview + +```bash +cd .github/dashboard +npm install +npx tsx src/generate.tsx +npx serve site -l 8901 +``` + +For faster iteration, set `maxRuns: 10` in the CI section of `src/config.ts` โ€” full CI fetch takes ~90s. + +## Type Check + +```bash +npx tsc --noEmit +``` diff --git a/.github/dashboard/package-lock.json b/.github/dashboard/package-lock.json new file mode 100644 index 00000000..8cede744 --- /dev/null +++ b/.github/dashboard/package-lock.json @@ -0,0 +1,3258 @@ +{ + "name": "dashboard", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "dependencies": { + "@kitajs/html": "4.2.7" + }, + "devDependencies": { + "@types/node": "20.17.57", + "chart.js": "4.5.1", + "esbuild": "0.28.0", + "tsx": "4.19.4", + "typescript": "5.8.3", + "vitest": "3.2.1" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.0.tgz", + "integrity": "sha512-lhRUCeuOyJQURhTxl4WkpFTjIsbDayJHih5kZC1giwE+MhIzAb7mEsQMqMf18rHLsrb5qI1tafG20mLxEWcWlA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.0.tgz", + "integrity": "sha512-wqh0ByljabXLKHeWXYLqoJ5jKC4XBaw6Hk08OfMrCRd2nP2ZQ5eleDZC41XHyCNgktBGYMbqnrJKq/K/lzPMSQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.0.tgz", + "integrity": "sha512-+WzIXQOSaGs33tLEgYPYe/yQHf0WTU0X42Jca3y8NWMbUVhp7rUnw+vAsRC/QiDrdD31IszMrZy+qwPOPjd+rw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.0.tgz", + "integrity": "sha512-+VJggoaKhk2VNNqVL7f6S189UzShHC/mR9EE8rDdSkdpN0KflSwWY/gWjDrNxxisg8Fp1ZCD9jLMo4m0OUfeUA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.0.tgz", + "integrity": "sha512-0T+A9WZm+bZ84nZBtk1ckYsOvyA3x7e2Acj1KdVfV4/2tdG4fzUp91YHx+GArWLtwqp77pBXVCPn2We7Letr0Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.0.tgz", + "integrity": "sha512-fyzLm/DLDl/84OCfp2f/XQ4flmORsjU7VKt8HLjvIXChJoFFOIL6pLJPH4Yhd1n1gGFF9mPwtlN5Wf82DZs+LQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.0.tgz", + "integrity": "sha512-l9GeW5UZBT9k9brBYI+0WDffcRxgHQD8ShN2Ur4xWq/NFzUKm3k5lsH4PdaRgb2w7mI9u61nr2gI2mLI27Nh3Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.0.tgz", + "integrity": "sha512-BXoQai/A0wPO6Es3yFJ7APCiKGc1tdAEOgeTNy3SsB491S3aHn4S4r3e976eUnPdU+NbdtmBuLncYir2tMU9Nw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.0.tgz", + "integrity": "sha512-CjaaREJagqJp7iTaNQjjidaNbCKYcd4IDkzbwwxtSvjI7NZm79qiHc8HqciMddQ6CKvJT6aBd8lO9kN/ZudLlw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.0.tgz", + "integrity": "sha512-RVyzfb3FWsGA55n6WY0MEIEPURL1FcbhFE6BffZEMEekfCzCIMtB5yyDcFnVbTnwk+CLAgTujmV/Lgvih56W+A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.0.tgz", + "integrity": "sha512-KBnSTt1kxl9x70q+ydterVdl+Cn0H18ngRMRCEQfrbqdUuntQQ0LoMZv47uB97NljZFzY6HcfqEZ2SAyIUTQBQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.0.tgz", + "integrity": "sha512-zpSlUce1mnxzgBADvxKXX5sl8aYQHo2ezvMNI8I0lbblJtp8V4odlm3Yzlj7gPyt3T8ReksE6bK+pT3WD+aJRg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.0.tgz", + "integrity": "sha512-2jIfP6mmjkdmeTlsX/9vmdmhBmKADrWqN7zcdtHIeNSCH1SqIoNI63cYsjQR8J+wGa4Y5izRcSHSm8K3QWmk3w==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.0.tgz", + "integrity": "sha512-bc0FE9wWeC0WBm49IQMPSPILRocGTQt3j5KPCA8os6VprfuJ7KD+5PzESSrJ6GmPIPJK965ZJHTUlSA6GNYEhg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.0.tgz", + "integrity": "sha512-SQPZOwoTTT/HXFXQJG/vBX8sOFagGqvZyXcgLA3NhIqcBv1BJU1d46c0rGcrij2B56Z2rNiSLaZOYW5cUk7yLQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.0.tgz", + "integrity": "sha512-SCfR0HN8CEEjnYnySJTd2cw0k9OHB/YFzt5zgJEwa+wL/T/raGWYMBqwDNAC6dqFKmJYZoQBRfHjgwLHGSrn3Q==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.0.tgz", + "integrity": "sha512-us0dSb9iFxIi8srnpl931Nvs65it/Jd2a2K3qs7fz2WfGPHqzfzZTfec7oxZJRNPXPnNYZtanmRc4AL/JwVzHQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.0.tgz", + "integrity": "sha512-CR/RYotgtCKwtftMwJlUU7xCVNg3lMYZ0RzTmAHSfLCXw3NtZtNpswLEj/Kkf6kEL3Gw+BpOekRX0BYCtklhUw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.0.tgz", + "integrity": "sha512-nU1yhmYutL+fQ71Kxnhg8uEOdC0pwEW9entHykTgEbna2pw2dkbFSMeqjjyHZoCmt8SBkOSvV+yNmm94aUrrqw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.0.tgz", + "integrity": "sha512-cXb5vApOsRsxsEl4mcZ1XY3D4DzcoMxR/nnc4IyqYs0rTI8ZKmW6kyyg+11Z8yvgMfAEldKzP7AdP64HnSC/6g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.0.tgz", + "integrity": "sha512-8wZM2qqtv9UP3mzy7HiGYNH/zjTA355mpeuA+859TyR+e+Tc08IHYpLJuMsfpDJwoLo1ikIJI8jC3GFjnRClzA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.0.tgz", + "integrity": "sha512-FLGfyizszcef5C3YtoyQDACyg95+dndv79i2EekILBofh5wpCa1KuBqOWKrEHZg3zrL3t5ouE5jgr94vA+Wb2w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.0.tgz", + "integrity": "sha512-1ZgjUoEdHZZl/YlV76TSCz9Hqj9h9YmMGAgAPYd+q4SicWNX3G5GCyx9uhQWSLcbvPW8Ni7lj4gDa1T40akdlw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.0.tgz", + "integrity": "sha512-Q9StnDmQ/enxnpxCCLSg0oo4+34B9TdXpuyPeTedN/6+iXBJ4J+zwfQI28u/Jl40nOYAxGoNi7mFP40RUtkmUA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.0.tgz", + "integrity": "sha512-zF3ag/gfiCe6U2iczcRzSYJKH1DCI+ByzSENHlM2FcDbEeo5Zd2C86Aq0tKUYAJJ1obRP84ymxIAksZUcdztHA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.0.tgz", + "integrity": "sha512-pEl1bO9mfAmIC+tW5btTmrKaujg3zGtUmWNdCw/xs70FBjwAL3o9OEKNHvNmnyylD6ubxUERiEhdsL0xBQ9efw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@kitajs/html": { + "version": "4.2.7", + "resolved": "https://registry.npmjs.org/@kitajs/html/-/html-4.2.7.tgz", + "integrity": "sha512-HMAHy0GLH7i7wY1o0W1TeugNhtWefUkhb9l6zx6v2fIkR1cPlUtQQdkTt/jkBA7723yO7uBERcXFPZpWT7HWPQ==", + "license": "MIT", + "dependencies": { + "csstype": "^3.1.3" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/kitajs/html?sponsor=1" + } + }, + "node_modules/@kurkle/color": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.4.tgz", + "integrity": "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.2.tgz", + "integrity": "sha512-dnlp69efPPg6Uaw2dVqzWRfAWRnYVb1XJ8CyyhIbZeaq4CA5/mLeZ1IEt9QqQxmbdvagjLIm2ZL8BxXv5lH4Yw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.2.tgz", + "integrity": "sha512-OqZTwDRDchGRHHm/hwLOL7uVPB9aUvI0am/eQuWMNyFHf5PSEQmyEeYYheA0EPPKUO/l0uigCp+iaTjoLjVoHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.2.tgz", + "integrity": "sha512-UwRE7CGpvSVEQS8gUMBe1uADWjNnVgP3Iusyda1nSRwNDCsRjnGc7w6El6WLQsXmZTbLZx9cecegumcitNfpmA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.2.tgz", + "integrity": "sha512-gjEtURKLCC5VXm1I+2i1u9OhxFsKAQJKTVB8WvDAHF+oZlq0GTVFOlTlO1q3AlCTE/DF32c16ESvfgqR7343/g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.2.tgz", + "integrity": "sha512-Bcl6CYDeAgE70cqZaMojOi/eK63h5Me97ZqAQoh77VPjMysA/4ORQBRGo3rRy45x4MzVlU9uZxs8Uwy7ZaKnBw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.2.tgz", + "integrity": "sha512-LU+TPda3mAE2QB0/Hp5VyeKJivpC6+tlOXd1VMoXV/YFMvk/MNk5iXeBfB4MQGRWyOYVJ01625vjkr0Az98OJQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.2.tgz", + "integrity": "sha512-2QxQrM+KQ7DAW4o22j+XZ6RKdxjLD7BOWTP0Bv0tmjdyhXSsr2Ul1oJDQqh9Zf5qOwTuTc7Ek83mOFaKnodPjg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.2.tgz", + "integrity": "sha512-TbziEu2DVsTEOPif2mKWkMeDMLoYjx95oESa9fkQQK7r/Orta0gnkcDpzwufEcAO2BLBsD7mZkXGFqEdMRRwfw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.2.tgz", + "integrity": "sha512-bO/rVDiDUuM2YfuCUwZ1t1cP+/yqjqz+Xf2VtkdppefuOFS2OSeAfgafaHNkFn0t02hEyXngZkxtGqXcXwO8Rg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.2.tgz", + "integrity": "sha512-hr26p7e93Rl0Za+JwW7EAnwAvKkehh12BU1Llm9Ykiibg4uIr2rbpxG9WCf56GuvidlTG9KiiQT/TXT1yAWxTA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.2.tgz", + "integrity": "sha512-pOjB/uSIyDt+ow3k/RcLvUAOGpysT2phDn7TTUB3n75SlIgZzM6NKAqlErPhoFU+npgY3/n+2HYIQVbF70P9/A==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.2.tgz", + "integrity": "sha512-2/w+q8jszv9Ww1c+6uJT3OwqhdmGP2/4T17cu8WuwyUuuaCDDJ2ojdyYwZzCxx0GcsZBhzi3HmH+J5pZNXnd+Q==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.2.tgz", + "integrity": "sha512-11+aL5vKheYgczxtPVVRhdptAM2H7fcDR5Gw4/bTcteuZBlH4oP9f5s9zYO9aGZvoGeBpqXI/9TZZihZ609wKw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.2.tgz", + "integrity": "sha512-i16fokAGK46IVZuV8LIIwMdtqhin9hfYkCh8pf8iC3QU3LpwL+1FSFGej+O7l3E/AoknL6Dclh2oTdnRMpTzFQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.2.tgz", + "integrity": "sha512-49FkKS6RGQoriDSK/6E2GkAsAuU5kETFCh7pG4yD/ylj9rKhTmO3elsnmBvRD4PgJPds5W2PkhC82aVwmUcJ7A==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.2.tgz", + "integrity": "sha512-mjYNkHPfGpUR00DuM1ZZIgs64Hpf4bWcz9Z41+4Q+pgDx73UwWdAYyf6EG/lRFldmdHHzgrYyge5akFUW0D3mQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.2.tgz", + "integrity": "sha512-ALyvJz965BQk8E9Al/JDKKDLH2kfKFLTGMlgkAbbYtZuJt9LU8DW3ZoDMCtQpXAltZxwBHevXz5u+gf0yA0YoA==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.2.tgz", + "integrity": "sha512-UQjrkIdWrKI626Du8lCQ6MJp/6V1LAo2bOK9OTu4mSn8GGXIkPXk/Vsp4bLHCd9Z9Iz2OTEaokUE90VweJgIYQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.2.tgz", + "integrity": "sha512-bTsRGj6VlSdn/XD4CGyzMnzaBs9bsRxy79eTqTCBsA8TMIEky7qg48aPkvJvFe1HyzQ5oMZdg7AnVlWQSKLTnw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.2.tgz", + "integrity": "sha512-6d4Z3534xitaA1FcMWP7mQPq5zGwBmGbhphh2DwaA1aNIXUu3KTOfwrWpbwI4/Gr0uANo7NTtaykFyO2hPuFLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.2.tgz", + "integrity": "sha512-NetAg5iO2uN7eB8zE5qrZ3CSil+7IJt4WDFLcC75Ymywq1VZVD6qJ6EvNLjZ3rEm6gB7XW5JdT60c6MN35Z85Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.2.tgz", + "integrity": "sha512-NCYhOotpgWZ5kdxCZsv6Iudx0wX8980Q/oW4pNFNihpBKsDbEA1zpkfxJGC0yugsUuyDZ7gL37dbzwhR0VI7pQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.2.tgz", + "integrity": "sha512-RXsaOqXxfoUBQoOgvmmijVxJnW2IGB0eoMO7F8FAjaj0UTywUO/luSqimWBJn04WNgUkeNhh7fs7pESXajWmkg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.2.tgz", + "integrity": "sha512-qdAzEULD+/hzObedtmV6iBpdL5TIbKVztGiK7O3/KYSf+HIzU257+MX1EXJcyIiDbMAqmbwaufcYPvyRryeZtA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.2.tgz", + "integrity": "sha512-Nd/SgG27WoA9e+/TdK74KnHz852TLa94ovOYySo/yMPuTmpckK/jIF2jSwS3g7ELSKXK13/cVdmg1Z/DaCWKxA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "20.17.57", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.57.tgz", + "integrity": "sha512-f3T4y6VU4fVQDKVqJV4Uppy8c1p/sVvS3peyqxyWnzkqXFJLRU7Y1Bl7rMS1Qe9z0v4M6McY0Fp9yBsgHJUsWQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.19.2" + } + }, + "node_modules/@vitest/expect": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.1.tgz", + "integrity": "sha512-FqS/BnDOzV6+IpxrTg5GQRyLOCtcJqkwMwcS8qGCI2IyRVDwPAtutztaf1CjtPHlZlWtl1yUPCd7HM0cNiDOYw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/spy": "3.2.1", + "@vitest/utils": "3.2.1", + "chai": "^5.2.0", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/pretty-format": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.4.tgz", + "integrity": "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.1.tgz", + "integrity": "sha512-kygXhNTu/wkMYbwYpS3z/9tBe0O8qpdBuC3dD/AW9sWa0LE/DAZEjnHtWA9sIad7lpD4nFW1yQ+zN7mEKNH3yA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "3.2.1", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.1.tgz", + "integrity": "sha512-5xko/ZpW2Yc65NVK9Gpfg2y4BFvcF+At7yRT5AHUpTg9JvZ4xZoyuRY4ASlmNcBZjMslV08VRLDrBOmUe2YX3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.1", + "magic-string": "^0.30.17", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot/node_modules/@vitest/pretty-format": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.1.tgz", + "integrity": "sha512-xBh1X2GPlOGBupp6E1RcUQWIxw0w/hRLd3XyBS6H+dMdKTAqHDNsIR2AnJwPA3yYe9DFy3VUKTe3VRTrAiQ01g==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.1.tgz", + "integrity": "sha512-Nbfib34Z2rfcJGSetMxjDCznn4pCYPZOtQYox2kzebIJcgH75yheIKd5QYSFmR8DIZf2M8fwOm66qSDIfRFFfQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^4.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.1.tgz", + "integrity": "sha512-KkHlGhePEKZSub5ViknBcN5KEF+u7dSUr9NW8QsVICusUojrgrOnnY3DEWWO877ax2Pyopuk2qHmt+gkNKnBVw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.1", + "loupe": "^3.1.3", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils/node_modules/@vitest/pretty-format": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.1.tgz", + "integrity": "sha512-xBh1X2GPlOGBupp6E1RcUQWIxw0w/hRLd3XyBS6H+dMdKTAqHDNsIR2AnJwPA3yYe9DFy3VUKTe3VRTrAiQ01g==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/chai": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", + "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/chart.js": { + "version": "4.5.1", + "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.5.1.tgz", + "integrity": "sha512-GIjfiT9dbmHRiYi6Nl2yFCq7kkwdkp1W/lp2J99rX0yo9tgJGn3lKQATztIjb5tVtevcBtIdICNWqlq5+E8/Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@kurkle/color": "^0.3.0" + }, + "engines": { + "pnpm": ">=8" + } + }, + "node_modules/check-error": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz", + "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-eql": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "license": "MIT" + }, + "node_modules/esbuild": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.0.tgz", + "integrity": "sha512-sNR9MHpXSUV/XB4zmsFKN+QgVG82Cc7+/aaxJ8Adi8hyOac+EXptIp45QBPaVyX3N70664wRbTcLTOemCAnyqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.0", + "@esbuild/android-arm": "0.28.0", + "@esbuild/android-arm64": "0.28.0", + "@esbuild/android-x64": "0.28.0", + "@esbuild/darwin-arm64": "0.28.0", + "@esbuild/darwin-x64": "0.28.0", + "@esbuild/freebsd-arm64": "0.28.0", + "@esbuild/freebsd-x64": "0.28.0", + "@esbuild/linux-arm": "0.28.0", + "@esbuild/linux-arm64": "0.28.0", + "@esbuild/linux-ia32": "0.28.0", + "@esbuild/linux-loong64": "0.28.0", + "@esbuild/linux-mips64el": "0.28.0", + "@esbuild/linux-ppc64": "0.28.0", + "@esbuild/linux-riscv64": "0.28.0", + "@esbuild/linux-s390x": "0.28.0", + "@esbuild/linux-x64": "0.28.0", + "@esbuild/netbsd-arm64": "0.28.0", + "@esbuild/netbsd-x64": "0.28.0", + "@esbuild/openbsd-arm64": "0.28.0", + "@esbuild/openbsd-x64": "0.28.0", + "@esbuild/openharmony-arm64": "0.28.0", + "@esbuild/sunos-x64": "0.28.0", + "@esbuild/win32-arm64": "0.28.0", + "@esbuild/win32-ia32": "0.28.0", + "@esbuild/win32-x64": "0.28.0" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/expect-type": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", + "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fsevents": { + "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, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/get-tsconfig": { + "version": "4.14.0", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.14.0.tgz", + "integrity": "sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/loupe": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", + "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathval": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", + "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.16" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.10", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.10.tgz", + "integrity": "sha512-pMMHxBOZKFU6HgAZ4eyGnwXF/EvPGGqUr0MnZ5+99485wwW41kW91A4LOGxSHhgugZmSChL5AlElNdwlNgcnLQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "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, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/rollup": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.2.tgz", + "integrity": "sha512-J9qZyW++QK/09NyN/zeO0dG/1GdGfyp9lV8ajHnRVLfo/uFsbji5mHnDgn/qYdUHyCkM2N+8VyspgZclfAh0eQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.60.2", + "@rollup/rollup-android-arm64": "4.60.2", + "@rollup/rollup-darwin-arm64": "4.60.2", + "@rollup/rollup-darwin-x64": "4.60.2", + "@rollup/rollup-freebsd-arm64": "4.60.2", + "@rollup/rollup-freebsd-x64": "4.60.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.60.2", + "@rollup/rollup-linux-arm-musleabihf": "4.60.2", + "@rollup/rollup-linux-arm64-gnu": "4.60.2", + "@rollup/rollup-linux-arm64-musl": "4.60.2", + "@rollup/rollup-linux-loong64-gnu": "4.60.2", + "@rollup/rollup-linux-loong64-musl": "4.60.2", + "@rollup/rollup-linux-ppc64-gnu": "4.60.2", + "@rollup/rollup-linux-ppc64-musl": "4.60.2", + "@rollup/rollup-linux-riscv64-gnu": "4.60.2", + "@rollup/rollup-linux-riscv64-musl": "4.60.2", + "@rollup/rollup-linux-s390x-gnu": "4.60.2", + "@rollup/rollup-linux-x64-gnu": "4.60.2", + "@rollup/rollup-linux-x64-musl": "4.60.2", + "@rollup/rollup-openbsd-x64": "4.60.2", + "@rollup/rollup-openharmony-arm64": "4.60.2", + "@rollup/rollup-win32-arm64-msvc": "4.60.2", + "@rollup/rollup-win32-ia32-msvc": "4.60.2", + "@rollup/rollup-win32-x64-gnu": "4.60.2", + "@rollup/rollup-win32-x64-msvc": "4.60.2", + "fsevents": "~2.3.2" + } + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.16", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", + "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinypool": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", + "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "node_modules/tinyrainbow": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz", + "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-4.0.4.tgz", + "integrity": "sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tsx": { + "version": "4.19.4", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.19.4.tgz", + "integrity": "sha512-gK5GVzDkJK1SI1zwHf32Mqxf2tSJkNx+eYcNly5+nHvWqXUJYUkWBQtKauoESz3ymezAI++ZwT855x5p5eop+Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.25.0", + "get-tsconfig": "^4.7.5" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/tsx/node_modules/@esbuild/aix-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/android-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/android-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/android-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/darwin-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/darwin-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-loong64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-mips64el": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/win32-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/win32-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" + } + }, + "node_modules/typescript": { + "version": "5.8.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", + "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.19.8", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", + "dev": true, + "license": "MIT" + }, + "node_modules/vite-node": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.2.1.tgz", + "integrity": "sha512-V4EyKQPxquurNJPtQJRZo8hKOoKNBRIhxcDbQFPFig0JdoWcUhwRgK8yoCXXrfYVPKS6XwirGHPszLnR8FbjCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.4.1", + "es-module-lexer": "^1.7.0", + "pathe": "^2.0.3", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vite-node/node_modules/@esbuild/aix-ppc64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz", + "integrity": "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite-node/node_modules/@esbuild/android-arm": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.7.tgz", + "integrity": "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite-node/node_modules/@esbuild/android-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.7.tgz", + "integrity": "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite-node/node_modules/@esbuild/android-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.7.tgz", + "integrity": "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite-node/node_modules/@esbuild/darwin-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.7.tgz", + "integrity": "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite-node/node_modules/@esbuild/darwin-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.7.tgz", + "integrity": "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite-node/node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.7.tgz", + "integrity": "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite-node/node_modules/@esbuild/freebsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.7.tgz", + "integrity": "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite-node/node_modules/@esbuild/linux-arm": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.7.tgz", + "integrity": "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite-node/node_modules/@esbuild/linux-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.7.tgz", + "integrity": "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite-node/node_modules/@esbuild/linux-ia32": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.7.tgz", + "integrity": "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite-node/node_modules/@esbuild/linux-loong64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.7.tgz", + "integrity": "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite-node/node_modules/@esbuild/linux-mips64el": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.7.tgz", + "integrity": "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite-node/node_modules/@esbuild/linux-ppc64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.7.tgz", + "integrity": "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite-node/node_modules/@esbuild/linux-riscv64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.7.tgz", + "integrity": "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite-node/node_modules/@esbuild/linux-s390x": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.7.tgz", + "integrity": "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite-node/node_modules/@esbuild/linux-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.7.tgz", + "integrity": "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite-node/node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.7.tgz", + "integrity": "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite-node/node_modules/@esbuild/netbsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.7.tgz", + "integrity": "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite-node/node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.7.tgz", + "integrity": "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite-node/node_modules/@esbuild/openbsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.7.tgz", + "integrity": "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite-node/node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.7.tgz", + "integrity": "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite-node/node_modules/@esbuild/sunos-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.7.tgz", + "integrity": "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite-node/node_modules/@esbuild/win32-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.7.tgz", + "integrity": "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite-node/node_modules/@esbuild/win32-ia32": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.7.tgz", + "integrity": "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite-node/node_modules/@esbuild/win32-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.7.tgz", + "integrity": "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite-node/node_modules/@types/node": { + "version": "25.6.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.6.0.tgz", + "integrity": "sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "undici-types": "~7.19.0" + } + }, + "node_modules/vite-node/node_modules/esbuild": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz", + "integrity": "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.7", + "@esbuild/android-arm": "0.27.7", + "@esbuild/android-arm64": "0.27.7", + "@esbuild/android-x64": "0.27.7", + "@esbuild/darwin-arm64": "0.27.7", + "@esbuild/darwin-x64": "0.27.7", + "@esbuild/freebsd-arm64": "0.27.7", + "@esbuild/freebsd-x64": "0.27.7", + "@esbuild/linux-arm": "0.27.7", + "@esbuild/linux-arm64": "0.27.7", + "@esbuild/linux-ia32": "0.27.7", + "@esbuild/linux-loong64": "0.27.7", + "@esbuild/linux-mips64el": "0.27.7", + "@esbuild/linux-ppc64": "0.27.7", + "@esbuild/linux-riscv64": "0.27.7", + "@esbuild/linux-s390x": "0.27.7", + "@esbuild/linux-x64": "0.27.7", + "@esbuild/netbsd-arm64": "0.27.7", + "@esbuild/netbsd-x64": "0.27.7", + "@esbuild/openbsd-arm64": "0.27.7", + "@esbuild/openbsd-x64": "0.27.7", + "@esbuild/openharmony-arm64": "0.27.7", + "@esbuild/sunos-x64": "0.27.7", + "@esbuild/win32-arm64": "0.27.7", + "@esbuild/win32-ia32": "0.27.7", + "@esbuild/win32-x64": "0.27.7" + } + }, + "node_modules/vite-node/node_modules/undici-types": { + "version": "7.19.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.19.2.tgz", + "integrity": "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true + }, + "node_modules/vite-node/node_modules/vite": { + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.2.tgz", + "integrity": "sha512-Bby3NOsna2jsjfLVOHKes8sGwgl4TT0E6vvpYgnAYDIF/tie7MRaFthmKuHx1NSXjiTueXH3do80FMQgvEktRg==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.27.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vitest": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.1.tgz", + "integrity": "sha512-VZ40MBnlE1/V5uTgdqY3DmjUgZtIzsYq758JGlyQrv5syIsaYcabkfPkEuWML49Ph0D/SoqpVFd0dyVTr551oA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/expect": "3.2.1", + "@vitest/mocker": "3.2.1", + "@vitest/pretty-format": "^3.2.1", + "@vitest/runner": "3.2.1", + "@vitest/snapshot": "3.2.1", + "@vitest/spy": "3.2.1", + "@vitest/utils": "3.2.1", + "chai": "^5.2.0", + "debug": "^4.4.1", + "expect-type": "^1.2.1", + "magic-string": "^0.30.17", + "pathe": "^2.0.3", + "picomatch": "^4.0.2", + "std-env": "^3.9.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.2", + "tinyglobby": "^0.2.14", + "tinypool": "^1.1.0", + "tinyrainbow": "^2.0.0", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", + "vite-node": "3.2.1", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/debug": "^4.1.12", + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "@vitest/browser": "3.2.1", + "@vitest/ui": "3.2.1", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/debug": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/vitest/node_modules/@esbuild/aix-ppc64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz", + "integrity": "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/android-arm": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.7.tgz", + "integrity": "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/android-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.7.tgz", + "integrity": "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/android-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.7.tgz", + "integrity": "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/darwin-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.7.tgz", + "integrity": "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/darwin-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.7.tgz", + "integrity": "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.7.tgz", + "integrity": "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/freebsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.7.tgz", + "integrity": "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/linux-arm": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.7.tgz", + "integrity": "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/linux-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.7.tgz", + "integrity": "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/linux-ia32": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.7.tgz", + "integrity": "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/linux-loong64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.7.tgz", + "integrity": "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/linux-mips64el": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.7.tgz", + "integrity": "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/linux-ppc64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.7.tgz", + "integrity": "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/linux-riscv64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.7.tgz", + "integrity": "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/linux-s390x": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.7.tgz", + "integrity": "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/linux-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.7.tgz", + "integrity": "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.7.tgz", + "integrity": "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/netbsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.7.tgz", + "integrity": "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.7.tgz", + "integrity": "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/openbsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.7.tgz", + "integrity": "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.7.tgz", + "integrity": "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/sunos-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.7.tgz", + "integrity": "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/win32-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.7.tgz", + "integrity": "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/win32-ia32": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.7.tgz", + "integrity": "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/win32-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.7.tgz", + "integrity": "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@vitest/mocker": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.1.tgz", + "integrity": "sha512-OXxMJnx1lkB+Vl65Re5BrsZEHc90s5NMjD23ZQ9NlU7f7nZiETGoX4NeKZSmsKjseuMq2uOYXdLOeoM0pJU+qw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "3.2.1", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.17" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/vitest/node_modules/esbuild": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz", + "integrity": "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.7", + "@esbuild/android-arm": "0.27.7", + "@esbuild/android-arm64": "0.27.7", + "@esbuild/android-x64": "0.27.7", + "@esbuild/darwin-arm64": "0.27.7", + "@esbuild/darwin-x64": "0.27.7", + "@esbuild/freebsd-arm64": "0.27.7", + "@esbuild/freebsd-x64": "0.27.7", + "@esbuild/linux-arm": "0.27.7", + "@esbuild/linux-arm64": "0.27.7", + "@esbuild/linux-ia32": "0.27.7", + "@esbuild/linux-loong64": "0.27.7", + "@esbuild/linux-mips64el": "0.27.7", + "@esbuild/linux-ppc64": "0.27.7", + "@esbuild/linux-riscv64": "0.27.7", + "@esbuild/linux-s390x": "0.27.7", + "@esbuild/linux-x64": "0.27.7", + "@esbuild/netbsd-arm64": "0.27.7", + "@esbuild/netbsd-x64": "0.27.7", + "@esbuild/openbsd-arm64": "0.27.7", + "@esbuild/openbsd-x64": "0.27.7", + "@esbuild/openharmony-arm64": "0.27.7", + "@esbuild/sunos-x64": "0.27.7", + "@esbuild/win32-arm64": "0.27.7", + "@esbuild/win32-ia32": "0.27.7", + "@esbuild/win32-x64": "0.27.7" + } + }, + "node_modules/vitest/node_modules/vite": { + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.2.tgz", + "integrity": "sha512-Bby3NOsna2jsjfLVOHKes8sGwgl4TT0E6vvpYgnAYDIF/tie7MRaFthmKuHx1NSXjiTueXH3do80FMQgvEktRg==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.27.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + } + } +} diff --git a/.github/dashboard/package.json b/.github/dashboard/package.json new file mode 100644 index 00000000..ee1eaea8 --- /dev/null +++ b/.github/dashboard/package.json @@ -0,0 +1,20 @@ +{ + "private": true, + "type": "module", + "scripts": { + "generate": "tsx src/generate.tsx", + "typecheck": "tsc --noEmit", + "test": "vitest run" + }, + "devDependencies": { + "@types/node": "20.17.57", + "chart.js": "4.5.1", + "esbuild": "0.28.0", + "tsx": "4.19.4", + "typescript": "5.8.3", + "vitest": "3.2.1" + }, + "dependencies": { + "@kitajs/html": "4.2.7" + } +} diff --git a/.github/dashboard/src/__tests__/transform.test.ts b/.github/dashboard/src/__tests__/transform.test.ts new file mode 100644 index 00000000..14a3a153 --- /dev/null +++ b/.github/dashboard/src/__tests__/transform.test.ts @@ -0,0 +1,346 @@ +import { + bucketLabel, + buildHistogram, + computeStats, + formatHours, + parseIssues, + parsePRs, + percentiles, +} from '../transform.js'; +import type { GHIssue, GHPullRequestNode } from '../types.js'; +import { describe, expect, it } from 'vitest'; + +// โ”€โ”€ helpers โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + +function makeGHIssue(overrides: Partial = {}): GHIssue { + return { + number: 1, + title: 'test issue', + state: 'open', + created_at: '2026-01-01T00:00:00Z', + closed_at: null, + labels: [], + assignees: [], + comments: 0, + reactions: { total_count: 0 }, + state_reason: null, + closed_by: null, + user: { login: 'alice' }, + author_association: 'MEMBER', + ...overrides, + }; +} + +function makeGHPR(overrides: Partial = {}): GHPullRequestNode { + return { + number: 10, + title: 'test pr', + state: 'OPEN', + createdAt: '2026-01-01T00:00:00Z', + mergedAt: null, + closedAt: null, + isDraft: false, + author: { login: 'bob' }, + labels: { nodes: [] }, + reviews: { nodes: [] }, + commits: { nodes: [] }, + closingIssuesReferences: { nodes: [] }, + ...overrides, + }; +} + +// โ”€โ”€ percentiles โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + +describe('percentiles', () => { + it('returns zeros for empty array', () => { + expect(percentiles([])).toEqual({ median: 0, avg: 0, p90: 0 }); + }); + + it('returns the value for a single-element array', () => { + expect(percentiles([5])).toEqual({ median: 5, avg: 5, p90: 5 }); + }); + + it('computes correct values for known input', () => { + const vals = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; + const result = percentiles(vals); + expect(result.median).toBe(6); + expect(result.avg).toBeCloseTo(5.5); + expect(result.p90).toBe(10); + }); +}); + +// โ”€โ”€ formatHours โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + +describe('formatHours', () => { + it('returns minutes when < 1 hour', () => { + expect(formatHours(0.5)).toBe('30m'); + }); + + it('returns hours when >= 1 and < 24', () => { + expect(formatHours(2.5)).toBe('2.5h'); + }); + + it('returns days when >= 24', () => { + expect(formatHours(48)).toBe('2.0d'); + }); + + it('handles zero', () => { + expect(formatHours(0)).toBe('0m'); + }); + + it('handles boundary at exactly 1 hour', () => { + expect(formatHours(1)).toBe('1.0h'); + }); + + it('handles boundary at exactly 24 hours', () => { + expect(formatHours(24)).toBe('1.0d'); + }); +}); + +// โ”€โ”€ bucketLabel โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + +describe('bucketLabel', () => { + it('formats a range in minutes', () => { + expect(bucketLabel(0.25, 0.5)).toBe('15m-30m'); + }); + + it('formats a range in hours', () => { + expect(bucketLabel(1, 2)).toBe('1.0h-2.0h'); + }); + + it('formats a range in days', () => { + expect(bucketLabel(24, 48)).toBe('1.0d-2.0d'); + }); + + it('formats open-ended bucket when high is undefined', () => { + expect(bucketLabel(168, undefined)).toBe('>7.0d'); + }); +}); + +// โ”€โ”€ parseIssues โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + +describe('parseIssues', () => { + it('parses a minimal open issue', () => { + const result = parseIssues([makeGHIssue()]); + expect(result).toHaveLength(1); + expect(result[0]).toMatchObject({ + number: 1, + title: 'test issue', + state: 'open', + labels: [], + closed: null, + author: 'alice', + }); + expect(result[0].created).toBeInstanceOf(Date); + }); + + it('parses labels and assignees', () => { + const result = parseIssues([ + makeGHIssue({ + labels: [{ name: 'bug' }, { name: 'P1' }], + assignees: [{ login: 'carol' }], + }), + ]); + expect(result[0].labels).toEqual(['bug', 'P1']); + expect(result[0].assignees).toEqual(['carol']); + }); + + it('parses closed date', () => { + const result = parseIssues([ + makeGHIssue({ + state: 'closed', + closed_at: '2026-01-02T00:00:00Z', + }), + ]); + expect(result[0].state).toBe('closed'); + expect(result[0].closed).toBeInstanceOf(Date); + }); + + it('filters out pull request items', () => { + const result = parseIssues([makeGHIssue(), makeGHIssue({ number: 2, pull_request: {} })]); + expect(result).toHaveLength(1); + expect(result[0].number).toBe(1); + }); + + it('returns empty array for empty input', () => { + expect(parseIssues([])).toEqual([]); + }); +}); + +// โ”€โ”€ parsePRs โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + +describe('parsePRs', () => { + it('calculates ttfrHours from first review', () => { + const pr = makeGHPR({ + createdAt: '2026-01-01T00:00:00Z', + reviews: { + nodes: [{ author: { login: 'rev' }, state: 'COMMENTED', submittedAt: '2026-01-01T02:00:00Z' }], + }, + }); + const result = parsePRs([pr]); + expect(result[0].ttfrHours).toBeCloseTo(2); + }); + + it('calculates ttmHours from merged date', () => { + const pr = makeGHPR({ + createdAt: '2026-01-01T00:00:00Z', + mergedAt: '2026-01-02T12:00:00Z', + state: 'MERGED', + }); + const result = parsePRs([pr]); + expect(result[0].ttmHours).toBeCloseTo(36); + }); + + it('returns null ttfrHours when no reviews', () => { + const result = parsePRs([makeGHPR()]); + expect(result[0].ttfrHours).toBeNull(); + }); + + it('assigns needs-initial-review when no reviews', () => { + const result = parsePRs([makeGHPR()]); + expect(result[0].bucket).toBe('needs-initial-review'); + }); + + it('assigns needs-re-review when commit after review', () => { + const pr = makeGHPR({ + reviews: { + nodes: [{ author: { login: 'rev' }, state: 'CHANGES_REQUESTED', submittedAt: '2026-01-02T00:00:00Z' }], + }, + commits: { + nodes: [{ commit: { committedDate: '2026-01-03T00:00:00Z' } }], + }, + }); + const result = parsePRs([pr]); + expect(result[0].bucket).toBe('needs-re-review'); + }); + + it('assigns approved when all reviews approved', () => { + const pr = makeGHPR({ + reviews: { + nodes: [ + { author: { login: 'rev1' }, state: 'APPROVED', submittedAt: '2026-01-02T00:00:00Z' }, + { author: { login: 'rev2' }, state: 'APPROVED', submittedAt: '2026-01-02T01:00:00Z' }, + ], + }, + }); + const result = parsePRs([pr]); + expect(result[0].bucket).toBe('approved'); + }); + + it('assigns waiting-on-author when review is not all approved', () => { + const pr = makeGHPR({ + reviews: { + nodes: [{ author: { login: 'rev' }, state: 'CHANGES_REQUESTED', submittedAt: '2026-01-02T00:00:00Z' }], + }, + }); + const result = parsePRs([pr]); + expect(result[0].bucket).toBe('waiting-on-author'); + }); + + it('assigns closed bucket for merged PRs', () => { + const pr = makeGHPR({ state: 'MERGED', mergedAt: '2026-01-02T00:00:00Z' }); + const result = parsePRs([pr]); + expect(result[0].bucket).toBe('closed'); + }); + + it('extracts linkedIssuePriority with P0 > P1 > bug > enhancement', () => { + const pr = makeGHPR({ + closingIssuesReferences: { + nodes: [ + { number: 1, labels: { nodes: [{ name: 'enhancement' }, { name: 'P1' }] } }, + { number: 2, labels: { nodes: [{ name: 'bug' }] } }, + ], + }, + }); + const result = parsePRs([pr]); + expect(result[0].linkedIssuePriority).toBe('P1'); + }); + + it('returns null linkedIssuePriority when no closing issues', () => { + const result = parsePRs([makeGHPR()]); + expect(result[0].linkedIssuePriority).toBeNull(); + }); + + it('uses ghost as author when author is null', () => { + const pr = makeGHPR({ author: null }); + const result = parsePRs([pr]); + expect(result[0].author).toBe('ghost'); + }); +}); + +// โ”€โ”€ computeStats โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + +describe('computeStats', () => { + const openIssue = parseIssues([makeGHIssue()])[0]; + const closedIssue = parseIssues([ + makeGHIssue({ + number: 2, + state: 'closed', + created_at: '2026-01-01T00:00:00Z', + closed_at: '2026-01-02T00:00:00Z', + state_reason: 'completed', + closed_by: { login: 'closer' }, + }), + ])[0]; + + it('computes total, open, closed counts', () => { + const stats = computeStats(['total', 'open', 'closed'], [openIssue, closedIssue]); + expect(stats).toEqual([ + { key: 'Total', value: 2 }, + { key: 'Open', value: 1, color: 'green' }, + { key: 'Closed', value: 1 }, + ]); + }); + + it('returns N/A for unknown metric', () => { + const stats = computeStats(['nonexistent'], [openIssue]); + expect(stats[0]).toEqual({ key: 'nonexistent', value: 'N/A' }); + }); + + it('handles empty items', () => { + const stats = computeStats(['total', 'open'], []); + expect(stats).toEqual([ + { key: 'Total', value: 0 }, + { key: 'Open', value: 0, color: 'green' }, + ]); + }); + + it('computes resolution percentiles via medianResolution', () => { + const stats = computeStats(['medianResolution'], [closedIssue]); + expect(stats[0].key).toBe('Median Resolution'); + expect(stats[0].value).toBe('1.0d'); + }); +}); + +// โ”€โ”€ buildHistogram โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + +describe('buildHistogram', () => { + it('buckets values into explicit boundaries', () => { + const values = [0.1, 0.3, 1, 5, 25, 50]; + const buckets = [0, 0.25, 0.5, 1, 2, 24, 48]; + const result = buildHistogram(values, buckets); + + expect(result[0]).toMatchObject({ label: '<15m', count: 1 }); + expect(result[1]).toMatchObject({ count: 1 }); + expect(result[result.length - 1].label).toContain('>'); + expect(result[result.length - 1].count).toBe(1); + }); + + it('returns zero counts for empty values', () => { + const result = buildHistogram([], [0, 1, 2]); + expect(result.every(b => b.count === 0)).toBe(true); + }); + + it('handles single bucket', () => { + const result = buildHistogram([5], [0]); + expect(result).toHaveLength(1); + expect(result[0].count).toBe(1); + }); + + it('places boundary values in the lower bucket (>= low, < high)', () => { + const result = buildHistogram([1], [0, 1, 2]); + expect(result[0].count).toBe(0); + expect(result[1].count).toBe(1); + expect(result[2].count).toBe(0); + }); +}); diff --git a/.github/dashboard/src/charts.ts b/.github/dashboard/src/charts.ts new file mode 100644 index 00000000..f743dd2f --- /dev/null +++ b/.github/dashboard/src/charts.ts @@ -0,0 +1,43 @@ +/* @strip */ +declare const Chart: { + new (canvas: HTMLCanvasElement, config: unknown): unknown; + getChart(canvas: HTMLCanvasElement): { resize(): void } | undefined; + defaults: { color: string; borderColor: string }; +}; +type InitCharts = (container: Element) => void; +interface Window { + initCharts: InitCharts; +} +/* @strip */ + +function initCanvas(canvas: HTMLCanvasElement): void { + if (Chart.getChart(canvas)) return; + const raw = canvas.getAttribute('data-chart'); + if (!raw) return; + try { + new Chart(canvas, JSON.parse(raw) as unknown); + } catch (e) { + console.error('Failed to initialize chart:', e); + } +} + +function initCharts(container: Element): void { + container.querySelectorAll('[data-chart]').forEach(c => { + if (Chart.getChart(c)) { + Chart.getChart(c)!.resize(); + } else { + initCanvas(c); + } + }); +} + +(window as unknown as Window).initCharts = initCharts; + +document.addEventListener('DOMContentLoaded', () => { + Chart.defaults.color = '#8b949e'; + Chart.defaults.borderColor = '#30363d'; + + document.querySelectorAll('[data-chart]').forEach(canvas => { + if ((canvas as HTMLElement).offsetParent !== null) initCanvas(canvas); + }); +}); diff --git a/.github/dashboard/src/components/CISection.tsx b/.github/dashboard/src/components/CISection.tsx new file mode 100644 index 00000000..23134e9f --- /dev/null +++ b/.github/dashboard/src/components/CISection.tsx @@ -0,0 +1,227 @@ +import { PALETTE } from '../palette.js'; +import type { CIData } from '../types.js'; + +function rateColor(pct: number): string { + return pct >= 90 ? PALETTE.green : pct >= 70 ? PALETTE.yellow : PALETTE.red; +} + +function PassRateCards({ overall, passRate }: { overall: number; passRate: Record }) { + return ( +
+
+
+ {overall}% +
+
Overall Pass Rate
+
+ {Object.entries(passRate).map(([name, rate]) => ( +
+
+ {rate}% +
+
{name}
+
+ ))} +
+ ); +} + +const CI_TAB_SCRIPT = ` +(function(){ + document.querySelectorAll('[data-ci-tabs]').forEach(function(container){ + container.querySelectorAll('.tab').forEach(function(btn){ + btn.onclick=function(){ + container.querySelectorAll('.tab').forEach(function(b){b.classList.remove('active')}); + btn.classList.add('active'); + container.querySelectorAll('.tab-panel').forEach(function(p){p.style.display='none'}); + container.querySelector('[data-panel="'+btn.dataset.idx+'"]').style.display=''; + }; + }); + }); +})(); +`; + +function buildTimelineChart(ci: CIData) { + return { + type: 'bar', + data: { + labels: ci.timeline.map(w => w.week), + datasets: [ + { + label: 'Pass', + data: ci.timeline.map(w => w.pass), + backgroundColor: 'rgba(63,185,80,0.7)', + borderRadius: 3, + }, + { + label: 'Fail', + data: ci.timeline.map(w => w.fail), + backgroundColor: 'rgba(248,81,73,0.7)', + borderRadius: 3, + }, + ], + }, + options: { + responsive: true, + plugins: { legend: { position: 'bottom', labels: { boxWidth: 10 } } }, + scales: { x: { stacked: true }, y: { stacked: true, beginAtZero: true } }, + }, + }; +} + +function buildDurationChart(ci: CIData) { + const jobs = Object.keys(ci.avgDuration); + return { + type: 'bar', + data: { + labels: jobs, + datasets: [ + { + data: jobs.map(j => ci.avgDuration[j]), + backgroundColor: 'rgba(88,166,255,0.7)', + borderRadius: 3, + }, + ], + }, + options: { + indexAxis: 'y', + responsive: true, + plugins: { legend: { display: false }, title: { display: true, text: 'Avg Duration (min)' } }, + }, + }; +} + +export default function CISection({ ci }: { ci: CIData }) { + const windows = ci.windows ?? {}; + const tabs = ['All Time', ...Object.keys(windows)]; + const allData: Record }> = { + 'All Time': { overallPassRate: ci.overallPassRate, passRate: ci.passRate }, + ...windows, + }; + + return ( + <> +
+
+ {tabs.map((t, j) => ( + + ))} +
+ {tabs.map((t, j) => { + const d = allData[t]; + return ( +
0 ? { display: 'none' } : undefined} data-panel={j}> + {d && } +
+ ); + })} + +
+ +
+
+

๐Ÿ“ˆ Pass/Fail Over Time

+ +
+
+ +
+
+

โŒ Most Failing Jobs

+ {ci.failingJobs.length > 0 ? ( + + + + + + + + + + + {ci.failingJobs.map(j => { + const cls = j.rate >= 20 ? 'b-red' : j.rate >= 10 ? 'b-yellow' : 'b-dim'; + return ( + + + + + + + ); + })} + +
JobFailuresTotal RunsFail Rate
{j.job}{j.failures}{j.total} + {j.rate}% +
+ ) : ( +

No failures!

+ )} +
+
+

๐Ÿ”„ Flaky Jobs (passโ†”fail flips)

+ {ci.flaky.length > 0 ? ( + + + + + + + + + {ci.flaky.map(f => ( + + + + + ))} + +
JobPass/Fail Flips
{f.job} + {f.flipCount} +
+ ) : ( +

No flaky jobs detected (threshold: 3+ flips)

+ )} +
+
+ +
+
+

๐Ÿ• Avg Job Duration

+ +
+
+

๐Ÿ”ฅ Recent Failures

+ {ci.recentFailures.length > 0 ? ( + + + + + + + + + + {ci.recentFailures.map(r => ( + + + + + + ))} + +
DateWorkflowFailed Jobs
{r.date}{r.workflow} + {r.failedJobs.map(j => ( + + {j} + + ))} +
+ ) : null} +
+
+ + ); +} diff --git a/.github/dashboard/src/components/ChartSection.tsx b/.github/dashboard/src/components/ChartSection.tsx new file mode 100644 index 00000000..2eb6bcc0 --- /dev/null +++ b/.github/dashboard/src/components/ChartSection.tsx @@ -0,0 +1,148 @@ +import { CHART_COLORS, PALETTE } from '../palette.js'; +import type { DistributionSection, SectionData, TimelineSection } from '../types.js'; + +function buildTimelineConfig(s: SectionData) { + const cfg = s.config as TimelineSection; + const timeline = s.timeline ?? []; + const labels = timeline.map(b => b.week); + const datasets = cfg.series.map((k, j) => { + const cum = k.startsWith('cumulative'); + return { + label: k.replace(/([A-Z])/g, ' $1').trim(), + data: timeline.map(b => (b[k] as number) || 0), + type: cum ? 'line' : 'bar', + backgroundColor: cum ? 'transparent' : j === 0 ? 'rgba(210,153,34,0.7)' : 'rgba(63,185,80,0.7)', + borderColor: cum ? PALETTE.accent : undefined, + borderWidth: cum ? 2 : 0, + pointRadius: cum ? 2 : undefined, + yAxisID: cum ? 'y1' : 'y', + order: cum ? 0 : 1, + borderRadius: cum ? 0 : 3, + }; + }); + return { + type: 'bar', + data: { labels, datasets }, + options: { + responsive: true, + interaction: { mode: 'index' }, + plugins: { legend: { position: 'bottom', labels: { boxWidth: 12, padding: 16 } } }, + scales: { + y: { beginAtZero: true, title: { display: true, text: 'Weekly' } }, + y1: { + position: 'right', + beginAtZero: true, + title: { display: true, text: 'Cumulative' }, + grid: { drawOnChartArea: false }, + }, + }, + }, + }; +} + +function buildDistributionConfig(s: SectionData) { + const cfg = s.config as DistributionSection; + const chart = s.chart; + if (!chart) return null; + if (cfg.chart === 'doughnut') { + return { + type: 'doughnut', + data: { + labels: chart.labels, + datasets: [{ data: chart.values, backgroundColor: CHART_COLORS.slice(0, chart.labels.length) }], + }, + options: { + responsive: true, + plugins: { legend: { position: 'right', labels: { boxWidth: 10, padding: 8, font: { size: 11 } } } }, + }, + }; + } + return { + type: 'bar', + data: { + labels: chart.labels, + datasets: [ + { + data: chart.values, + backgroundColor: chart.colors ?? CHART_COLORS.slice(0, chart.labels.length), + borderRadius: 3, + }, + ], + }, + options: { + indexAxis: cfg.orientation === 'horizontal' ? 'y' : 'x', + responsive: true, + plugins: { legend: { display: false } }, + }, + }; +} + +function buildHistogramConfig(s: SectionData) { + if (s.histogramGrouped) { + const g = s.histogramGrouped; + const keys = Object.keys(g); + const first = g[keys[0] ?? '']; + const labels = (first ?? []).map(b => b.label); + return { + type: 'bar', + data: { + labels, + datasets: keys.map((k, j) => ({ + label: k, + data: (g[k] ?? []).map(b => b.count), + backgroundColor: CHART_COLORS[j % CHART_COLORS.length], + borderRadius: 2, + })), + }, + options: { + responsive: true, + plugins: { legend: { position: 'bottom', labels: { boxWidth: 10 } } }, + scales: { y: { beginAtZero: true } }, + }, + }; + } + if (s.histogram) { + const n = s.histogram.length; + const bg = s.histogram.map((_, j) => { + const t = n > 1 ? j / (n - 1) : 0; + return t < 0.25 + ? 'rgba(63,185,80,0.7)' + : t < 0.5 + ? 'rgba(88,166,255,0.7)' + : t < 0.75 + ? 'rgba(210,153,34,0.7)' + : 'rgba(248,81,73,0.7)'; + }); + return { + type: 'bar', + data: { + labels: s.histogram.map(b => b.label), + datasets: [{ data: s.histogram.map(b => b.count), backgroundColor: bg, borderRadius: 3 }], + }, + options: { + responsive: true, + plugins: { legend: { display: false } }, + scales: { y: { beginAtZero: true } }, + }, + }; + } + return null; +} + +function buildChartConfig(s: SectionData) { + const type = s.config.type; + if (type === 'timeline') return buildTimelineConfig(s); + if (type === 'distribution') return buildDistributionConfig(s); + if (type === 'histogram') return buildHistogramConfig(s); + return null; +} + +export default function ChartSection({ sectionData, index }: { sectionData: SectionData; index: number }) { + const chartConfig = buildChartConfig(sectionData); + if (!chartConfig) return
; + return ( +
+ +
+ ); +} diff --git a/.github/dashboard/src/components/Page.tsx b/.github/dashboard/src/components/Page.tsx new file mode 100644 index 00000000..15c100e7 --- /dev/null +++ b/.github/dashboard/src/components/Page.tsx @@ -0,0 +1,147 @@ +import { PALETTE } from '../palette.js'; +import type { DashboardConfig, PageData, SectionData } from '../types.js'; +import Section from './Section.js'; + +const CSS = ` +:root{--bg:${PALETTE.bg};--card:${PALETTE.card};--text:${PALETTE.text};--border:${PALETTE.border};--dim:${PALETTE.dim};--accent:${PALETTE.accent};--green:${PALETTE.green};--red:${PALETTE.red};--yellow:${PALETTE.yellow};--purple:${PALETTE.purple}} +*{margin:0;padding:0;box-sizing:border-box} +body{background:var(--bg);color:var(--text);font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Helvetica,Arial,sans-serif;font-size:14px;padding:24px;max-width:1400px;margin:0 auto} +h1{font-size:22px;margin-bottom:4px} +.sub{color:var(--dim);font-size:13px;margin-bottom:16px} +nav{display:flex;gap:8px;margin-bottom:20px;padding:8px 12px;background:var(--card);border-radius:8px;border:1px solid var(--border)} +nav a{color:var(--dim);text-decoration:none;padding:6px 14px;border-radius:6px;font-weight:600;font-size:13px} +nav a:hover{color:var(--text);background:rgba(255,255,255,.04)} +nav a.active{color:var(--accent);background:rgba(31,111,235,.12)} +.grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(340px,1fr));gap:16px;margin-bottom:16px} +.card{background:var(--card);border:1px solid var(--border);border-radius:8px;padding:16px} +.card h2{font-size:14px;color:var(--accent);margin-bottom:12px;padding-bottom:8px;border-bottom:1px solid var(--border);position:relative} +.wide{grid-column:1/-1} +.row{display:flex;flex-wrap:wrap;gap:12px;margin-bottom:10px} +.st{text-align:center;flex:1;min-width:90px;padding:8px 4px} +.st .v{font-size:26px;font-weight:700;line-height:1.2} +.st .l{font-size:11px;color:var(--dim);margin-top:2px} +.sm .v{font-size:18px} +.green{color:var(--green)}.red{color:var(--red)}.yellow{color:var(--yellow)}.accent{color:var(--accent)}.purple{color:var(--purple)}.dim{color:var(--dim)} +table{width:100%;border-collapse:collapse;font-size:13px} +th{text-align:left;padding:6px 8px;border-bottom:1px solid var(--border);color:var(--dim);font-weight:600} +td{padding:6px 8px;border-bottom:1px solid #21262d} +tr:hover{background:rgba(255,255,255,.02)} +a{color:var(--accent);text-decoration:none} +.b{display:inline-block;padding:2px 8px;border-radius:12px;font-size:11px;font-weight:600} +.b-green{background:rgba(63,185,80,.15);color:var(--green)} +.b-red{background:rgba(248,81,73,.15);color:var(--red)} +.b-yellow{background:rgba(210,153,34,.15);color:var(--yellow)} +.b-blue{background:rgba(88,166,255,.15);color:var(--accent)} +.b-dim{background:rgba(139,148,158,.15);color:var(--dim)} +.b-purple{background:rgba(188,140,255,.15);color:var(--purple)} +footer{text-align:center;color:#484f58;font-size:12px;margin-top:24px;padding:16px} +canvas{max-height:300px} +.copy-btn{position:absolute;right:0;top:-2px;background:none;border:none;color:var(--dim);cursor:pointer;font-size:14px;padding:4px 8px;border-radius:4px} +.copy-btn:hover{color:var(--text);background:rgba(255,255,255,.06)} +.copied{color:var(--green)!important} +.tabs{display:flex;gap:4px;margin-bottom:12px;border-bottom:1px solid var(--border);padding-bottom:8px} +.tab{background:none;border:none;color:var(--dim);font-size:12px;font-weight:600;padding:4px 12px;border-radius:4px;cursor:pointer} +.tab:hover{color:var(--text);background:rgba(255,255,255,.04)} +.tab.active{color:var(--accent);background:rgba(31,111,235,.12)} +.extra{margin-top:12px} +.extra h4{font-size:13px;color:var(--dim);margin-bottom:8px} +`; + +const COPY_SCRIPT = ` +document.querySelectorAll('.copy-btn').forEach(function(btn){ + btn.onclick=function(){ + var card=btn.closest('.card'); + var table=card.querySelector('table'); + var text=''; + if(table){ + var rows=[].slice.call(table.querySelectorAll('tr')); + text=rows.map(function(r){return [].slice.call(r.querySelectorAll('th,td')).map(function(c){return c.textContent.trim()}).join(' | ')}).join('\\n'); + }else{text=card.textContent.replace(/๐Ÿ“‹/g,'').trim()} + navigator.clipboard.writeText(text).then(function(){btn.textContent='โœ“';btn.classList.add('copied');setTimeout(function(){btn.textContent='๐Ÿ“‹';btn.classList.remove('copied')},1500)}).catch(function(){}); + }; +}); +`; + +const GLOBAL_TAB_SCRIPT = ` +(function(){ + var container = document.querySelector('[data-global-tabs]'); + if (!container) return; + container.querySelectorAll('.tab').forEach(function(btn){ + btn.onclick = function(){ + container.querySelectorAll('.tab').forEach(function(b){ b.classList.remove('active'); }); + btn.classList.add('active'); + document.querySelectorAll('[data-window-panel]').forEach(function(p){ p.style.display = 'none'; }); + var target = document.querySelector('[data-window-panel="' + btn.dataset.idx + '"]'); + if (target) { target.style.display = ''; if (window.initCharts) window.initCharts(target); } + }; + }); +})(); +`; + +function WindowedSections({ page, repo }: { page: PageData; repo: string }) { + const windows = page.windowedSections ?? {}; + const tabs = ['All Time', ...Object.keys(windows)]; + const allSections: Record = { 'All Time': page.sections, ...windows }; + + return ( + <> +
+ {tabs.map((t, j) => ( + + ))} +
+ {tabs.map((t, j) => ( +
0 ? { display: 'none' } : undefined}> +
+ {allSections[t].map((s, i) => ( +
+ ))} +
+
+ ))} + + + ); +} + +export default function Page({ page, config }: { page: PageData; config: DashboardConfig }) { + const repoName = config.repo.split('/')[1]; + return ( + <> + + + + {`${page.title} โ€” ${config.repo} Dashboard`} + + + + ); +} diff --git a/.github/dashboard/src/components/Section.tsx b/.github/dashboard/src/components/Section.tsx new file mode 100644 index 00000000..ef4b7403 --- /dev/null +++ b/.github/dashboard/src/components/Section.tsx @@ -0,0 +1,162 @@ +import { CHART_COLORS } from '../palette.js'; +import type { SectionData, TrendSection as TrendSectionConfig } from '../types.js'; +import CISection from './CISection.js'; +import ChartSection from './ChartSection.js'; +import StatsSection from './StatsSection.js'; +import TableSectionComponent from './TableSection.js'; +import TermFrequencySection from './TermFrequencySection.js'; + +function sectionTitle(s: SectionData): string { + const c = s.config; + switch (c.type) { + case 'stats': + return '๐Ÿ“Š Overview'; + case 'timeline': + return '๐Ÿ“ˆ Activity Over Time'; + case 'distribution': { + const map: Record = { + labels: '๐Ÿท๏ธ Issues by Label', + age: '๐Ÿ“… Open Issue Age', + sizeLabel: '๐Ÿ“ PR Size Distribution', + bucket: '๐Ÿ“Š Open PR Status', + linkedIssuePriority: '๐ŸŽฏ PR Priority (from linked issues)', + }; + return map[c.field] ?? `๐Ÿ“Š ${c.field}`; + } + case 'histogram': + return c.title ? `โฑ๏ธ ${c.title}` : `โฑ๏ธ ${c.field}`; + case 'table': + return `${c.id === 'stale' ? '๐ŸงŠ' : c.id === 'engagement' ? '๐Ÿ’ฌ' : '๐Ÿ“‹'} ${c.title}`; + case 'termFrequency': + return c.title ?? '๐Ÿ” Common Terms in Unlabeled Issues'; + case 'ci': + return '๐Ÿงช CI / Test Health'; + case 'trend': + return `๐Ÿ“ˆ ${c.title}`; + case 'weeklyTable': + return `๐Ÿ“… ${c.title}`; + } +} + +const WIDE_TYPES = new Set(['stats', 'timeline', 'table', 'termFrequency', 'ci', 'weeklyTable']); + +function TrendChart({ + trend, + config, +}: { + trend: { weeks: string[]; series: Record }; + config: TrendSectionConfig; +}) { + const chartConfig = { + type: 'line' as const, + data: { + labels: trend.weeks, + datasets: Object.entries(trend.series).map(([name, data], j) => ({ + label: name, + data, + borderColor: CHART_COLORS[j % CHART_COLORS.length], + backgroundColor: 'transparent', + borderWidth: 2, + pointRadius: 2, + tension: 0.3, + })), + }, + options: { + responsive: true, + plugins: { legend: { position: 'bottom' as const, labels: { boxWidth: 10 } } }, + scales: { + y: { + beginAtZero: true, + title: { + display: true, + text: config.title.includes('days') + ? config.aggregate === 'median' + ? 'Median (days)' + : 'Avg (days)' + : config.aggregate === 'median' + ? 'Median (hours)' + : 'Avg (hours)', + }, + }, + }, + }, + }; + return ; +} + +function WeeklyTable({ data }: { data: { weeks: string[]; rows: Record } }) { + return ( +
+ + + + + {data.weeks.map(w => ( + + ))} + + + + {Object.entries(data.rows).map(([metric, values]) => ( + + + {values.map(v => ( + + ))} + + ))} + +
Metric{w}
+ {metric} + {v}
+
+ ); +} + +function SectionContent({ sectionData, index, repo }: { sectionData: SectionData; index: number; repo: string }) { + const type = sectionData.config.type; + if (type === 'stats' && sectionData.stats) { + return ; + } + if (type === 'timeline' || type === 'distribution' || type === 'histogram') { + return ; + } + if (type === 'table' && sectionData.table && sectionData.config.type === 'table') { + return ; + } + if (type === 'ci' && sectionData.ci) { + return ; + } + if (type === 'termFrequency') { + return ; + } + if (type === 'trend' && sectionData.trend) { + return ; + } + if (type === 'weeklyTable' && sectionData.weeklyTable) { + return ; + } + return null; +} + +export default function Section({ + sectionData, + index, + repo, +}: { + sectionData: SectionData; + index: number; + repo: string; +}) { + const wide = WIDE_TYPES.has(sectionData.config.type); + const title = sectionTitle(sectionData); + return ( +
+

+ {title} + +

+ +
+ ); +} diff --git a/.github/dashboard/src/components/StatsSection.tsx b/.github/dashboard/src/components/StatsSection.tsx new file mode 100644 index 00000000..206b0ab4 --- /dev/null +++ b/.github/dashboard/src/components/StatsSection.tsx @@ -0,0 +1,38 @@ +import { PALETTE } from '../palette.js'; +import type { StatValue } from '../types.js'; + +const COLOR_MAP: Record = { + green: PALETTE.green, + red: PALETTE.red, + yellow: PALETTE.yellow, + accent: PALETTE.accent, + purple: PALETTE.purple, + dim: PALETTE.dim, +}; + +function StatRow({ items, small }: { items: StatValue[]; small?: boolean }) { + return ( +
+ {items.map(st => ( +
+
+ {st.value} +
+
+ {st.key} + {st.sublabel ? ` (${st.sublabel})` : ''} +
+
+ ))} +
+ ); +} + +export default function StatsSection({ stats }: { stats: StatValue[] }) { + return ( + <> + + {stats.length > 6 && } + + ); +} diff --git a/.github/dashboard/src/components/TableSection.tsx b/.github/dashboard/src/components/TableSection.tsx new file mode 100644 index 00000000..814dbd88 --- /dev/null +++ b/.github/dashboard/src/components/TableSection.tsx @@ -0,0 +1,91 @@ +import type { TableRow, TableSection as TableSectionConfig } from '../types.js'; + +const PRIORITY_COLORS: Record = { + P0: 'b-red', + P1: 'b-yellow', + P2: 'b-blue', + bug: 'b-red', + enhancement: 'b-blue', +}; + +const BUCKET_COLORS: Record = { + 'needs-re-review': 'b-red', + 'needs-initial-review': 'b-yellow', + 'waiting-on-author': 'b-dim', + approved: 'b-green', + closed: 'b-dim', +}; + +function CellValue({ col, value, repo, isPR }: { col: string; value: unknown; repo: string; isPR: boolean }) { + const str = typeof value === 'string' || typeof value === 'number' ? String(value) : ''; + if (col === 'number') { + const url = `https://github.com/${repo}/${isPR ? 'pull' : 'issues'}/${str}`; + return #{String(value)}; + } + if (col === 'labels' && Array.isArray(value)) { + if (value.length === 0) return unlabeled; + return ( + <> + {value.map((l: string) => ( + + {l} + + ))} + + ); + } + if (col === 'state') { + const cls = value === 'open' ? 'b-green' : 'b-dim'; + return {str}; + } + if (col === 'draft') { + return value ? draft : null; + } + if (col === 'priority') { + if (!value) return โ€”; + const cls = PRIORITY_COLORS[str] ?? 'b-dim'; + return {str}; + } + if (col === 'bucket') { + const cls = BUCKET_COLORS[str] ?? 'b-dim'; + return {str}; + } + if (col === 'age') return <>{String(value)}d; + return <>{str}; +} + +export default function TableSection({ + config, + table, + repo, +}: { + config: TableSectionConfig; + table: TableRow[]; + repo: string; +}) { + const isPR = config.columns.includes('draft'); + return ( +
+ + + + {config.columns.map(c => ( + + ))} + + + + {table.map(row => ( + + {config.columns.map(col => ( + + ))} + + ))} + +
{c}
+ +
+
+ ); +} diff --git a/.github/dashboard/src/components/TermFrequencySection.tsx b/.github/dashboard/src/components/TermFrequencySection.tsx new file mode 100644 index 00000000..84e67cb4 --- /dev/null +++ b/.github/dashboard/src/components/TermFrequencySection.tsx @@ -0,0 +1,42 @@ +import type { TermCount } from '../types.js'; + +export default function TermFrequencySection({ terms, unusedLabels }: { terms: TermCount[]; unusedLabels: string[] }) { + return ( + <> + {terms.length > 0 && ( +
+ + + + + + + + + {terms.map(t => ( + + + + + ))} + +
TermOccurrences in Unlabeled Issues
+ {t.term} + {t.count}
+
+ )} + {unusedLabels.length > 0 && ( +
+

Defined but Unused Labels

+
+ {unusedLabels.map(l => ( + + {l} + + ))} +
+
+ )} + + ); +} diff --git a/.github/dashboard/src/components/index.ts b/.github/dashboard/src/components/index.ts new file mode 100644 index 00000000..c93a7582 --- /dev/null +++ b/.github/dashboard/src/components/index.ts @@ -0,0 +1 @@ +export { default as Page } from './Page.js'; diff --git a/.github/dashboard/src/config.ts b/.github/dashboard/src/config.ts new file mode 100644 index 00000000..a81bf4ce --- /dev/null +++ b/.github/dashboard/src/config.ts @@ -0,0 +1,157 @@ +import type { DashboardConfig } from './types.js'; + +export const config: DashboardConfig = { + repo: 'aws/agentcore-cli', + outputDir: 'site/dashboard', + pages: [ + { + id: 'issues', + title: 'Issues', + dataSource: 'issues', + windows: [ + { label: 'Past 24h', days: 1 }, + { label: 'Past 7 days', days: 7 }, + { label: 'Past 30 days', days: 30 }, + ], + sections: [ + { + type: 'stats', + metrics: [ + 'total', + 'open', + 'closed', + 'weeklyRate', + 'unlabeled', + 'unassigned', + 'medianResolution', + 'avgResolution', + 'p90Resolution', + 'completed', + 'notPlanned', + 'duplicates', + ], + }, + { type: 'timeline', bucket: 'week', series: ['opened', 'closed', 'cumulativeOpen'] }, + // Row: label pie + age bar + { type: 'distribution', field: 'labels', chart: 'doughnut' }, + { type: 'distribution', field: 'age', chart: 'bar', orientation: 'horizontal' }, + // Row: open age trend + resolution histogram + { type: 'trend', title: 'Avg Open Issue Age Over Time (days)', fields: ['openAgeDays'], aggregate: 'avg' }, + { + type: 'histogram', + field: 'resolutionHours', + buckets: [0, 1, 4, 8, 12, 24, 48, 72, 168, 336, 720], + groupBy: 'labels', + }, + // Tables + { + type: 'table', + id: 'engagement', + title: 'Most Discussed Open Issues', + filter: { state: 'open' }, + columns: ['number', 'title', 'comments', 'reactions', 'state'], + limit: 10, + }, + { + type: 'table', + id: 'stale', + title: 'Stale Open Issues (>14 days, 0 comments)', + filter: { state: 'open', minAgeDays: 14, maxComments: 0 }, + columns: ['number', 'title', 'age', 'labels'], + limit: 20, + }, + { type: 'termFrequency', filter: { labeled: false }, minCount: 3 }, + { + type: 'weeklyTable', + title: 'Weekly Summary (recent 8 weeks)', + metrics: ['opened', 'closed', 'net', 'medianResolution'], + weeks: 8, + }, + ], + }, + { + id: 'prs', + title: 'Pull Requests', + dataSource: 'prs', + windows: [ + { label: 'Past 24h', days: 1 }, + { label: 'Past 7 days', days: 7 }, + { label: 'Past 30 days', days: 30 }, + ], + sections: [ + { + type: 'stats', + metrics: [ + 'total', + 'merged', + 'closedNoMerge', + 'open', + 'drafts', + 'mergeRate', + 'medianTTFR', + 'avgTTFR', + 'p90TTFR', + 'medianTTM', + 'avgTTM', + 'p90TTM', + ], + }, + { type: 'timeline', bucket: 'week', series: ['opened', 'merged', 'cumulativeOpen'] }, + // Row: PR status + open age trend + { type: 'distribution', field: 'bucket', chart: 'doughnut' }, + { type: 'trend', title: 'Avg Open PR Age Over Time (days)', fields: ['openAgeDays'], aggregate: 'avg' }, + // Row: TTFR + TTM histograms + { + type: 'histogram', + field: 'ttfrHours', + title: 'Time to First Review', + buckets: [0, 0.25, 0.5, 1, 2, 4, 8, 12, 24, 48, 72, 168], + }, + { + type: 'histogram', + field: 'ttmHours', + title: 'Time to Merge', + buckets: [0, 0.25, 0.5, 1, 2, 4, 8, 12, 24, 48, 72, 168], + }, + // Row: size pie + TTM by size + { type: 'distribution', field: 'sizeLabel', chart: 'doughnut' }, + { + type: 'histogram', + field: 'ttmHours', + title: 'Time to Merge by Size', + buckets: [0, 0.25, 0.5, 1, 2, 4, 8, 12, 24, 48, 72, 168], + groupBy: 'sizeLabel', + }, + // Tables + { + type: 'table', + id: 'stale', + title: 'Stale Open PRs (>7 days)', + filter: { state: 'open', minAgeDays: 7 }, + columns: ['number', 'title', 'age', 'author', 'priority', 'lastActivity', 'draft'], + limit: 15, + }, + { + type: 'weeklyTable', + title: 'Weekly Summary (recent 8 weeks)', + metrics: ['opened', 'merged', 'net', 'medianTTFR', 'medianTTM'], + weeks: 8, + }, + ], + }, + { + id: 'ci', + title: 'CI / Tests', + dataSource: 'ci', + sections: [ + { + type: 'ci', + workflows: ['Build and Test', 'E2E Tests (Full Suite)', 'E2E Tests'], + branch: 'main', + // 900 runs รท 3 workflows = 300 per workflow, ~120 failed-run job fetches + maxRuns: 900, + }, + ], + }, + ], +}; diff --git a/.github/dashboard/src/generate.tsx b/.github/dashboard/src/generate.tsx new file mode 100644 index 00000000..6ef98e1a --- /dev/null +++ b/.github/dashboard/src/generate.tsx @@ -0,0 +1,56 @@ +import { Page } from './components/index.js'; +import { config } from './config.js'; +import { fetchCIRuns, fetchIssues, fetchPRs } from './github.js'; +import { computePage, parseIssues, parsePRs } from './transform.js'; +import { transformSync } from 'esbuild'; +import { copyFileSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'; +import { dirname, join } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const outDir = join(__dirname, '..', config.outputDir); + +function main(): void { + mkdirSync(outDir, { recursive: true }); + + // Copy chart.js and charts.js to output dir + const chartSrc = join(__dirname, '..', 'node_modules', 'chart.js', 'dist', 'chart.umd.js'); + copyFileSync(chartSrc, join(outDir, 'chart.js')); + + const chartsClientSrc = join(__dirname, '..', 'src', 'charts.ts'); + const chartsTs = readFileSync(chartsClientSrc, 'utf-8').replace(/\/\* @strip \*\/[\s\S]*?\/\* @strip \*\/\n?/g, ''); + const chartsClient = transformSync(chartsTs, { loader: 'ts', target: 'es2020' }).code; + writeFileSync(join(outDir, 'charts.js'), chartsClient); + + const rawIssues = fetchIssues(config.repo); + const issues = parseIssues(rawIssues); + + const rawPRs = fetchPRs(config.repo); + const prs = parsePRs(rawPRs); + + // Fetch CI runs only if a CI page exists + const ciPage = config.pages.find(p => p.dataSource === 'ci'); + const ciSection = ciPage?.sections.find(s => s.type === 'ci'); + const ciRuns = ciSection + ? fetchCIRuns(config.repo, ciSection.workflows, ciSection.branch, ciSection.maxRuns) + : undefined; + + for (const page of config.pages) { + const data = computePage(page, issues, prs, ciRuns); + const markup = String(); + const html = `${markup}`; + const outPath = join(outDir, `${page.id}.html`); + writeFileSync(outPath, html); + console.error(` โ†’ ${outPath}`); + } + + // Index redirect to first page + writeFileSync( + join(outDir, 'index.html'), + `` + ); + console.error(` โ†’ ${join(outDir, 'index.html')}`); + console.error('Done!'); +} + +main(); diff --git a/.github/dashboard/src/github.ts b/.github/dashboard/src/github.ts new file mode 100644 index 00000000..4174cfe2 --- /dev/null +++ b/.github/dashboard/src/github.ts @@ -0,0 +1,158 @@ +import type { GHIssue, GHPullRequestNode, RunConclusion, WorkflowJob, WorkflowRun } from './types.js'; +import { execFileSync } from 'node:child_process'; + +// 50MB buffer handles large paginated API responses (~10MB typical for 900 CI runs) +const EXEC_OPTS = { encoding: 'utf-8' as const, maxBuffer: 50 * 1024 * 1024 }; + +function ghApi(...args: string[]): string { + try { + return execFileSync('gh', ['api', ...args], EXEC_OPTS); + } catch (err) { + const msg = err instanceof Error ? err.message : String(err); + throw new Error(`gh api call failed (args: ${args.join(' ')}): ${msg}`); + } +} + +function parseJSON(raw: string, context: string): T { + try { + return JSON.parse(raw) as T; + } catch (err) { + const msg = err instanceof Error ? err.message : String(err); + throw new Error(`Failed to parse JSON (${context}): ${msg}`); + } +} + +export function fetchIssues(repo: string): GHIssue[] { + process.stderr.write(`Fetching issues for ${repo}...\n`); + const raw = ghApi('--paginate', `/repos/${repo}/issues?state=all&per_page=100`); + const items = parseJSON(raw.trim(), 'fetchIssues'); + const issues = items.filter(i => !i.pull_request); + process.stderr.write(` Fetched ${issues.length} issues\n`); + return issues; +} + +export function fetchPRs(repo: string): GHPullRequestNode[] { + const [owner, name] = repo.split('/'); + const prs: GHPullRequestNode[] = []; + let cursor: string | null = null; + let page = 0; + + const query = ` + query($owner: String!, $name: String!, $cursor: String) { + repository(owner: $owner, name: $name) { + pullRequests(first: 100, after: $cursor, orderBy: {field: CREATED_AT, direction: DESC}) { + pageInfo { hasNextPage endCursor } + nodes { + number title state createdAt mergedAt closedAt isDraft + author { login } + labels(first: 10) { nodes { name } } + reviews(first: 20) { nodes { author { login } state submittedAt } } + commits(last: 1) { nodes { commit { committedDate } } } + closingIssuesReferences(first: 3) { nodes { number labels(first: 5) { nodes { name } } } } + } + } + } + }`; + + for (;;) { + page++; + process.stderr.write(`Fetching PRs page ${page}...\n`); + const args = ['graphql', '-F', `owner=${owner}`, '-F', `name=${name}`, '-f', `query=${query}`]; + if (cursor) { + args.push('-f', `cursor=${cursor}`); + } + const raw = ghApi(...args); + const resp = parseJSON<{ + data: { + repository: { + pullRequests: { + pageInfo: { hasNextPage: boolean; endCursor: string }; + nodes: GHPullRequestNode[]; + }; + }; + }; + }>(raw, `fetchPRs page ${page}`); + const data = resp.data.repository.pullRequests; + prs.push(...data.nodes); + if (!data.pageInfo.hasNextPage) break; + cursor = data.pageInfo.endCursor; + } + + const filtered = prs.filter(pr => pr.author?.login !== 'github-actions[bot]'); + process.stderr.write(` Fetched ${filtered.length} PRs (filtered from ${prs.length})\n`); + return filtered; +} + +interface GHWorkflow { + id: number; + name: string; +} +interface GHRunsResponse { + workflow_runs: { id: number; conclusion: RunConclusion | null; created_at: string }[]; +} +interface GHJobsResponse { + jobs: { name: string; conclusion: RunConclusion | null; started_at: string; completed_at: string }[]; +} + +export function fetchCIRuns(repo: string, workflowNames: string[], branch: string, maxRuns: number): WorkflowRun[] { + process.stderr.write(`Fetching CI runs for ${branch}...\n`); + const wfList = parseJSON( + ghApi(`/repos/${repo}/actions/workflows`, '--jq', '.workflows'), + 'fetchCIRuns workflows' + ); + const matched = wfList.filter(w => workflowNames.includes(w.name)); + if (matched.length === 0) { + throw new Error( + `No workflows found matching: ${workflowNames.join(', ')}\nAvailable: ${wfList.map(w => w.name).join(', ')}` + ); + } + const runs: WorkflowRun[] = []; + // Distribute maxRuns evenly across workflows, staying under GitHub API rate limit (5000/hour) + const perWf = Math.ceil(maxRuns / matched.length); + + for (const wf of matched) { + process.stderr.write(` ${wf.name}...\n`); + let fetched = 0; + let page = 1; + while (fetched < perWf) { + const resp = parseJSON( + ghApi(`/repos/${repo}/actions/workflows/${wf.id}/runs?branch=${branch}&per_page=100&page=${page}`), + `fetchCIRuns runs page ${page}` + ); + if (resp.workflow_runs.length === 0) break; + for (const run of resp.workflow_runs) { + if (fetched >= perWf) break; + let jobs: WorkflowJob[] = []; + if (run.conclusion === 'failure') { + const jobsResp = parseJSON( + ghApi(`/repos/${repo}/actions/runs/${run.id}/jobs`), + `fetchCIRuns jobs for run ${run.id}` + ); + jobs = jobsResp.jobs.map( + (j): WorkflowJob => ({ + name: j.name, + conclusion: j.conclusion ?? 'in_progress', + durationMin: + j.completed_at && j.started_at + ? Math.round(((new Date(j.completed_at).getTime() - new Date(j.started_at).getTime()) / 60000) * 10) / + 10 + : 0, + }) + ); + } + runs.push({ + id: run.id, + workflowName: wf.name, + conclusion: run.conclusion ?? 'in_progress', + created: new Date(run.created_at), + jobs, + }); + fetched++; + } + process.stderr.write(` ...${fetched} runs\n`); + page++; + } + } + process.stderr.write(` ${runs.length} CI runs fetched\n`); + return runs; +} diff --git a/.github/dashboard/src/palette.ts b/.github/dashboard/src/palette.ts new file mode 100644 index 00000000..a5c83cc8 --- /dev/null +++ b/.github/dashboard/src/palette.ts @@ -0,0 +1,25 @@ +export const PALETTE = { + bg: '#0d1117', + card: '#161b22', + text: '#e6edf3', + border: '#30363d', + dim: '#8b949e', + accent: '#58a6ff', + green: '#3fb950', + red: '#f85149', + yellow: '#d29922', + purple: '#bc8cff', +} as const; + +export const CHART_COLORS = [ + PALETTE.accent, + PALETTE.green, + PALETTE.red, + PALETTE.yellow, + PALETTE.purple, + '#f778ba', + '#79c0ff', + '#7ee787', + '#ffa657', + '#ff7b72', +]; diff --git a/.github/dashboard/src/transform.ts b/.github/dashboard/src/transform.ts new file mode 100644 index 00000000..14f7b12e --- /dev/null +++ b/.github/dashboard/src/transform.ts @@ -0,0 +1,835 @@ +import type { + CIData, + ChartData, + GHIssue, + GHPullRequestNode, + HistogramBucket, + Issue, + PageConfig, + PageData, + PullRequest, + SectionData, + StatValue, + TableRow, + TableSection, + TermCount, + WeekBucket, + WorkflowRun, +} from './types.js'; + +const MS_PER_HOUR = 3_600_000; +const MS_PER_DAY = 86_400_000; + +export function formatHours(h: number): string { + if (h < 1) return `${Math.round(h * 60)}m`; + if (h < 24) return `${h.toFixed(1)}h`; + return `${(h / 24).toFixed(1)}d`; +} + +export function percentiles(vals: number[]): { median: number; avg: number; p90: number } { + if (vals.length === 0) return { median: 0, avg: 0, p90: 0 }; + const sorted = [...vals].sort((a, b) => a - b); + const median = sorted[Math.floor(sorted.length / 2)] ?? 0; + const avg = sorted.reduce((s, v) => s + v, 0) / sorted.length; + const p90 = sorted[Math.floor(sorted.length * 0.9)] ?? sorted[sorted.length - 1] ?? 0; + return { median, avg, p90 }; +} + +function ageDays(created: Date): number { + return (Date.now() - created.getTime()) / MS_PER_DAY; +} + +function formatRelativeTime(d: Date): string { + const diffMs = Date.now() - d.getTime(); + const hours = diffMs / MS_PER_HOUR; + if (hours < 1) return `${Math.round(hours * 60)}m ago`; + if (hours < 24) return `${Math.round(hours)}h ago`; + return `${Math.round(hours / 24)}d ago`; +} + +function isIssue(item: Issue | PullRequest): item is Issue { + return 'stateReason' in item; +} + +// โ”€โ”€ Parsers โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + +export function parseIssues(raw: GHIssue[]): Issue[] { + return raw + .filter(r => !r.pull_request) + .map(r => ({ + number: r.number, + title: r.title, + state: r.state.toLowerCase() as 'open' | 'closed', + created: new Date(r.created_at), + closed: r.closed_at ? new Date(r.closed_at) : null, + labels: r.labels.map(l => l.name), + assignees: r.assignees.map(a => a.login), + comments: r.comments, + reactions: r.reactions.total_count, + stateReason: r.state_reason, + closedBy: r.closed_by?.login ?? null, + author: r.user.login, + authorType: r.author_association, + })); +} + +export function parsePRs(raw: GHPullRequestNode[]): PullRequest[] { + return raw.map(r => { + const created = new Date(r.createdAt); + const sortedReviews = r.reviews.nodes + .filter(rv => rv.submittedAt) + .sort((a, b) => new Date(a.submittedAt!).getTime() - new Date(b.submittedAt!).getTime()); + const firstReview = sortedReviews[0]; + const lastReview = sortedReviews[sortedReviews.length - 1]; + const ttfrHours = firstReview?.submittedAt + ? (new Date(firstReview.submittedAt).getTime() - created.getTime()) / MS_PER_HOUR + : null; + const ttmHours = r.mergedAt ? (new Date(r.mergedAt).getTime() - created.getTime()) / MS_PER_HOUR : null; + const lastCommitNode = r.commits.nodes[0]; + const lastCommitDate = lastCommitNode ? new Date(lastCommitNode.commit.committedDate) : null; + const lastReviewDate = lastReview?.submittedAt ? new Date(lastReview.submittedAt) : null; + const priorityRank: Record = { P0: 0, P1: 1, P2: 2, P3: 3, bug: 4, enhancement: 5 }; + const priorityMatch = r.closingIssuesReferences.nodes + .flatMap(issue => issue.labels.nodes) + .flatMap(l => + Object.entries(priorityRank) + .filter(([prefix]) => l.name.startsWith(prefix)) + .map(([, rank]) => ({ name: l.name, rank })) + ) + .reduce<{ name: string; rank: number } | null>((best, cur) => (!best || cur.rank < best.rank ? cur : best), null); + const linkedIssuePriority = priorityMatch?.name ?? null; + const allApproved = sortedReviews.length > 0 && sortedReviews.every(rv => rv.state === 'APPROVED'); + const state: 'open' | 'closed' = r.state === 'OPEN' ? 'open' : 'closed'; + let bucket: PullRequest['bucket']; + if (state === 'closed') { + bucket = 'closed'; + } else if (!lastReviewDate) { + bucket = 'needs-initial-review'; + } else if (lastCommitDate && lastCommitDate > lastReviewDate) { + bucket = 'needs-re-review'; + } else if (allApproved) { + bucket = 'approved'; + } else { + bucket = 'waiting-on-author'; + } + return { + number: r.number, + title: r.title, + state, + created, + merged: r.mergedAt ? new Date(r.mergedAt) : null, + draft: r.isDraft, + author: r.author?.login ?? 'ghost', + labels: r.labels.nodes.map(l => l.name), + ttfrHours, + ttmHours, + reviewers: [ + ...new Set( + r.reviews.nodes + .filter(rv => rv.author?.login && rv.author.login !== (r.author?.login ?? 'ghost')) + .map(rv => rv.author!.login) + ), + ], + lastCommitDate, + lastReviewDate, + linkedIssuePriority, + bucket, + }; + }); +} + +// โ”€โ”€ Stats โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + +export function computeStats(metrics: string[], items: (Issue | PullRequest)[]): StatValue[] { + const issues = items.filter(isIssue); + const prs = items.filter((i): i is PullRequest => !isIssue(i)); + + const resolutionHours = issues + .filter(i => i.closed) + .map(i => (i.closed!.getTime() - i.created.getTime()) / MS_PER_HOUR); + const resPct = percentiles(resolutionHours); + + const ttfrVals = prs.map(p => p.ttfrHours).filter((v): v is number => v !== null); + const ttmVals = prs.map(p => p.ttmHours).filter((v): v is number => v !== null); + const ttfrPct = percentiles(ttfrVals); + const ttmPct = percentiles(ttmVals); + + const fourWeeksAgo = Date.now() - 28 * MS_PER_DAY; + const recentClosed = issues.filter(i => i.closed && i.closed.getTime() > fourWeeksAgo).length; + + const lookup: Record StatValue> = { + total: () => ({ key: 'Total', value: items.length }), + open: () => ({ key: 'Open', value: items.filter(i => i.state === 'open').length, color: 'green' }), + closed: () => ({ key: 'Closed', value: items.filter(i => i.state === 'closed').length }), + weeklyRate: () => ({ key: 'Weekly Close Rate', value: +(recentClosed / 4).toFixed(1), sublabel: 'last 4 weeks' }), + unlabeled: () => ({ key: 'Unlabeled', value: issues.filter(i => i.labels.length === 0).length, color: 'yellow' }), + unassigned: () => ({ + key: 'Unassigned', + value: issues.filter(i => i.assignees.length === 0).length, + color: 'yellow', + }), + medianResolution: () => ({ key: 'Median Resolution', value: formatHours(resPct.median) }), + avgResolution: () => ({ key: 'Avg Resolution', value: formatHours(resPct.avg) }), + p90Resolution: () => ({ key: 'P90 Resolution', value: formatHours(resPct.p90), color: 'red' }), + completed: () => ({ + key: 'Completed', + value: issues.filter(i => i.stateReason === 'completed').length, + color: 'green', + }), + notPlanned: () => ({ + key: 'Not Planned', + value: issues.filter(i => i.stateReason === 'not_planned').length, + color: 'dim', + }), + duplicates: () => ({ key: 'Duplicates', value: issues.filter(i => i.stateReason === 'duplicate').length }), + merged: () => ({ key: 'Merged', value: prs.filter(p => p.merged).length, color: 'purple' }), + closedNoMerge: () => ({ + key: 'Closed (no merge)', + value: prs.filter(p => p.state === 'closed' && !p.merged).length, + color: 'red', + }), + drafts: () => ({ key: 'Drafts', value: prs.filter(p => p.draft).length }), + mergeRate: () => { + const closed = prs.filter(p => p.state === 'closed').length; + const rate = closed > 0 ? (prs.filter(p => p.merged).length / closed) * 100 : 0; + return { key: 'Merge Rate', value: `${rate.toFixed(0)}%`, color: 'accent' }; + }, + medianTTFR: () => ({ key: 'Median TTFR', value: formatHours(ttfrPct.median) }), + avgTTFR: () => ({ key: 'Avg TTFR', value: formatHours(ttfrPct.avg) }), + p90TTFR: () => ({ key: 'P90 TTFR', value: formatHours(ttfrPct.p90), color: 'red' }), + medianTTM: () => ({ key: 'Median TTM', value: formatHours(ttmPct.median) }), + avgTTM: () => ({ key: 'Avg TTM', value: formatHours(ttmPct.avg) }), + p90TTM: () => ({ key: 'P90 TTM', value: formatHours(ttmPct.p90), color: 'red' }), + }; + + return metrics.map(m => lookup[m]?.() ?? { key: m, value: 'N/A' }); +} + +// โ”€โ”€ Timeline โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + +function computeTimeline( + _bucket: 'week', + series: string[], + items: (Issue | PullRequest)[], + bucketDays = 7 +): WeekBucket[] { + const fmt = + bucketDays >= 7 + ? (d: Date) => { + const s = new Date(d); + s.setDate(s.getDate() - s.getDay()); + return s.toISOString().slice(0, 10); + } + : bucketDays >= 1 + ? (d: Date) => d.toISOString().slice(0, 10) + : (d: Date) => d.toISOString().slice(0, 13) + ':00'; + const weeks = new Map(); + const ensure = (w: string) => { + if (!weeks.has(w)) { + const b: WeekBucket = { week: w, ...Object.fromEntries(series.map(s => [s, 0])) }; + weeks.set(w, b); + } + return weeks.get(w)!; + }; + + items.forEach(item => { + const w = ensure(fmt(item.created)); + if (series.includes('opened')) (w.opened as number)++; + + const closedDate = isIssue(item) ? item.closed : 'merged' in item ? item.merged : null; + if (closedDate) { + const cw = ensure(fmt(closedDate)); + if (series.includes('closed')) (cw.closed as number)++; + if (series.includes('merged') && !isIssue(item) && item.merged) { + (cw.merged as number)++; + } + } + }); + + const sorted = [...weeks.entries()].sort(([a], [b]) => a.localeCompare(b)); + let cumulative = 0; + return sorted.map(([, b]) => { + cumulative += ((b.opened as number) ?? 0) - ((b.closed as number) ?? 0) - ((b.merged as number) ?? 0); + if (series.includes('cumulativeOpen')) b.cumulativeOpen = Math.max(0, cumulative); + return b; + }); +} + +// โ”€โ”€ Distribution โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + +function computeDistribution(field: string, items: (Issue | PullRequest)[]): ChartData { + const counts = new Map(); + const inc = (k: string) => counts.set(k, (counts.get(k) ?? 0) + 1); + + if (field === 'labels') { + let unlabeled = 0; + items.forEach(item => { + if (item.labels.length === 0) { + unlabeled++; + return; + } + item.labels.forEach(l => inc(l)); + }); + if (unlabeled > 0) counts.set('(unlabeled)', unlabeled); + } else if (field === 'age') { + const bucketNames = ['<1d', '1-3d', '3-7d', '1-2w', '2-4w', '1-2m', '>2m']; + const thresholds = [1, 3, 7, 14, 28, 60]; + bucketNames.forEach(b => counts.set(b, 0)); + items + .filter(i => i.state === 'open') + .forEach(item => { + const d = ageDays(item.created); + const idx = thresholds.findIndex(t => d < t); + inc(bucketNames[idx === -1 ? bucketNames.length - 1 : idx]); + }); + } else if (field === 'sizeLabel') { + items.forEach(item => { + const sizeLabel = item.labels.find(l => l.startsWith('size/')); + inc(sizeLabel ?? '(no size label)'); + }); + } else if (field === 'author') { + items.forEach(item => inc(item.author)); + const sorted = [...counts.entries()].sort((a, b) => b[1] - a[1]).slice(0, 15); + return { labels: sorted.map(([l]) => l), values: sorted.map(([, v]) => v) }; + } else if (field === 'reviewer') { + items.forEach(item => { + if (!isIssue(item)) item.reviewers.forEach(r => inc(r)); + }); + const sorted = [...counts.entries()].sort((a, b) => b[1] - a[1]).slice(0, 15); + return { labels: sorted.map(([l]) => l), values: sorted.map(([, v]) => v) }; + } else if (field === 'resolver') { + items.forEach(item => { + if (isIssue(item) && item.closedBy) inc(item.closedBy); + }); + const sorted = [...counts.entries()].sort((a, b) => b[1] - a[1]).slice(0, 15); + return { labels: sorted.map(([l]) => l), values: sorted.map(([, v]) => v) }; + } else if (field === 'bucket') { + items.forEach(item => { + if (item.state === 'open' && !isIssue(item)) inc(item.bucket); + }); + } else if (field === 'linkedIssuePriority') { + items.forEach(item => { + if (!isIssue(item)) inc(item.linkedIssuePriority ?? '(none)'); + }); + } + + const entries = [...counts.entries()]; + return { labels: entries.map(([l]) => l), values: entries.map(([, v]) => v) }; +} + +// โ”€โ”€ Histogram โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + +function extractNumericField(field: string, item: Issue | PullRequest): number | null { + if (field === 'resolutionHours' && isIssue(item) && item.closed) { + return (item.closed.getTime() - item.created.getTime()) / MS_PER_HOUR; + } + if (field === 'ttfrHours' && !isIssue(item)) return item.ttfrHours; + if (field === 'ttmHours' && !isIssue(item)) return item.ttmHours; + return null; +} + +export function bucketLabel(low: number, high: number | undefined): string { + if (high === undefined) return `>${formatHours(low)}`; + return `${formatHours(low)}-${formatHours(high)}`; +} + +export function buildHistogram(values: number[], buckets: number[]): HistogramBucket[] { + return buckets.map((low, i) => { + const high = buckets[i + 1]; + const label = i === 0 ? `<${formatHours(high ?? low)}` : bucketLabel(low, high); + const count = values.filter(v => (high !== undefined ? v >= low && v < high : v >= low)).length; + return { label, count }; + }); +} + +function autoBuckets(values: number[]): number[] { + if (values.length === 0) return [0]; + const sorted = [...values].sort((a, b) => a - b); + const max = sorted[sorted.length - 1]; + const step = max / 8; + return Array.from({ length: 9 }, (_, i) => +(i * step).toFixed(1)); +} + +function getSizeLabel(item: Issue | PullRequest): string { + return item.labels.find(l => l.startsWith('size/')) ?? '(no size label)'; +} + +function computeHistogram( + field: string, + buckets: number[] | 'auto', + items: (Issue | PullRequest)[], + groupBy?: string +): { histogram?: HistogramBucket[]; histogramGrouped?: Record } { + if (groupBy) { + const groups = new Map(); + items.forEach(item => { + const v = extractNumericField(field, item); + if (v === null) return; + const key = + groupBy === 'sizeLabel' ? getSizeLabel(item) : groupBy === 'labels' ? (item.labels[0] ?? '(unlabeled)') : 'all'; + if (!groups.has(key)) groups.set(key, []); + groups.get(key)!.push(v); + }); + const histogramGrouped = Object.fromEntries( + [...groups.entries()].map(([key, vals]) => [ + key, + buildHistogram(vals, buckets === 'auto' ? autoBuckets(vals) : buckets), + ]) + ); + return { histogramGrouped }; + } + + const values = items.map(i => extractNumericField(field, i)).filter((v): v is number => v !== null); + const b = buckets === 'auto' ? autoBuckets(values) : buckets; + return { histogram: buildHistogram(values, b) }; +} + +// โ”€โ”€ Table โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + +function computeTable(config: TableSection, items: (Issue | PullRequest)[]): TableRow[] { + let filtered = [...items]; + const { filter } = config; + + if (filter.state) filtered = filtered.filter(i => i.state === filter.state); + if (filter.minAgeDays !== undefined) filtered = filtered.filter(i => ageDays(i.created) >= filter.minAgeDays!); + if (filter.maxComments !== undefined) + filtered = filtered.filter(i => isIssue(i) && i.comments <= filter.maxComments!); + if (filter.labeled === true) filtered = filtered.filter(i => i.labels.length > 0); + if (filter.labeled === false) filtered = filtered.filter(i => i.labels.length === 0); + + if (config.id === 'stale') { + filtered.sort((a, b) => a.created.getTime() - b.created.getTime()); + } else if (config.id === 'engagement') { + filtered.sort((a, b) => { + const ac = isIssue(a) ? a.comments : 0; + const bc = isIssue(b) ? b.comments : 0; + return bc - ac; + }); + } + + return filtered.slice(0, config.limit ?? 20).map(item => { + const colValue = (col: string): string | number | boolean | string[] | undefined => { + if (col === 'number') return item.number; + if (col === 'title') return item.title; + if (col === 'state') return item.state; + if (col === 'labels') return item.labels; + if (col === 'author') return item.author; + if (col === 'age') return `${Math.floor(ageDays(item.created))}d`; + if (col === 'comments' && isIssue(item)) return item.comments; + if (col === 'reactions' && isIssue(item)) return item.reactions; + if (col === 'draft' && !isIssue(item)) return item.draft; + if (col === 'priority' && !isIssue(item)) return item.linkedIssuePriority ?? ''; + if (col === 'bucket' && !isIssue(item)) return item.bucket; + if (col === 'lastActivity' && !isIssue(item)) { + const pr = item; + const latest = [pr.lastCommitDate, pr.lastReviewDate] + .filter((d): d is Date => d !== null) + .sort((a, b) => b.getTime() - a.getTime())[0]; + return latest ? formatRelativeTime(latest) : ''; + } + return undefined; + }; + return Object.fromEntries( + config.columns.map(col => [col, colValue(col)]).filter(([, v]) => v !== undefined) + ) as TableRow; + }); +} + +// โ”€โ”€ Term Frequency โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + +const STOP_WORDS = new Set([ + 'the', + 'and', + 'for', + 'are', + 'but', + 'not', + 'you', + 'all', + 'can', + 'had', + 'her', + 'was', + 'one', + 'our', + 'out', + 'has', + 'have', + 'been', + 'from', + 'this', + 'that', + 'with', + 'they', + 'will', + 'each', + 'make', + 'like', + 'into', + 'them', + 'then', + 'than', + 'its', + 'also', + 'after', + 'should', + 'would', + 'could', + 'when', + 'what', + 'which', + 'their', + 'about', + 'other', + 'there', + 'does', + 'just', + 'more', +]); + +function computeTermFrequency( + filter: { labeled: boolean }, + minCount: number, + items: Issue[] +): { terms: TermCount[]; unusedLabels: string[] } { + const filtered = filter.labeled ? items.filter(i => i.labels.length > 0) : items.filter(i => i.labels.length === 0); + + const wordCounts = new Map(); + filtered.forEach(item => { + item.title + .toLowerCase() + .replace(/[^a-z0-9\s]/g, '') + .split(/\s+/) + .filter(w => w.length >= 3 && !STOP_WORDS.has(w)) + .forEach(w => wordCounts.set(w, (wordCounts.get(w) ?? 0) + 1)); + }); + + const terms = [...wordCounts.entries()] + .filter(([, c]) => c >= minCount) + .sort((a, b) => b[1] - a[1]) + .slice(0, 20) + .map(([term, count]) => ({ term, count })); + + const usedLabels = new Set(filtered.flatMap(i => i.labels)); + const allLabels = new Set(items.flatMap(i => i.labels)); + const unusedLabels = [...allLabels].filter(l => !usedLabels.has(l)); + + return { terms, unusedLabels }; +} + +// โ”€โ”€ Page Computation โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + +export function computePage( + pageConfig: PageConfig, + issues: Issue[], + prs: PullRequest[], + ciRuns?: WorkflowRun[] +): PageData { + const items: (Issue | PullRequest)[] = pageConfig.dataSource === 'issues' ? issues : prs; + + function computeSections(sectionItems: (Issue | PullRequest)[], bucketDays = 7): SectionData[] { + return pageConfig.sections.map((sec): SectionData => { + switch (sec.type) { + case 'stats': + return { config: sec, stats: computeStats(sec.metrics, sectionItems) }; + case 'timeline': + return { config: sec, timeline: computeTimeline(sec.bucket, sec.series, sectionItems, bucketDays) }; + case 'distribution': + return { config: sec, chart: computeDistribution(sec.field, sectionItems) }; + case 'histogram': { + const h = computeHistogram(sec.field, sec.buckets, sectionItems, sec.groupBy); + return { config: sec, ...h }; + } + case 'table': + return { config: sec, table: computeTable(sec, sectionItems) }; + case 'termFrequency': { + const tf = computeTermFrequency(sec.filter, sec.minCount, sectionItems.filter(isIssue)); + return { config: sec, terms: tf.terms, unusedLabels: tf.unusedLabels }; + } + case 'ci': + return { config: sec, ci: computeCI(ciRuns ?? []) }; + case 'trend': + return { config: sec, trend: computeTrend(sec.fields, sec.aggregate, sectionItems) }; + case 'weeklyTable': + return { config: sec, weeklyTable: computeWeeklyTable(sec.metrics, sec.weeks, sectionItems) }; + } + }); + } + + const sections = computeSections(items); + + let windowedSections: Record | undefined; + if (pageConfig.windows) { + const now = new Date(); + windowedSections = Object.fromEntries( + pageConfig.windows.map(w => { + const cutoff = new Date(now.getTime() - w.days * MS_PER_DAY); + const filtered = items.filter(i => i.created >= cutoff); + // Adapt timeline granularity: <=1d โ†’ hourly, <=7d โ†’ daily, else weekly + const bucketDays = w.days <= 1 ? 1 / 24 : w.days <= 7 ? 1 : 7; + return [w.label, computeSections(filtered, bucketDays)]; + }) + ); + } + + return { + id: pageConfig.id, + title: pageConfig.title, + generatedAt: new Date().toISOString(), + sections, + windowedSections, + }; +} + +function groupBy(items: T[], keyFn: (item: T) => string): Record { + const result: Record = {}; + for (const item of items) (result[keyFn(item)] ??= []).push(item); + return result; +} + +function calcPassRates(runs: WorkflowRun[]): { overall: number; perWf: Record } { + const overall = + runs.length > 0 ? Math.round((runs.filter(r => r.conclusion === 'success').length / runs.length) * 100) : 0; + const perWf = Object.fromEntries( + Object.entries(groupBy(runs, r => r.workflowName)).map(([name, wfRuns]) => [ + name, + wfRuns.length > 0 ? Math.round((wfRuns.filter(r => r.conclusion === 'success').length / wfRuns.length) * 100) : 0, + ]) + ); + return { overall, perWf }; +} + +function buildCITimeline(runs: WorkflowRun[]): CIData['timeline'] { + const sorted = [...runs].sort((a, b) => a.created.getTime() - b.created.getTime()); + const start = + sorted.length > 0 ? new Date(sorted[0].created.getTime() - sorted[0].created.getDay() * MS_PER_DAY) : new Date(); + start.setHours(0, 0, 0, 0); + const end = sorted.length > 0 ? sorted[sorted.length - 1].created : new Date(); + const timeline: CIData['timeline'] = []; + const cur = new Date(start); + while (cur <= end) { + const nxt = new Date(cur.getTime() + 7 * MS_PER_DAY); + const weekRuns = runs.filter(r => r.created >= cur && r.created < nxt); + timeline.push({ + week: cur.toLocaleDateString('en-US', { month: 'short', day: 'numeric' }), + pass: weekRuns.filter(r => r.conclusion === 'success').length, + fail: weekRuns.filter(r => r.conclusion === 'failure').length, + }); + cur.setTime(nxt.getTime()); + } + return timeline; +} + +function findFailingJobs(runs: WorkflowRun[]): CIData['failingJobs'] { + const jobStats: Record = {}; + runs.forEach(r => { + r.jobs + .filter(j => j.conclusion !== 'skipped') + .forEach(j => { + const s = (jobStats[j.name] ??= { failures: 0, total: 0 }); + s.total++; + if (j.conclusion === 'failure') s.failures++; + }); + }); + return Object.entries(jobStats) + .filter(([, s]) => s.failures > 0) + .map(([job, s]) => ({ job, failures: s.failures, total: s.total, rate: Math.round((s.failures / s.total) * 100) })) + .sort((a, b) => b.failures - a.failures); +} + +function detectFlakyJobs(runs: WorkflowRun[]): CIData['flaky'] { + const flaky: CIData['flaky'] = []; + Object.values(groupBy(runs, r => r.workflowName)).forEach(wfRuns => { + const chronological = [...wfRuns].sort((a, b) => a.created.getTime() - b.created.getTime()); + const jobHistory: Record = {}; + chronological.forEach(r => { + r.jobs.filter(j => j.conclusion !== 'skipped').forEach(j => (jobHistory[j.name] ??= []).push(j.conclusion)); + }); + Object.entries(jobHistory).forEach(([job, history]) => { + // Stateful flip counting โ€” each iteration depends on the previous element, + // so a reduce would obscure the logic without simplifying it. + let flips = 0; + for (let i = 1; i < history.length; i++) { + if (history[i] !== history[i - 1]) flips++; + } + if (flips >= 3) { + const existing = flaky.find(f => f.job === job); + if (existing) existing.flipCount += flips; + else flaky.push({ job, flipCount: flips }); + } + }); + }); + return flaky.sort((a, b) => b.flipCount - a.flipCount); +} + +function getRecentFailures(runs: WorkflowRun[]): CIData['recentFailures'] { + return runs + .filter(r => r.conclusion === 'failure') + .sort((a, b) => b.created.getTime() - a.created.getTime()) + .slice(0, 20) + .map(r => ({ + id: r.id, + workflow: r.workflowName, + date: r.created.toISOString().slice(0, 16).replace('T', ' '), + failedJobs: r.jobs.filter(j => j.conclusion === 'failure').map(j => j.name), + })); +} + +function calcAvgDuration(runs: WorkflowRun[]): Record { + const jobDurations: Record = {}; + runs.forEach(r => { + r.jobs.filter(j => j.durationMin > 0).forEach(j => (jobDurations[j.name] ??= []).push(j.durationMin)); + }); + return Object.fromEntries( + Object.entries(jobDurations).map(([job, durations]) => [ + job, + Math.round((durations.reduce((a, b) => a + b, 0) / durations.length) * 10) / 10, + ]) + ); +} + +function calcCIWindows(runs: WorkflowRun[]): CIData['windows'] { + return Object.fromEntries( + [ + ['Past 24h', 1], + ['Past 7 days', 7], + ['Past 30 days', 30], + ].map(([label, days]) => { + const cutoff = new Date(Date.now() - (days as number) * MS_PER_DAY); + const subset = runs.filter(r => r.created >= cutoff); + const rates = calcPassRates(subset); + return [label, { overallPassRate: rates.overall, passRate: rates.perWf }]; + }) + ); +} + +function computeCI(runs: WorkflowRun[]): CIData { + const { overall: overallPassRate, perWf: passRate } = calcPassRates(runs); + return { + overallPassRate, + passRate, + timeline: buildCITimeline(runs), + failingJobs: findFailingJobs(runs), + flaky: detectFlakyJobs(runs), + recentFailures: getRecentFailures(runs), + avgDuration: calcAvgDuration(runs), + windows: calcCIWindows(runs), + }; +} + +// โ”€โ”€ Trend (weekly aggregates of numeric fields over time) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + +function computeTrend( + fields: string[], + aggregate: 'median' | 'avg', + items: (Issue | PullRequest)[] +): { weeks: string[]; series: Record } { + const sorted = [...items].sort((a, b) => a.created.getTime() - b.created.getTime()); + if (sorted.length === 0) return { weeks: [], series: {} }; + + const start = new Date(sorted[0].created); + start.setHours(0, 0, 0, 0); + start.setDate(start.getDate() - start.getDay()); + const end = sorted[sorted.length - 1].created; + + const weeks: string[] = []; + const series: Record = Object.fromEntries(fields.map(f => [f, []])); + const cur = new Date(start); + + while (cur <= end) { + const nxt = new Date(cur.getTime() + 7 * MS_PER_DAY); + weeks.push(cur.toLocaleDateString('en-US', { month: 'short', day: 'numeric' })); + fields.forEach(field => { + let vals: number[]; + if (field === 'openAgeDays') { + // For each week, compute avg age (in days) of items that were open at that point + const weekEnd = nxt; + vals = items + .filter(i => { + const closedDate = isIssue(i) ? i.closed : i.merged; + return i.created < weekEnd && (!closedDate || closedDate >= cur); + }) + .map(i => (Math.min(weekEnd.getTime(), Date.now()) - i.created.getTime()) / MS_PER_DAY); + } else { + vals = items + .filter(i => i.created >= cur && i.created < nxt) + .map(i => extractNumericField(field, i)) + .filter((v): v is number => v !== null); + } + const agg = vals.length > 0 ? (aggregate === 'median' ? percentiles(vals).median : percentiles(vals).avg) : 0; + series[field].push(Math.round(agg * 10) / 10); + }); + cur.setTime(nxt.getTime()); + } + return { weeks, series }; +} + +// โ”€โ”€ Weekly Table (recent weeks summary) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + +function computeWeeklyTable( + metrics: string[], + numWeeks: number, + items: (Issue | PullRequest)[] +): { weeks: string[]; rows: Record } { + const now = new Date(); + const weeks: string[] = []; + const rows: Record = Object.fromEntries(metrics.map(m => [m, []])); + + for (let w = numWeeks - 1; w >= 0; w--) { + const end = new Date(now.getTime() - w * 7 * MS_PER_DAY); + const start = new Date(end.getTime() - 7 * MS_PER_DAY); + weeks.push(start.toLocaleDateString('en-US', { month: 'short', day: 'numeric' })); + + const created = items.filter(i => i.created >= start && i.created < end); + const closed = items.filter(i => { + const d = isIssue(i) ? i.closed : i.merged; + return d && d >= start && d < end; + }); + + metrics.forEach(m => { + switch (m) { + case 'opened': + rows[m].push(created.length); + break; + case 'closed': + rows[m].push(closed.length); + break; + case 'merged': + rows[m].push(closed.filter(i => !isIssue(i) && i.merged).length); + break; + case 'net': + rows[m].push(created.length - closed.length); + break; + case 'medianResolution': { + const hrs = closed + .map(i => { + const c = isIssue(i) ? i.closed : i.merged; + return c ? (c.getTime() - i.created.getTime()) / MS_PER_HOUR : null; + }) + .filter((v): v is number => v !== null); + rows[m].push(hrs.length > 0 ? formatHours(percentiles(hrs).median) : 'โ€”'); + break; + } + case 'medianTTFR': { + const vals = created + .filter((i): i is PullRequest => !isIssue(i)) + .map(p => p.ttfrHours) + .filter((v): v is number => v !== null); + rows[m].push(vals.length > 0 ? formatHours(percentiles(vals).median) : 'โ€”'); + break; + } + case 'medianTTM': { + const vals = closed + .filter((i): i is PullRequest => !isIssue(i)) + .map(p => p.ttmHours) + .filter((v): v is number => v !== null); + rows[m].push(vals.length > 0 ? formatHours(percentiles(vals).median) : 'โ€”'); + break; + } + default: + rows[m].push('โ€”'); + } + }); + } + return { weeks, rows }; +} diff --git a/.github/dashboard/src/types.ts b/.github/dashboard/src/types.ts new file mode 100644 index 00000000..404009f8 --- /dev/null +++ b/.github/dashboard/src/types.ts @@ -0,0 +1,302 @@ +// โ”€โ”€ Config types โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + +export interface DashboardConfig { + repo: string; + outputDir: string; + pages: PageConfig[]; +} + +export interface PageConfig { + id: string; + title: string; + dataSource: 'issues' | 'prs' | 'ci'; + sections: SectionConfig[]; + windows?: StatsWindow[]; +} + +export type SectionConfig = + | StatsSection + | TimelineSection + | DistributionSection + | HistogramSection + | TableSection + | TermFrequencySection + | CIStatsSection + | TrendSection + | WeeklyTableSection; + +export type MetricKey = + | 'total' + | 'open' + | 'closed' + | 'weeklyRate' + | 'unlabeled' + | 'unassigned' + | 'medianResolution' + | 'avgResolution' + | 'p90Resolution' + | 'completed' + | 'notPlanned' + | 'duplicates' + | 'merged' + | 'closedNoMerge' + | 'drafts' + | 'mergeRate' + | 'medianTTFR' + | 'avgTTFR' + | 'p90TTFR' + | 'medianTTM' + | 'avgTTM' + | 'p90TTM'; + +export interface StatsSection { + type: 'stats'; + metrics: MetricKey[]; +} + +export interface StatsWindow { + label: string; + days: number; +} + +export interface TimelineSection { + type: 'timeline'; + bucket: 'week'; + series: string[]; +} + +export interface DistributionSection { + type: 'distribution'; + field: string; + chart: 'doughnut' | 'bar'; + orientation?: 'horizontal' | 'vertical'; +} + +export interface HistogramSection { + type: 'histogram'; + field: string; + buckets: number[] | 'auto'; + title?: string; + groupBy?: string; +} + +export interface TableSection { + type: 'table'; + id: string; + title: string; + filter: TableFilter; + columns: string[]; + limit?: number; +} + +export interface TermFrequencySection { + type: 'termFrequency'; + filter: { labeled: boolean }; + minCount: number; + title?: string; +} + +export interface TableFilter { + state?: 'open' | 'closed'; + minAgeDays?: number; + maxComments?: number; + labeled?: boolean; +} + +// โ”€โ”€ Raw GitHub API types โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + +export type AuthorAssociation = + | 'COLLABORATOR' + | 'CONTRIBUTOR' + | 'FIRST_TIMER' + | 'FIRST_TIME_CONTRIBUTOR' + | 'MANNEQUIN' + | 'MEMBER' + | 'NONE' + | 'OWNER'; + +export type RunConclusion = + | 'success' + | 'failure' + | 'cancelled' + | 'skipped' + | 'timed_out' + | 'action_required' + | 'neutral' + | 'stale' + | 'in_progress'; + +export interface GHIssue { + number: number; + title: string; + state: 'open' | 'closed'; + created_at: string; + closed_at: string | null; + labels: { name: string }[]; + assignees: { login: string }[]; + comments: number; + reactions: { total_count: number }; + state_reason: string | null; + closed_by: { login: string } | null; + user: { login: string }; + author_association: AuthorAssociation; + pull_request?: unknown; +} + +export interface GHPullRequestNode { + number: number; + title: string; + state: 'OPEN' | 'MERGED' | 'CLOSED'; + createdAt: string; + mergedAt: string | null; + closedAt: string | null; + isDraft: boolean; + author: { login: string } | null; + labels: { nodes: { name: string }[] }; + reviews: { nodes: GHReviewNode[] }; + commits: { nodes: { commit: { committedDate: string } }[] }; + closingIssuesReferences: { nodes: { number: number; labels: { nodes: { name: string }[] } }[] }; +} + +export interface GHReviewNode { + author: { login: string } | null; + state: 'APPROVED' | 'CHANGES_REQUESTED' | 'COMMENTED' | 'DISMISSED' | 'PENDING'; + submittedAt: string | null; +} + +// โ”€โ”€ Processed data types โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + +export interface Issue { + number: number; + title: string; + state: 'open' | 'closed'; + created: Date; + closed: Date | null; + labels: string[]; + assignees: string[]; + comments: number; + reactions: number; + stateReason: string | null; + closedBy: string | null; + author: string; + authorType: AuthorAssociation; +} + +export interface PullRequest { + number: number; + title: string; + state: 'open' | 'closed'; + created: Date; + merged: Date | null; + draft: boolean; + author: string; + labels: string[]; + ttfrHours: number | null; + ttmHours: number | null; + reviewers: string[]; + lastCommitDate: Date | null; + lastReviewDate: Date | null; + linkedIssuePriority: string | null; + bucket: 'needs-re-review' | 'waiting-on-author' | 'needs-initial-review' | 'approved' | 'closed'; +} + +export interface WeekBucket { + week: string; + [series: string]: string | number; +} + +export interface StatValue { + key: string; + value: string | number; + color?: 'green' | 'red' | 'yellow' | 'accent' | 'purple' | 'dim'; + sublabel?: string; +} + +export interface ChartData { + labels: string[]; + values: number[]; + colors?: string[]; +} + +export interface HistogramBucket { + label: string; + count: number; +} + +export type TableRow = Record; + +export interface TermCount { + term: string; + count: number; +} + +export interface SectionData { + config: SectionConfig; + stats?: StatValue[]; + timeline?: WeekBucket[]; + chart?: ChartData; + histogram?: HistogramBucket[]; + histogramGrouped?: Record; + table?: TableRow[]; + terms?: TermCount[]; + unusedLabels?: string[]; + ci?: CIData; + trend?: { weeks: string[]; series: Record }; + weeklyTable?: { weeks: string[]; rows: Record }; +} + +export interface PageData { + id: string; + title: string; + generatedAt: string; + sections: SectionData[]; + windowedSections?: Record; +} + +// โ”€โ”€ CI types โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + +export interface CIStatsSection { + type: 'ci'; + workflows: string[]; + branch: string; + maxRuns: number; +} + +export interface TrendSection { + type: 'trend'; + title: string; + fields: string[]; + aggregate: 'median' | 'avg'; +} + +export interface WeeklyTableSection { + type: 'weeklyTable'; + title: string; + metrics: string[]; + weeks: number; +} + +export interface CIData { + overallPassRate: number; + passRate: Record; + timeline: { week: string; pass: number; fail: number }[]; + failingJobs: { job: string; failures: number; total: number; rate: number }[]; + flaky: { job: string; flipCount: number }[]; + recentFailures: { id: number; workflow: string; date: string; failedJobs: string[] }[]; + avgDuration: Record; + windows: Record }>; +} + +export interface WorkflowRun { + id: number; + workflowName: string; + conclusion: RunConclusion; + created: Date; + jobs: WorkflowJob[]; +} + +export interface WorkflowJob { + name: string; + conclusion: RunConclusion; + durationMin: number; +} diff --git a/.github/dashboard/tsconfig.json b/.github/dashboard/tsconfig.json new file mode 100644 index 00000000..2272f203 --- /dev/null +++ b/.github/dashboard/tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "Node16", + "moduleResolution": "Node16", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "outDir": "dist", + "rootDir": "src", + "jsx": "react-jsx", + "jsxImportSource": "@kitajs/html" + }, + "include": ["src"] +} diff --git a/.github/workflows/dashboard.yml b/.github/workflows/dashboard.yml new file mode 100644 index 00000000..a0683a04 --- /dev/null +++ b/.github/workflows/dashboard.yml @@ -0,0 +1,52 @@ +name: Dashboard +on: + schedule: + - cron: '0 6 * * *' # Daily at 6am UTC + workflow_dispatch: # Manual trigger from Actions tab + +permissions: + contents: read + pages: write + id-token: write + issues: read + pull-requests: read + +concurrency: + group: dashboard + cancel-in-progress: true + +jobs: + build: + runs-on: ubuntu-latest + timeout-minutes: 15 + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: '20' + - name: Install dependencies + working-directory: .github/dashboard + run: npm ci + - name: Type check + working-directory: .github/dashboard + run: npx tsc --noEmit + - name: Test + working-directory: .github/dashboard + run: npx vitest run + - name: Generate dashboard + working-directory: .github/dashboard + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: npx tsx src/generate.tsx + - uses: actions/upload-pages-artifact@v3 + with: + path: .github/dashboard/site + deploy: + needs: build + runs-on: ubuntu-latest + environment: + name: github-pages + url: ${{ steps.deploy.outputs.page_url }} + steps: + - id: deploy + uses: actions/deploy-pages@v4