diff --git a/.github/self-heal-schedule.yml b/.github/self-heal-schedule.yml new file mode 100644 index 00000000..da296771 --- /dev/null +++ b/.github/self-heal-schedule.yml @@ -0,0 +1,4 @@ +# AUTO-UPDATED +schedule: "0 0 * * *" +rationale: "Bootstrap default schedule (daily at midnight UTC)" +last_updated: "2024-05-21T00:00:00Z" diff --git a/.github/workflows/compute-schedule.yml b/.github/workflows/compute-schedule.yml new file mode 100644 index 00000000..7103a1ca --- /dev/null +++ b/.github/workflows/compute-schedule.yml @@ -0,0 +1,88 @@ +name: Compute Self-Heal Schedule + +on: + schedule: + - cron: '0 0 * * 0' # Weekly run on Sunday + workflow_dispatch: + +permissions: + contents: write + pull-requests: write + actions: read + +concurrency: + group: selfheal-schedule-${{ github.ref }} + cancel-in-progress: true + +jobs: + compute-schedule: + if: !startsWith(github.ref_name, 'selfheal-schedule-') + runs-on: ubuntu-latest + timeout-minutes: 10 + + steps: + - name: Checkout codebase + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '22' + cache: 'npm' + + - name: Install Dependencies + run: npm ci + + - name: Compute New Schedule + id: compute + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + node scripts/compute_schedule.mjs > compute_schedule.log 2>&1 || true + cat compute_schedule.log + + if [ -z "$(git status --porcelain)" ]; then + echo "No changes in schedule." + echo "has_diff=false" >> $GITHUB_OUTPUT + exit 0 + fi + echo "has_diff=true" >> $GITHUB_OUTPUT + + - name: Guard against duplicate PRs + id: check_pr + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + if [ "${{ steps.compute.outputs.has_diff }}" != "true" ]; then + exit 0 + fi + + open_prs=$(gh pr list --label self-heal-schedule --state open --json number -q '.[].number') + if [ -n "$open_prs" ]; then + echo "An open self-heal-schedule PR already exists. Skipping PR creation." + echo "create_pr=false" >> $GITHUB_OUTPUT + else + echo "create_pr=true" >> $GITHUB_OUTPUT + fi + + - name: Create Pull Request + if: steps.compute.outputs.has_diff == 'true' && steps.check_pr.outputs.create_pr == 'true' + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + BRANCH_NAME="selfheal-schedule-$(date +%s)" + git checkout -b "$BRANCH_NAME" + + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + + git commit -a -m "Update self-heal schedule cadence based on telemetry" + git push origin "$BRANCH_NAME" + + gh pr create \ + --title "[Self-Heal Schedule] Update cadence" \ + --body "Automated PR to update the self-heal schedule based on telemetry." \ + --label "self-heal-schedule" \ + --label "automation" diff --git a/.github/workflows/self-heal.yml b/.github/workflows/self-heal.yml new file mode 100644 index 00000000..7f7f5d80 --- /dev/null +++ b/.github/workflows/self-heal.yml @@ -0,0 +1,131 @@ +name: Self-Heal Auto-Repair + +on: + schedule: + # AUTO-UPDATED - Do not manually modify this line; use compute_schedule logic or self-heal-schedule.yml + - cron: '0 0 * * *' + workflow_run: + workflows: ["ci"] + types: + - completed + workflow_dispatch: + +permissions: + contents: write + pull-requests: write + actions: read + +concurrency: + group: selfheal-${{ github.ref }} + cancel-in-progress: true + +jobs: + self-heal: + if: > + !startsWith(github.ref_name, 'selfheal-') && + ( + github.event_name == 'schedule' || + github.event_name == 'workflow_dispatch' || + (github.event_name == 'workflow_run' && github.event.workflow_run.conclusion == 'failure') + ) + runs-on: ubuntu-latest + timeout-minutes: 15 + + steps: + - name: Checkout codebase + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '22' + cache: 'npm' + + - name: Install Dependencies + run: npm ci + + - name: Pre-healthcheck (Optional recording state) + id: pre + run: | + node scripts/healthcheck.mjs > pre_healthcheck.log 2>&1 || true + cat pre_healthcheck.log + + - name: Run Self-Heal Pipeline + id: repair + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + node scripts/self_heal.mjs > repair.log 2>&1 || echo "No fix found or fully healthy" + cat repair.log + + - name: Post-healthcheck + id: post + run: | + node scripts/healthcheck.mjs > post_healthcheck.log 2>&1 + cat post_healthcheck.log + + - name: Verify meaningful diff and no secrets + id: check_diff + run: | + if [ -z "$(git status --porcelain)" ]; then + echo "No changes made by self-heal." + echo "has_diff=false" >> $GITHUB_OUTPUT + exit 0 + fi + echo "has_diff=true" >> $GITHUB_OUTPUT + + git add -A + git reset HEAD .github/workflows/ci.yml .env secrets/ src/**/migrations/ 2>/dev/null || true + + if git diff --cached -G"(?i)(password|secret|api[_-]?key|token)" --name-only | grep -q .; then + echo "Potential secrets found in diff!" + exit 1 + fi + + - name: Guard against duplicate PRs + id: check_pr + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + if [ "${{ steps.check_diff.outputs.has_diff }}" != "true" ]; then + exit 0 + fi + + open_prs=$(gh pr list --label self-heal --state open --json number -q '.[].number') + if [ -n "$open_prs" ]; then + echo "An open self-heal PR already exists. Skipping PR creation." + echo "create_pr=false" >> $GITHUB_OUTPUT + else + echo "create_pr=true" >> $GITHUB_OUTPUT + fi + + - name: Create Pull Request + if: steps.check_diff.outputs.has_diff == 'true' && steps.check_pr.outputs.create_pr == 'true' + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + BRANCH_NAME="selfheal-$(date +%s)" + git checkout -b "$BRANCH_NAME" + + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + + git commit -m "Self-heal auto-repair" + git push origin "$BRANCH_NAME" + + TITLE="[Self-Heal] Reactive/Scheduled Repair" + if [ "${{ github.event_name }}" == "schedule" ]; then + TITLE="[Self-Heal Scheduled] Drift fixes" + elif [ "${{ github.event_name }}" == "workflow_dispatch" ]; then + TITLE="[Self-Heal Manual] Repair" + elif [ "${{ github.event_name }}" == "workflow_run" ]; then + TITLE="[Self-Heal Reactive] CI fix" + fi + + gh pr create \ + --title "$TITLE" \ + --body "Automated self-heal repair triggered by ${{ github.event_name }}. See workflow logs for details. (Generated by Claude Code / Jules coding agent)" \ + --label "self-heal" \ + --label "automation" diff --git a/SELF_HEAL_SETUP.md b/SELF_HEAL_SETUP.md new file mode 100644 index 00000000..5ba9d13e --- /dev/null +++ b/SELF_HEAL_SETUP.md @@ -0,0 +1,39 @@ +# Self-Heal Auto-Repair Automation + +This project uses an automated self-healing CI pipeline configured via GitHub Actions. + +## Triggers + +1. **Scheduled:** Proactively runs to detect and repair drift. The schedule is dynamically computed based on the repository's activity level. +2. **CI Failure (Reactive):** Triggers when the main `ci` workflow fails. It attempts to repair the build and opens a PR if it succeeds. +3. **Manual Dispatch:** Can be run on demand from the Actions tab. + +## The Repair Pipeline (Idempotent) + +The `scripts/self_heal.mjs` script performs the following idempotent steps to resolve issues: +1. **Clean Install:** Runs `npm ci` to rebuild tools and lock dependencies to the lockfile. +2. **Lint/Format:** Uses `eslint --fix` and `prettier -w` to automatically format the code. +3. **Test Snapshots:** Runs `vitest -u` to update any failing test snapshots. +4. **Clean Rebuild:** Wipes out previous TypeScript typestubs (e.g., `tsbuildinfo`) and does a clean build to clear out cache issues. +5. **Dependency Re-resolve:** Refreshes lockfiles cleanly if needed. +6. **Static Build:** Re-runs the final build scripts to regenerate static assets. + +After each step, it runs a healthcheck (`scripts/healthcheck.mjs`). If the healthcheck passes and there's a git diff, it stops immediately, exits successfully, and the GitHub Action creates a PR with the fixes. + +## Schedule Dynamics + +The schedule is controlled by `.github/self-heal-schedule.yml` and computed by `.github/workflows/compute-schedule.yml` (using `scripts/compute_schedule.mjs`). It reads telemetry via the GitHub API (`gh pr list`) and scales the run frequency based on PR velocity (e.g., High velocity = runs every 6 hours; Low velocity = runs weekly). + +## How to Override the Schedule + +To manually override the schedule: +1. Edit `.github/self-heal-schedule.yml` directly. +2. Ensure the first line is exactly `# AUTO-UPDATED`. +3. The next compute cycle will see your changes. If you wish to freeze it completely, disable the `compute-schedule.yml` workflow. + +## Reviewer Checklist + +When reviewing a PR from this automation, check: +- [ ] Has it unintentionally altered test assertions or logic? (Only snapshots and formatting are expected). +- [ ] Has it modified restricted files like `.github/workflows/ci.yml` or leaked secrets? (It shouldn't, there are gates preventing it). +- [ ] Is it a legitimate repair of code drift or an unexpected artifact change? diff --git a/package-lock.json b/package-lock.json index 5643e919..9411c4d9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -35,8 +35,11 @@ "@types/which": "^3.0.4", "@vitest/coverage-v8": "3.1.1", "esbuild": "^0.25.2", + "eslint": "^10.4.0", + "js-yaml": "^4.1.1", "multer": "1.4.5-lts.1", "openai": "^4.91.1", + "prettier": "^3.8.3", "tsx": "^4.19.3", "typescript": "^5.8.2", "vitest": "^3.1.1" @@ -560,6 +563,218 @@ "node": ">=18" } }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.23.5", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.23.5.tgz", + "integrity": "sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^3.0.5", + "debug": "^4.3.1", + "minimatch": "^10.2.4" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/config-array/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/@eslint/config-array/node_modules/brace-expansion": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", + "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/@eslint/config-array/node_modules/minimatch": { + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.5" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.6.0.tgz", + "integrity": "sha512-ii6Bw9jJ2zi2cWA2Z+9/QZ/+3DX6kwaV5Q986D/CdP3Lap3w/pgQZ373FV7byY/i7L4IRH/G43I5dz1ClsCbpA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^1.2.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/core": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-1.2.1.tgz", + "integrity": "sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/object-schema": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-3.0.5.tgz", + "integrity": "sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.7.1.tgz", + "integrity": "sha512-rZAP3aVgB9ds9KOeUSL+zZ21hPmo8dh6fnIFwRQj5EAZl9gzR7wxYbYXYysAM8CTqGmUGyp2S4kUdV17MnGuWQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^1.2.1", + "levn": "^0.4.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", + "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/types": "^0.15.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz", + "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.2", + "@humanfs/types": "^0.15.0", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/types": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz", + "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, "node_modules/@isaacs/cliui": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", @@ -1180,6 +1395,13 @@ "@types/node": "*" } }, + "node_modules/@types/esrecurse": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@types/esrecurse/-/esrecurse-4.3.1.tgz", + "integrity": "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/estree": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz", @@ -1483,6 +1705,29 @@ "node": ">= 0.6" } }, + "node_modules/acorn": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, "node_modules/agentkeepalive": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.6.0.tgz", @@ -1496,9 +1741,10 @@ } }, "node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -1579,8 +1825,7 @@ "node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "peer": true + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" }, "node_modules/array-flatten": { "version": "1.1.1", @@ -2005,6 +2250,13 @@ "node": ">=6" } }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, "node_modules/delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", @@ -2173,6 +2425,207 @@ "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.4.0.tgz", + "integrity": "sha512-loXy6bWOoP3EP6JA7jo6p5jMpBJmHmsNZM5SFRHLdh1MGOPurMnNBj4ZlAbaqUAaQWbCr7jHV4P7gzAyryZWkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.2", + "@eslint/config-array": "^0.23.5", + "@eslint/config-helpers": "^0.6.0", + "@eslint/core": "^1.2.1", + "@eslint/plugin-kit": "^0.7.1", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.14.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^9.1.2", + "eslint-visitor-keys": "^5.0.1", + "espree": "^11.2.0", + "esquery": "^1.7.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "minimatch": "^10.2.4", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-scope": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-9.1.2.tgz", + "integrity": "sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@types/esrecurse": "^4.3.1", + "@types/estree": "^1.0.8", + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-scope/node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/eslint/node_modules/brace-expansion": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", + "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/eslint/node_modules/minimatch": { + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.5" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/espree": { + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-11.2.0.tgz", + "integrity": "sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.16.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^5.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, "node_modules/estree-walker": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", @@ -2182,6 +2635,16 @@ "@types/estree": "^1.0.0" } }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/etag": { "version": "1.8.1", "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", @@ -2310,6 +2773,13 @@ "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", "license": "MIT" }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, "node_modules/fast-uri": { "version": "3.0.6", "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.6.tgz", @@ -2369,6 +2839,19 @@ "node": ">= 8" } }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, "node_modules/finalhandler": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", @@ -2399,6 +2882,44 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", + "dev": true, + "license": "ISC" + }, "node_modules/follow-redirects": { "version": "1.15.9", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", @@ -2591,6 +3112,19 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, "node_modules/gopd": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", @@ -2688,6 +3222,26 @@ "node": ">=0.10.0" } }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, "node_modules/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", @@ -2701,6 +3255,16 @@ "node": ">= 0.10" } }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/is-fullwidth-code-point": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", @@ -2709,6 +3273,19 @@ "node": ">=8" } }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/is-promise": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", @@ -2794,10 +3371,10 @@ } }, "node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", - "peer": true, + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "license": "MIT", "dependencies": { "argparse": "^2.0.1" }, @@ -2805,11 +3382,65 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, "node_modules/json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", @@ -3019,6 +3650,13 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, "node_modules/negotiator": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", @@ -3219,6 +3857,56 @@ "resolved": "https://registry.npmjs.org/openapi-types/-/openapi-types-12.1.3.tgz", "integrity": "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw==" }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/package-json-from-dist": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", @@ -3233,6 +3921,16 @@ "node": ">= 0.8" } }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/path-key": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", @@ -3332,6 +4030,32 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prettier": { + "version": "3.8.3", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.3.tgz", + "integrity": "sha512-7igPTM53cGHMW8xWuVTydi2KO233VFiTNyF5hLJqpilHfmn8C8gPf+PS7dUT64YcXFbiMGZxS9pCSxL/Dxm/Jw==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, "node_modules/process-nextick-args": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", @@ -3986,6 +4710,19 @@ "fsevents": "~2.3.3" } }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/type-is": { "version": "1.6.18", "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", @@ -4282,6 +5019,16 @@ "node": ">=8" } }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/wrap-ansi": { "version": "8.1.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", @@ -4457,6 +5204,19 @@ "node": ">=8" } }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/zod": { "version": "3.24.1", "resolved": "https://registry.npmjs.org/zod/-/zod-3.24.1.tgz", diff --git a/package.json b/package.json index 8ea45a98..bbd0ba90 100644 --- a/package.json +++ b/package.json @@ -40,8 +40,11 @@ "@types/which": "^3.0.4", "@vitest/coverage-v8": "3.1.1", "esbuild": "^0.25.2", + "eslint": "^10.4.0", + "js-yaml": "^4.1.1", "multer": "1.4.5-lts.1", "openai": "^4.91.1", + "prettier": "^3.8.3", "tsx": "^4.19.3", "typescript": "^5.8.2", "vitest": "^3.1.1" diff --git a/scripts/compute_schedule.mjs b/scripts/compute_schedule.mjs new file mode 100755 index 00000000..4bd8456d --- /dev/null +++ b/scripts/compute_schedule.mjs @@ -0,0 +1,150 @@ +#!/usr/bin/env node +import { execSync } from 'node:child_process'; +import { readFileSync, writeFileSync } from 'node:fs'; +import { resolve, dirname } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import yaml from 'js-yaml'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); +const REPO_ROOT = resolve(__dirname, '..'); + +const SCHEDULE_FILE_PATH = resolve(REPO_ROOT, '.github/self-heal-schedule.yml'); +const WORKFLOW_FILE_PATH = resolve(REPO_ROOT, '.github/workflows/self-heal.yml'); + +function fetchPrTelemetry() { + try { + const dateLimit = new Date(); + dateLimit.setDate(dateLimit.getDate() - 30); + const dateLimitStr = dateLimit.toISOString().split('T')[0]; + + const mergedPrsJson = execSync(`gh pr list --state merged --json mergedAt --search "merged:>=${dateLimitStr}"`, { + stdio: ['ignore', 'pipe', 'ignore'], + cwd: REPO_ROOT + }).toString(); + const mergedPrs = JSON.parse(mergedPrsJson); + + const selfHealSuccessJson = execSync(`gh pr list --label self-heal --state merged --json mergedAt --search "merged:>=${dateLimitStr}"`, { + stdio: ['ignore', 'pipe', 'ignore'], + cwd: REPO_ROOT + }).toString(); + const selfHealSuccess = JSON.parse(selfHealSuccessJson); + + const ciFailuresJson = execSync(`gh run list --workflow=ci --json conclusion --limit 100`, { + stdio: ['ignore', 'pipe', 'ignore'], + cwd: REPO_ROOT + }).toString(); + const runs = JSON.parse(ciFailuresJson); + const failureRate = runs.length > 0 ? runs.filter(r => r.conclusion === 'failure').length / runs.length : 0; + + return { + mergedCount: mergedPrs.length, + selfHealSuccessRate: selfHealSuccess.length, + failureRate + }; + } catch (err) { + console.warn("Could not fetch full telemetry via gh CLI. Using fallback values."); + return null; + } +} + +function getActiveWindow() { + try { + const log = execSync(`git log --format=%aI --since="30 days ago"`, { + stdio: ['ignore', 'pipe', 'ignore'], + cwd: REPO_ROOT + }).toString(); + + const hours = new Array(24).fill(0); + const commits = log.split('\n').filter(Boolean); + commits.forEach(commit => { + const date = new Date(commit); + hours[date.getUTCHours()]++; + }); + + let minHour = 0; + let minCommits = hours[0]; + for (let i = 1; i < 24; i++) { + if (hours[i] < minCommits) { + minCommits = hours[i]; + minHour = i; + } + } + + let scheduledHour = (minHour - 1 + 24) % 24; + return scheduledHour; + } catch(e) { + return 0; // fallback midnight + } +} + +function computeSchedule(telemetry, activeHourOffset) { + let scheduleStr = `${activeHourOffset} 0 * * *`; + let rationale = `Default schedule (daily at ${activeHourOffset} UTC)`; + + if (!telemetry) { + return { scheduleStr: `0 ${activeHourOffset} * * *`, rationale: `Fallback schedule due to missing telemetry` }; + } + + const { mergedCount, failureRate, selfHealSuccessRate } = telemetry; + + if (mergedCount > 20 || failureRate > 0.2) { + scheduleStr = `0 */6 * * *`; + rationale = `High velocity/failure rate. Running every 6 hours.`; + } else if (mergedCount > 10) { + scheduleStr = `0 */12 * * *`; + rationale = `Active velocity. Running every 12 hours.`; + } else if (mergedCount > 2) { + scheduleStr = `0 ${activeHourOffset} * * *`; + rationale = `Standard velocity. Running daily before quiet period at ${activeHourOffset}:00 UTC.`; + } else { + scheduleStr = `0 ${activeHourOffset} * * 0`; + rationale = `Low velocity. Running weekly before quiet period at ${activeHourOffset}:00 UTC.`; + } + + return { scheduleStr, rationale }; +} + +function updateScheduleFiles(scheduleStr, rationale) { + const scheduleConfig = yaml.load(readFileSync(SCHEDULE_FILE_PATH, 'utf8')) || {}; + + if (scheduleConfig.schedule === scheduleStr) { + console.log("Schedule is unchanged. No updates needed."); + return; + } + + scheduleConfig.schedule = scheduleStr; + scheduleConfig.rationale = rationale; + scheduleConfig.last_updated = new Date().toISOString(); + + const dumpedSchedule = yaml.dump(scheduleConfig, { forceQuotes: true }); + writeFileSync(SCHEDULE_FILE_PATH, `# AUTO-UPDATED\n${dumpedSchedule}`); + + let workflowYaml = readFileSync(WORKFLOW_FILE_PATH, 'utf8'); + + const newCronLine = ` # AUTO-UPDATED - Do not manually modify this line; use compute_schedule logic or self-heal-schedule.yml\n - cron: '${scheduleStr}'`; + + const regex = /# AUTO-UPDATED[^\n]*\n\s+- cron:\s+['"][^'"]+['"]/g; + if (regex.test(workflowYaml)) { + workflowYaml = workflowYaml.replace(regex, newCronLine); + writeFileSync(WORKFLOW_FILE_PATH, workflowYaml); + console.log(`Successfully updated workflow cron to '${scheduleStr}'`); + } else { + console.warn("Could not find the `# AUTO-UPDATED` anchor in the workflow yaml."); + } +} + +function main() { + console.log("Computing Self-Heal Schedule..."); + + const telemetry = fetchPrTelemetry(); + const activeHourOffset = getActiveWindow(); + const { scheduleStr, rationale } = computeSchedule(telemetry, activeHourOffset); + + console.log(`Computed Schedule: ${scheduleStr}`); + console.log(`Rationale: ${rationale}`); + + updateScheduleFiles(scheduleStr, rationale); +} + +main(); diff --git a/scripts/healthcheck.mjs b/scripts/healthcheck.mjs new file mode 100755 index 00000000..b82aa121 --- /dev/null +++ b/scripts/healthcheck.mjs @@ -0,0 +1,52 @@ +#!/usr/bin/env node +import { execSync } from 'node:child_process'; + +/** + * Executes a command synchronously and logs the output. + * @param {string} cmd Command to run + * @param {string} name Friendly name of the step + * @returns {boolean} True if successful, false otherwise + */ +function runCheck(cmd, name) { + console.log(`\n--- Running ${name} ---`); + try { + execSync(cmd, { stdio: 'inherit' }); + console.log(`āœ… ${name} passed.`); + return true; + } catch (error) { + console.error(`āŒ ${name} failed.`); + return false; + } +} + +function main() { + console.log("Starting healthcheck..."); + + let allPassed = true; + + // Check 1: Types (TypeScript) + // Run tsc without emitting files to check type safety + allPassed = runCheck('npx tsc --noEmit', 'TypeScript Compilation') && allPassed; + + // Check 2: Linting + // Run eslint if available in package.json devDependencies + allPassed = runCheck('npx eslint . --ext .ts,.js,.mjs', 'ESLint') && allPassed; + + // Check 3: Tests (Vitest) + // Ensure we don't fail if no tests exist using passWithNoTests + allPassed = runCheck('npx vitest run --passWithNoTests', 'Vitest Suite') && allPassed; + + // Check 4: Build + // Ensures standard build step passes + allPassed = runCheck('npm run build', 'Build Script') && allPassed; + + if (allPassed) { + console.log("\nšŸŽ‰ All healthchecks passed."); + process.exit(0); + } else { + console.error("\nšŸ’„ Healthcheck failed. Automation should repair."); + process.exit(1); + } +} + +main(); diff --git a/scripts/self_heal.mjs b/scripts/self_heal.mjs new file mode 100755 index 00000000..a5e179b5 --- /dev/null +++ b/scripts/self_heal.mjs @@ -0,0 +1,108 @@ +#!/usr/bin/env node +import { execSync } from 'node:child_process'; +import { resolve, dirname } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); +const REPO_ROOT = resolve(__dirname, '..'); + +function runCommand(cmd, name) { + console.log(`\n--- [Self-Heal] Step: ${name} ---`); + try { + execSync(cmd, { stdio: 'inherit', cwd: REPO_ROOT }); + return true; + } catch (error) { + console.error(`āŒ Step ${name} encountered an error: ${error.message}`); + return false; + } +} + +function checkHealth() { + try { + execSync('node scripts/healthcheck.mjs', { stdio: 'ignore', cwd: REPO_ROOT }); + return true; + } catch { + return false; + } +} + +function hasDiff() { + try { + const diff = execSync('git status --porcelain', { cwd: REPO_ROOT }).toString().trim(); + return diff.length > 0; + } catch { + return false; + } +} + +function evaluateStateAndExitIfFixed() { + const isHealthy = checkHealth(); + const modified = hasDiff(); + + if (isHealthy && modified) { + console.log("āœ… Repair successful and diff generated. Exiting with success."); + process.exit(0); + } +} + +function cleanupStalePrs() { + console.log("Cleaning up stale selfheal PRs..."); + try { + const dateLimit = new Date(); + dateLimit.setDate(dateLimit.getDate() - 7); + const dateStr = dateLimit.toISOString().split('T')[0]; + + const prsJson = execSync(`gh pr list --label self-heal --state open --json number,createdAt --search "created:<=${dateStr}"`, { + stdio: ['ignore', 'pipe', 'ignore'], + cwd: REPO_ROOT + }).toString(); + + const prs = JSON.parse(prsJson); + for (const pr of prs) { + console.log(`Closing stale PR #${pr.number}...`); + execSync(`gh pr close ${pr.number} --comment "Closing stale self-heal PR"`, { cwd: REPO_ROOT }); + } + } catch (err) { + console.warn("Could not clean up stale PRs. Moving on."); + } +} + +function main() { + console.log("Starting Self-Heal Pipeline..."); + cleanupStalePrs(); + + runCommand('npm ci', 'Clean Install Dependencies'); + evaluateStateAndExitIfFixed(); + + runCommand('npx eslint . --fix || true', 'ESLint Fix'); + runCommand('npx prettier -w .', 'Prettier Format'); + evaluateStateAndExitIfFixed(); + + runCommand('npx vitest run -u --passWithNoTests', 'Update Test Snapshots'); + evaluateStateAndExitIfFixed(); + + runCommand('rm -rf tsconfig.tsbuildinfo && npx tsc --build', 'Clean Rebuild TypeScript'); + evaluateStateAndExitIfFixed(); + + runCommand('npm install --package-lock-only', 'Refresh Lockfile'); + evaluateStateAndExitIfFixed(); + + runCommand('npm run build', 'Static Build / Asset Gen'); + evaluateStateAndExitIfFixed(); + + const isHealthy = checkHealth(); + const modified = hasDiff(); + + if (isHealthy && !modified) { + console.log("Pipeline completed. System is healthy and no drift detected. Exiting cleanly with non-zero code to block PR."); + process.exit(1); + } else if (!isHealthy) { + console.error("āŒ Pipeline exhausted. System is still failing healthchecks."); + process.exit(1); + } else { + process.exit(0); + } +} + +main();