From 95643141e6dd7d6e954099b9f9786e69ebafc3b2 Mon Sep 17 00:00:00 2001 From: Ken Brewer Date: Wed, 28 May 2025 14:22:35 -0700 Subject: [PATCH 1/7] =?UTF-8?q?=E2=9C=A8=20feat(pipeline-proposals):=20add?= =?UTF-8?q?=20github=20action=20bot=20for=20tracking=20status?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/pipeline_proposals.yml | 267 +++++++++++++++++++++++ 1 file changed, 267 insertions(+) create mode 100644 .github/workflows/pipeline_proposals.yml diff --git a/.github/workflows/pipeline_proposals.yml b/.github/workflows/pipeline_proposals.yml new file mode 100644 index 0000000..67894af --- /dev/null +++ b/.github/workflows/pipeline_proposals.yml @@ -0,0 +1,267 @@ +name: Pipeline proposal approval automation + +on: + issues: + types: [opened, closed, labeled, unlabeled] + issue_comment: + types: [created, edited] + +jobs: + pipeline_approval: + # Only run for pipeline proposal issues + if: startsWith(github.event.issue.title, 'New Pipeline') + runs-on: ubuntu-latest + + steps: + - name: Handle pipeline proposal approval logic + uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea #v7.0.1 + with: + github-token: ${{ secrets.nf_core_bot_auth_token }} + script: | + const issueNumber = context.issue.number; + const org = context.repo.owner; + const repo = context.repo.repo; + + // Ignore comments on closed issues + if (context.eventName === 'issue_comment' && context.payload.issue.state === 'closed') { + console.log('Comment event on closed issue, ignoring.'); + return; + } + + // --------------------------------------------- + // Fetch members of the core and maintainer teams + // --------------------------------------------- + async function getTeamMembers(teamSlug) { + try { + const res = await github.request('GET /orgs/{org}/teams/{team_slug}/members', { + org, + team_slug: teamSlug, + per_page: 100 + }); + console.log(`Fetched ${res.data.length} ${teamSlug} team members.`); + return res.data.map(m => m.login); + } catch (err) { + console.error(`Failed to fetch ${teamSlug} team members:`, err); + throw err; + } + } + + const coreTeamMembers = await getTeamMembers('core'); + const maintainerTeamMembers = await getTeamMembers('maintainers'); + console.log('Core team members:', coreTeamMembers); + console.log('Maintainer team members:', maintainerTeamMembers); + + // Helper for list formatting and complete status body + function formatUserList(users) { + return users.length ? users.map(u => `[@${u}](https://github.com/${u})`).join(', ') : '-'; + } + + function generateStatusBody(status, coreApprovalsSet, maintainerApprovalsSet, rejectionsSet, awaitingCore, awaitingMaintainers) { + const coreApprovers = [...coreApprovalsSet]; + const maintainerApprovers = [...maintainerApprovalsSet]; + const rejecters = [...rejectionsSet]; + let body = `## Pipeline proposal approval status: ${status}\n\n`; + body += `Required approvals: Either 2 core team members OR 1 core team member + 1 maintainer\n\n`; + + if (coreApprovers.length > 0 || maintainerApprovers.length > 0 || rejecters.length > 0 || awaitingCore.length > 0 || awaitingMaintainers.length > 0) { + body += `|Review Status|Team members|\n|--|--|\n`; + if (coreApprovers.length > 0) { + body += `| ✅ Approved (Core) | ${formatUserList(coreApprovers)} |\n`; + } + if (maintainerApprovers.length > 0) { + body += `| ✅ Approved (Maintainer) | ${formatUserList(maintainerApprovers)} |\n`; + } + if (rejecters.length > 0) { + body += `| ❌ Rejected | ${formatUserList(rejecters)} |\n`; + } + if (awaitingCore.length > 0) { + body += `| 🕐 Pending (Core) | ${formatUserList(awaitingCore)} |\n`; + } + if (awaitingMaintainers.length > 0) { + body += `| 🕐 Pending (Maintainer) | ${formatUserList(awaitingMaintainers)} |\n`; + } + } + return body; + } + + // Helper function to update issue status and labels + async function updateIssueStatus(status, closeReason = null) { + const labels = []; + let state = 'open'; + + switch (status) { + case '✅ Approved': + labels.push('accepted'); + state = 'closed'; + break; + case '❌ Rejected': + labels.push('turned-down'); + state = 'closed'; + break; + case '⏰ Timed Out': + labels.push('timed-out'); + state = 'closed'; + break; + default: + labels.push('proposed'); + } + + // Update labels + await github.rest.issues.update({ + owner: org, + repo, + issue_number: issueNumber, + labels: labels + }); + + // Close the issue if needed + if (state === 'closed') { + await github.rest.issues.update({ + owner: org, + repo, + issue_number: issueNumber, + state: 'closed', + state_reason: closeReason || (status === '✅ Approved' ? 'completed' : 'not_planned') + }); + } + } + + // Handle label changes + if (context.eventName === 'issues' && (context.payload.action === 'labeled' || context.payload.action === 'unlabeled')) { + const label = context.payload.label.name; + if (label === 'timed-out') { + console.log('Timed-out label detected, updating status'); + const statusBody = generateStatusBody('⏰ Timed Out', new Set(), new Set(), new Set(), [], []); + + // Find and update the status comment + const comments = await github.paginate(github.rest.issues.listComments, { + owner: org, + repo, + issue_number: issueNumber, + per_page: 100 + }); + + let statusComment = comments.find(c => c.body.startsWith('## Pipeline proposal approval status:')); + if (statusComment) { + await github.rest.issues.updateComment({ + owner: org, + repo, + comment_id: statusComment.id, + body: statusBody + }); + } else { + await github.rest.issues.createComment({ + owner: org, + repo, + issue_number: issueNumber, + body: statusBody + }); + } + return; + } + } + + // ------------------------------------------------- + // If this workflow was triggered by issue creation + // ------------------------------------------------- + if (context.eventName === 'issues' && context.payload.action === 'opened') { + const body = generateStatusBody('🕐 Pending', new Set(), new Set(), new Set(), coreTeamMembers, maintainerTeamMembers); + console.log('Creating initial comment for review status'); + + await github.rest.issues.createComment({ + owner: org, + repo, + issue_number: issueNumber, + body + }); + + // Set initial status to proposed + await updateIssueStatus('🕐 Pending'); + return; + } + + // --------------------------------------------------------------------- + // Collect comments and compute votes (shared for comment & closed events) + // --------------------------------------------------------------------- + + // Collect all comments on the issue + const comments = await github.paginate(github.rest.issues.listComments, { + owner: org, + repo, + issue_number: issueNumber, + per_page: 100 + }); + + const coreApprovals = new Set(); + const maintainerApprovals = new Set(); + const rejections = new Set(); + + for (const comment of comments) { + const commenter = comment.user.login; + const isCoreMember = coreTeamMembers.includes(commenter); + const isMaintainer = maintainerTeamMembers.includes(commenter); + + if (!isCoreMember && !isMaintainer) continue; // Only team members count + + // Count approvals / rejections based on line starting with /approve or /reject + const lines = comment.body.split(/\r?\n/); + for (const rawLine of lines) { + const line = rawLine.trim(); + if (/^\/approve\b/i.test(line)) { + if (isCoreMember) { + coreApprovals.add(commenter); + } else if (isMaintainer) { + maintainerApprovals.add(commenter); + } + } else if (/^\/reject\b/i.test(line)) { + rejections.add(commenter); + } + } + } + + console.log(`Core approvals (${coreApprovals.size}):`, [...coreApprovals]); + console.log(`Maintainer approvals (${maintainerApprovals.size}):`, [...maintainerApprovals]); + console.log(`Rejections (${rejections.size}):`, [...rejections]); + + const awaitingCore = coreTeamMembers.filter(u => !coreApprovals.has(u) && !rejections.has(u)); + const awaitingMaintainers = maintainerTeamMembers.filter(u => !maintainerApprovals.has(u) && !rejections.has(u)); + + // Determine status + let status = '🕐 Pending'; + + if (context.eventName === 'issues' && context.payload.action === 'closed' && context.payload.issue.state_reason === 'not_planned' && rejections.size > 0) { + status = '❌ Rejected'; + } else if ((coreApprovals.size >= 2) || (coreApprovals.size >= 1 && maintainerApprovals.size >= 1)) { + status = '✅ Approved'; + } + + const statusBody = generateStatusBody(status, coreApprovals, maintainerApprovals, rejections, awaitingCore, awaitingMaintainers); + console.log('New status body to post:\n', statusBody); + + // Try to locate the existing status comment (starts with our header) + let statusComment = comments.find(c => c.body.startsWith('## Pipeline proposal approval status:')); + + if (statusComment) { + if (statusComment.body.trim() === statusBody.trim()) { + console.log('Status comment already up to date - no update required.'); + } else { + console.log('Updating existing status comment.'); + await github.rest.issues.updateComment({ + owner: org, + repo, + comment_id: statusComment.id, + body: statusBody + }); + } + } else { + // Fallback: create a new status comment if missing (shouldn't normally happen) + await github.rest.issues.createComment({ + owner: org, + repo, + issue_number: issueNumber, + body: statusBody + }); + } + + // Update issue status and labels + await updateIssueStatus(status); From 53f5ed74e87c492c4d22075f8405e6cb1621651b Mon Sep 17 00:00:00 2001 From: Ken Brewer Date: Wed, 28 May 2025 14:38:15 -0700 Subject: [PATCH 2/7] =?UTF-8?q?=F0=9F=93=9D=20docs:=20add=20details=20of?= =?UTF-8?q?=20github=20actions=20to=20pipeline=20approval=20workflow?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/pipeline_proposals.yml | 17 +--------------- README.md | 25 +++++++++++++++++++----- 2 files changed, 21 insertions(+), 21 deletions(-) diff --git a/.github/workflows/pipeline_proposals.yml b/.github/workflows/pipeline_proposals.yml index 67894af..4030524 100644 --- a/.github/workflows/pipeline_proposals.yml +++ b/.github/workflows/pipeline_proposals.yml @@ -85,22 +85,18 @@ jobs: } // Helper function to update issue status and labels - async function updateIssueStatus(status, closeReason = null) { + async function updateIssueStatus(status) { const labels = []; - let state = 'open'; switch (status) { case '✅ Approved': labels.push('accepted'); - state = 'closed'; break; case '❌ Rejected': labels.push('turned-down'); - state = 'closed'; break; case '⏰ Timed Out': labels.push('timed-out'); - state = 'closed'; break; default: labels.push('proposed'); @@ -113,17 +109,6 @@ jobs: issue_number: issueNumber, labels: labels }); - - // Close the issue if needed - if (state === 'closed') { - await github.rest.issues.update({ - owner: org, - repo, - issue_number: issueNumber, - state: 'closed', - state_reason: closeReason || (status === '✅ Approved' ? 'completed' : 'not_planned') - }); - } } // Handle label changes diff --git a/README.md b/README.md index 291d321..2ba9e35 100644 --- a/README.md +++ b/README.md @@ -23,15 +23,30 @@ To make a new proposal for a special interest group, please create a new issue i The curator workflow is as follows: -- [ ] Once a issue is made, update the 'Project' status to 'proposed' (right hand side bar, under 'new--proposals') +- [ ] Once a issue is made, a Github Actions workflow will: + - Add the 'proposed' label + - Create a status comment tracking approvals - [ ] Facilitate discussion on the the Issue thread, following the guidance [here](https://nf-co.re/docs/checklists/community_governance/core_team#new-pipeline-proposals-and-onboarding). - [ ] Acceptance requires a minimum of OKs from: - Two members of the core team. - One member of the core team and one member of the maintainers team. -- [ ] If a proposal is accepted, update both the label AND status to 'accepted', and when closing select 'Close as completed' -- [ ] If a proposal is turned down, update both the label AND status to 'turned-down', and when closing select 'Close as not planned' -- [ ] If a proposal is not completed or abandoned after a year, update both the label AND status to 'timed-out', and when closing select 'Close as not planned' -- [ ] Complete the reemaining new-pipeline onboarding tasks listed [here](https://nf-co.re/docs/checklists/community_governance/core_team#new-pipeline-proposals-and-onboarding) +- [ ] Core and maintainer members can use the following commands in comments: + - `/approve` - to approve the proposal + - `/reject` - to reject the proposal +- [ ] The automation will: + - Track approvals and rejections + - Update the status comment + - Update labels based on the current status +- [ ] If a proposal is accepted: + - Update the status to 'accepted' + - Close the issue as 'completed' +- [ ] If a proposal is turned down: + - Update the status to 'turned-down' + - Close the issue as 'not planned' +- [ ] If a proposal is not completed or abandoned after a year: + - Add the 'timed-out' label + - Close the issue as 'not planned' +- [ ] Complete the remaining new-pipeline onboarding tasks listed [here](https://nf-co.re/docs/checklists/community_governance/core_team#new-pipeline-proposals-and-onboarding) ### Special Interest Groups From fcf4f0ded1d2241521f9a881255159d84acfcc6a Mon Sep 17 00:00:00 2001 From: Ken Brewer Date: Sun, 20 Jul 2025 11:36:01 +0100 Subject: [PATCH 3/7] =?UTF-8?q?=E2=9C=A8=20feat:=20add=20approval=20automa?= =?UTF-8?q?tion=20for=20pipelines=20and=20special=20interest=20groups?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/ISSUE_TEMPLATE/new_pipeline.yml | 4 +- .github/ISSUE_TEMPLATE/new_rfc.yml | 7 +- .../new_special_interest_group.yml | 4 +- .github/workflows/lib/.gitignore | 58 ++ .github/workflows/lib/README.md | 151 +++++ .github/workflows/lib/approval.js | 163 ++++++ .github/workflows/lib/approval.test.js | 546 ++++++++++++++++++ .github/workflows/lib/package.json | 30 + .../lib/workflow-integration.test.js | 356 ++++++++++++ .github/workflows/pipeline_proposals.yml | 205 +------ .github/workflows/rfc_approval.yml | 144 ++--- .../workflows/test-approval-automation.yml | 204 +++++++ README.md | 128 ++-- 13 files changed, 1669 insertions(+), 331 deletions(-) create mode 100644 .github/workflows/lib/.gitignore create mode 100644 .github/workflows/lib/README.md create mode 100644 .github/workflows/lib/approval.js create mode 100644 .github/workflows/lib/approval.test.js create mode 100644 .github/workflows/lib/package.json create mode 100644 .github/workflows/lib/workflow-integration.test.js create mode 100644 .github/workflows/test-approval-automation.yml diff --git a/.github/ISSUE_TEMPLATE/new_pipeline.yml b/.github/ISSUE_TEMPLATE/new_pipeline.yml index 0231313..ed58c1a 100644 --- a/.github/ISSUE_TEMPLATE/new_pipeline.yml +++ b/.github/ISSUE_TEMPLATE/new_pipeline.yml @@ -1,8 +1,8 @@ name: New pipeline proposal description: Propose a new pipeline for incorporation into nf-core title: "New pipeline: nf-core/" -labels: "new-pipeline,proposed" -projects: nf-core/104 +labels: ["new-pipeline", "proposed"] +projects: ["nf-core/104"] body: - type: input attributes: diff --git a/.github/ISSUE_TEMPLATE/new_rfc.yml b/.github/ISSUE_TEMPLATE/new_rfc.yml index c4bb57b..700d1b6 100644 --- a/.github/ISSUE_TEMPLATE/new_rfc.yml +++ b/.github/ISSUE_TEMPLATE/new_rfc.yml @@ -1,10 +1,8 @@ name: New RFC proposal description: Propose a major new functionality for incorporation into nf-core title: "New RFC: " -labels: "new-rfc,proposed" -projects: nf-core/127 -assignees: - - nf-core/core +labels: ["new-rfc", "proposed"] +projects: ["nf-core/127"] body: - type: checkboxes attributes: @@ -28,7 +26,6 @@ body: attributes: label: Background & Motivation description: Provide the context of the proposal, summarising the existing status-quo or issues. - placeholder: validations: required: true - type: textarea diff --git a/.github/ISSUE_TEMPLATE/new_special_interest_group.yml b/.github/ISSUE_TEMPLATE/new_special_interest_group.yml index d97d2cf..a55835f 100644 --- a/.github/ISSUE_TEMPLATE/new_special_interest_group.yml +++ b/.github/ISSUE_TEMPLATE/new_special_interest_group.yml @@ -1,8 +1,8 @@ name: New special interest group proposal description: Propose a new special interest group for incorporation into nf-core title: "New special interest group: " -labels: "new-special-interest-group,proposed" -projects: nf-core/105 +labels: ["new-special-interest-group", "proposed"] +projects: ["nf-core/105"] body: - type: checkboxes attributes: diff --git a/.github/workflows/lib/.gitignore b/.github/workflows/lib/.gitignore new file mode 100644 index 0000000..821c051 --- /dev/null +++ b/.github/workflows/lib/.gitignore @@ -0,0 +1,58 @@ +# Dependencies +node_modules/ +package-lock.json + +# Coverage reports +coverage/ + +# Jest cache +.cache/ + +# Logs +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Optional npm cache directory +.npm + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variables file +.env +.env.test + +# parcel-bundler cache (https://parceljs.org/) +.cache +.parcel-cache + +# next.js build output +.next + +# nuxt.js build output +.nuxt + +# vuepress build output +.vuepress/dist + +# Serverless directories +.serverless + +# FuseBox cache +.fusebox/ + +# DynamoDB Local files +.dynamodb/ diff --git a/.github/workflows/lib/README.md b/.github/workflows/lib/README.md new file mode 100644 index 0000000..2753dda --- /dev/null +++ b/.github/workflows/lib/README.md @@ -0,0 +1,151 @@ +# GitHub Approval Automation + +This module contains the `ApprovalManager` class used by GitHub Actions workflows to automate the approval process for pipeline proposals and RFCs. + +## Files + +- `approval.js` - The main ApprovalManager class +- `approval.test.js` - Unit tests for ApprovalManager class +- `workflow-integration.test.js` - Integration tests for complete workflow scenarios +- `package.json` - Node.js package configuration with Jest setup +- `README.md` - This documentation file + +## ApprovalManager Class + +The `ApprovalManager` class handles: + +- Fetching team members from GitHub organization teams +- Processing comments to track approvals and rejections via `/approve` and `/reject` commands +- Updating issue status and labels based on approval state +- Managing status comments on issues + +### Usage Scenarios + +#### Pipeline Proposals + +- **Approval Criteria**: Either 2 core team members OR 1 core team member + 1 maintainer +- **Issue Title Pattern**: Must start with "New Pipeline" +- **Team Roles**: Both core team and maintainers can vote + +#### RFC Proposals + +- **Approval Criteria**: Quorum of core team members (Math.ceil(coreTeamMembers.length / 2)) +- **Issue Title Pattern**: Must start with "New RFC" +- **Team Roles**: Only core team members can vote + +## Running Tests + +### Prerequisites + +Install Node.js and npm, then install Jest: + +```bash +npm install +``` + +### Test Commands + +```bash +# Run all tests +npm test + +# Run tests in watch mode (reruns on file changes) +npm test:watch + +# Run tests with coverage report +npm test:coverage +``` + +## Test Suite + +The test suite includes both unit tests and integration tests: + +### Unit Tests (`approval.test.js`) + +Tests individual methods and functionality of the ApprovalManager class. + +### Integration Tests (`workflow-integration.test.js`) + +End-to-end tests that simulate complete workflow scenarios as they would run in GitHub Actions. + +## Test Coverage + +The combined test suites cover: + +### Core Functionality + +- ✅ Constructor initialization +- ✅ Team member fetching from GitHub API +- ✅ Comment processing with `/approve` and `/reject` commands +- ✅ Status comment updates +- ✅ Issue label management + +### Pipeline Proposal Scenarios + +- ✅ Approval with 2 core members +- ✅ Approval with 1 core + 1 maintainer +- ✅ No approval with only 1 core member +- ✅ No approval with only maintainers + +### RFC Proposal Scenarios + +- ✅ Approval with core team quorum +- ✅ No approval without quorum +- ✅ Quorum calculation for different team sizes +- ✅ Ignoring maintainer votes (core-only) + +### Edge Cases + +- ✅ Case-insensitive commands (`/APPROVE`, `/reject`) +- ✅ Commands only at start of line +- ✅ Multiple commands from same user +- ✅ Multiple commands within same comment +- ✅ Non-team member comments (ignored) +- ✅ Empty or malformed comments +- ✅ Mixed line endings +- ✅ Users in both core and maintainer teams + +### Error Handling + +- ✅ GitHub API errors +- ✅ Empty comment arrays +- ✅ Null/undefined comment bodies +- ✅ Malformed regex patterns + +## Command Format + +The approval system recognizes these commands when they appear at the start of a line in a comment: + +- `/approve` - Approve the proposal +- `/reject` - Reject the proposal + +Commands are case-insensitive and can include additional text after the command. + +### Examples + +``` +/approve +``` + +``` +/approve This looks good to me! +``` + +``` +/reject +This needs more work before approval. +``` + +``` +I have some concerns. +/reject +``` + +## Status Labels + +The system manages these issue labels: + +- `proposed` - Initial state for new proposals +- `accepted` - Proposal has sufficient approvals +- `turned-down` - Proposal was rejected +- `timed-out` - Proposal expired without sufficient votes diff --git a/.github/workflows/lib/approval.js b/.github/workflows/lib/approval.js new file mode 100644 index 0000000..ce01ab2 --- /dev/null +++ b/.github/workflows/lib/approval.js @@ -0,0 +1,163 @@ +class ApprovalManager { + constructor(github, org, repo, issueNumber) { + this.github = github; + this.org = org; + this.repo = repo; + this.issueNumber = issueNumber; + this.coreTeamMembers = []; + this.maintainerTeamMembers = []; + this.comments = []; + this.coreApprovals = new Set(); + this.maintainerApprovals = new Set(); + this.coreRejections = new Set(); + this.maintainerRejections = new Set(); + this.awaitingCore = []; + this.awaitingMaintainers = []; + } + + // Helper for list formatting + formatUserList(users) { + return users.length ? users.map((u) => `[@${u}](https://github.com/${u})`).join(", ") : "-"; + } + + // Helper to fetch team members + async getTeamMembers(teamSlug) { + try { + const res = await this.github.request("GET /orgs/{org}/teams/{team_slug}/members", { + org: this.org, + team_slug: teamSlug, + per_page: 100, + }); + console.log(`Fetched ${res.data.length} ${teamSlug} team members.`); + return res.data.map((m) => m.login); + } catch (err) { + console.error(`Failed to fetch ${teamSlug} team members:`, err); + throw err; + } + } + + // Initialize the manager with team members and comments + async initialize() { + // Fetch team members + this.coreTeamMembers = await this.getTeamMembers("core"); + this.maintainerTeamMembers = await this.getTeamMembers("maintainers"); + console.log("Core team members:", this.coreTeamMembers); + console.log("Maintainer team members:", this.maintainerTeamMembers); + + // Fetch comments + this.comments = await this.github.paginate(this.github.rest.issues.listComments, { + owner: this.org, + repo: this.repo, + issue_number: this.issueNumber, + per_page: 100, + }); + + // Process comments + this.processComments(); + return this; + } + + // Helper to update issue status and labels + async updateIssueStatus(status) { + const labels = []; + + switch (status) { + case "✅ Approved": + labels.push("accepted"); + break; + case "❌ Rejected": + labels.push("turned-down"); + break; + case "⏰ Timed Out": + labels.push("timed-out"); + break; + default: + labels.push("proposed"); + } + + // Update labels + await this.github.rest.issues.update({ + owner: this.org, + repo: this.repo, + issue_number: this.issueNumber, + labels: labels, + }); + } + + // Helper to find and update status comment + async updateStatusComment(statusBody) { + let statusComment = this.comments.find((c) => c.body.startsWith("## Approval status:")); + if (statusComment) { + if (statusComment.body.trim() === statusBody.trim()) { + console.log("Status comment already up to date - no update required."); + } else { + console.log("Updating existing status comment."); + await this.github.rest.issues.updateComment({ + owner: this.org, + repo: this.repo, + comment_id: statusComment.id, + body: statusBody, + }); + } + } else { + // Fallback: create a new status comment if missing + await this.github.rest.issues.createComment({ + owner: this.org, + repo: this.repo, + issue_number: this.issueNumber, + body: statusBody, + }); + } + } + + // Helper to process comments and collect votes + processComments() { + // Reset all approval sets + this.coreApprovals = new Set(); + this.maintainerApprovals = new Set(); + this.coreRejections = new Set(); + this.maintainerRejections = new Set(); + + for (const comment of this.comments) { + const commenter = comment.user.login; + const isCoreMember = this.coreTeamMembers.includes(commenter); + const isMaintainer = this.maintainerTeamMembers.includes(commenter); + + if (!isCoreMember && !isMaintainer) continue; // Only team members count + + // Skip comments with no body + if (!comment.body) continue; + + // Count approvals / rejections based on line starting with /approve or /reject + const lines = comment.body.split(/\r\n|\r|\n/); + for (const rawLine of lines) { + const line = rawLine.trim(); + if (/^\/approve\b/i.test(line)) { + if (isCoreMember) { + this.coreApprovals.add(commenter); + this.coreRejections.delete(commenter); // Remove any previous rejection + } else if (isMaintainer) { + this.maintainerApprovals.add(commenter); + this.maintainerRejections.delete(commenter); // Remove any previous rejection + } + } else if (/^\/reject\b/i.test(line)) { + if (isCoreMember) { + this.coreRejections.add(commenter); + this.coreApprovals.delete(commenter); // Remove any previous approval + } else if (isMaintainer) { + this.maintainerRejections.add(commenter); + this.maintainerApprovals.delete(commenter); // Remove any previous approval + } + } + } + } + + // Update awaiting lists + this.awaitingCore = this.coreTeamMembers.filter((u) => !this.coreApprovals.has(u) && !this.coreRejections.has(u)); + this.awaitingMaintainers = this.maintainerTeamMembers.filter( + (u) => !this.maintainerApprovals.has(u) && !this.maintainerRejections.has(u), + ); + } +} + +module.exports = ApprovalManager; diff --git a/.github/workflows/lib/approval.test.js b/.github/workflows/lib/approval.test.js new file mode 100644 index 0000000..ff219c0 --- /dev/null +++ b/.github/workflows/lib/approval.test.js @@ -0,0 +1,546 @@ +const ApprovalManager = require("./approval.js"); + +// Mock GitHub API +const mockGithub = { + request: jest.fn(), + paginate: jest.fn(), + rest: { + issues: { + listComments: jest.fn(), + update: jest.fn(), + updateComment: jest.fn(), + createComment: jest.fn(), + }, + }, +}; + +describe("ApprovalManager", () => { + let approvalManager; + const mockOrg = "test-org"; + const mockRepo = "test-repo"; + const mockIssueNumber = 123; + + beforeEach(() => { + jest.clearAllMocks(); + approvalManager = new ApprovalManager(mockGithub, mockOrg, mockRepo, mockIssueNumber); + }); + + describe("constructor", () => { + it("should initialize with correct properties", () => { + expect(approvalManager.github).toBe(mockGithub); + expect(approvalManager.org).toBe(mockOrg); + expect(approvalManager.repo).toBe(mockRepo); + expect(approvalManager.issueNumber).toBe(mockIssueNumber); + expect(approvalManager.coreTeamMembers).toEqual([]); + expect(approvalManager.maintainerTeamMembers).toEqual([]); + expect(approvalManager.comments).toEqual([]); + expect(approvalManager.coreApprovals).toBeInstanceOf(Set); + expect(approvalManager.maintainerApprovals).toBeInstanceOf(Set); + expect(approvalManager.coreRejections).toBeInstanceOf(Set); + expect(approvalManager.maintainerRejections).toBeInstanceOf(Set); + expect(approvalManager.awaitingCore).toEqual([]); + expect(approvalManager.awaitingMaintainers).toEqual([]); + }); + }); + + describe("formatUserList", () => { + it("should format empty array correctly", () => { + expect(approvalManager.formatUserList([])).toBe("-"); + }); + + it("should format single user correctly", () => { + expect(approvalManager.formatUserList(["user1"])).toBe("[@user1](https://github.com/user1)"); + }); + + it("should format multiple users correctly", () => { + expect(approvalManager.formatUserList(["user1", "user2"])).toBe( + "[@user1](https://github.com/user1), [@user2](https://github.com/user2)", + ); + }); + }); + + describe("getTeamMembers", () => { + it("should fetch team members successfully", async () => { + const mockMembers = [{ login: "user1" }, { login: "user2" }, { login: "user3" }]; + + mockGithub.request.mockResolvedValue({ data: mockMembers }); + + const result = await approvalManager.getTeamMembers("core"); + + expect(mockGithub.request).toHaveBeenCalledWith("GET /orgs/{org}/teams/{team_slug}/members", { + org: mockOrg, + team_slug: "core", + per_page: 100, + }); + expect(result).toEqual(["user1", "user2", "user3"]); + }); + + it("should handle API errors", async () => { + const error = new Error("API Error"); + mockGithub.request.mockRejectedValue(error); + + await expect(approvalManager.getTeamMembers("core")).rejects.toThrow("API Error"); + }); + }); + + describe("initialize", () => { + it("should initialize with team members and comments", async () => { + const mockCoreMembers = [{ login: "core1" }, { login: "core2" }]; + const mockMaintainerMembers = [{ login: "maintainer1" }, { login: "maintainer2" }]; + const mockComments = [ + { id: 1, user: { login: "core1" }, body: "/approve" }, + { id: 2, user: { login: "maintainer1" }, body: "/reject" }, + ]; + + mockGithub.request + .mockResolvedValueOnce({ data: mockCoreMembers }) + .mockResolvedValueOnce({ data: mockMaintainerMembers }); + + mockGithub.paginate.mockResolvedValue(mockComments); + + const result = await approvalManager.initialize(); + + expect(result).toBe(approvalManager); + expect(approvalManager.coreTeamMembers).toEqual(["core1", "core2"]); + expect(approvalManager.maintainerTeamMembers).toEqual(["maintainer1", "maintainer2"]); + expect(approvalManager.comments).toEqual(mockComments); + }); + }); + + describe("updateIssueStatus", () => { + it("should add accepted label for approved status", async () => { + await approvalManager.updateIssueStatus("✅ Approved"); + + expect(mockGithub.rest.issues.update).toHaveBeenCalledWith({ + owner: mockOrg, + repo: mockRepo, + issue_number: mockIssueNumber, + labels: ["accepted"], + }); + }); + + it("should add turned-down label for rejected status", async () => { + await approvalManager.updateIssueStatus("❌ Rejected"); + + expect(mockGithub.rest.issues.update).toHaveBeenCalledWith({ + owner: mockOrg, + repo: mockRepo, + issue_number: mockIssueNumber, + labels: ["turned-down"], + }); + }); + + it("should add timed-out label for timed out status", async () => { + await approvalManager.updateIssueStatus("⏰ Timed Out"); + + expect(mockGithub.rest.issues.update).toHaveBeenCalledWith({ + owner: mockOrg, + repo: mockRepo, + issue_number: mockIssueNumber, + labels: ["timed-out"], + }); + }); + + it("should add proposed label for pending status", async () => { + await approvalManager.updateIssueStatus("🕐 Pending"); + + expect(mockGithub.rest.issues.update).toHaveBeenCalledWith({ + owner: mockOrg, + repo: mockRepo, + issue_number: mockIssueNumber, + labels: ["proposed"], + }); + }); + }); + + describe("updateStatusComment", () => { + it("should update existing status comment if content changed", async () => { + const existingComment = { id: 1, body: "## Approval status: Old status" }; + approvalManager.comments = [existingComment]; + const newStatusBody = "## Approval status: New status"; + + await approvalManager.updateStatusComment(newStatusBody); + + expect(mockGithub.rest.issues.updateComment).toHaveBeenCalledWith({ + owner: mockOrg, + repo: mockRepo, + comment_id: 1, + body: newStatusBody, + }); + }); + + it("should not update existing status comment if content is the same", async () => { + const statusBody = "## Approval status: Same status"; + const existingComment = { id: 1, body: statusBody }; + approvalManager.comments = [existingComment]; + + await approvalManager.updateStatusComment(statusBody); + + expect(mockGithub.rest.issues.updateComment).not.toHaveBeenCalled(); + }); + + it("should create new status comment if none exists", async () => { + approvalManager.comments = []; + const statusBody = "## Approval status: New status"; + + await approvalManager.updateStatusComment(statusBody); + + expect(mockGithub.rest.issues.createComment).toHaveBeenCalledWith({ + owner: mockOrg, + repo: mockRepo, + issue_number: mockIssueNumber, + body: statusBody, + }); + }); + }); + + describe("processComments", () => { + beforeEach(() => { + approvalManager.coreTeamMembers = ["core1", "core2", "core3"]; + approvalManager.maintainerTeamMembers = ["maintainer1", "maintainer2"]; + }); + + it("should process core member approvals", () => { + approvalManager.comments = [ + { user: { login: "core1" }, body: "/approve" }, + { user: { login: "core2" }, body: "/approve this looks good" }, + ]; + + approvalManager.processComments(); + + expect(approvalManager.coreApprovals.has("core1")).toBe(true); + expect(approvalManager.coreApprovals.has("core2")).toBe(true); + expect(approvalManager.awaitingCore).toEqual(["core3"]); + }); + + it("should process maintainer approvals", () => { + approvalManager.comments = [ + { user: { login: "maintainer1" }, body: "/approve" }, + { user: { login: "maintainer2" }, body: "This is great\n/approve" }, + ]; + + approvalManager.processComments(); + + expect(approvalManager.maintainerApprovals.has("maintainer1")).toBe(true); + expect(approvalManager.maintainerApprovals.has("maintainer2")).toBe(true); + expect(approvalManager.awaitingMaintainers).toEqual([]); + }); + + it("should process core member rejections", () => { + approvalManager.comments = [ + { user: { login: "core1" }, body: "/reject" }, + { user: { login: "core2" }, body: "/reject needs more work" }, + ]; + + approvalManager.processComments(); + + expect(approvalManager.coreRejections.has("core1")).toBe(true); + expect(approvalManager.coreRejections.has("core2")).toBe(true); + expect(approvalManager.awaitingCore).toEqual(["core3"]); + }); + + it("should process maintainer rejections", () => { + approvalManager.comments = [ + { user: { login: "maintainer1" }, body: "/reject" }, + { user: { login: "maintainer2" }, body: "Issues found\n/reject" }, + ]; + + approvalManager.processComments(); + + expect(approvalManager.maintainerRejections.has("maintainer1")).toBe(true); + expect(approvalManager.maintainerRejections.has("maintainer2")).toBe(true); + expect(approvalManager.awaitingMaintainers).toEqual([]); + }); + + it("should ignore comments from non-team members", () => { + approvalManager.comments = [ + { user: { login: "external_user" }, body: "/approve" }, + { user: { login: "another_user" }, body: "/reject" }, + ]; + + approvalManager.processComments(); + + expect(approvalManager.coreApprovals.size).toBe(0); + expect(approvalManager.maintainerApprovals.size).toBe(0); + expect(approvalManager.coreRejections.size).toBe(0); + expect(approvalManager.maintainerRejections.size).toBe(0); + }); + + it("should handle case-insensitive approve/reject commands", () => { + approvalManager.comments = [ + { user: { login: "core1" }, body: "/APPROVE" }, + { user: { login: "core2" }, body: "/Reject" }, + { user: { login: "maintainer1" }, body: "/approve" }, + { user: { login: "maintainer2" }, body: "/REJECT" }, + ]; + + approvalManager.processComments(); + + expect(approvalManager.coreApprovals.has("core1")).toBe(true); + expect(approvalManager.coreRejections.has("core2")).toBe(true); + expect(approvalManager.maintainerApprovals.has("maintainer1")).toBe(true); + expect(approvalManager.maintainerRejections.has("maintainer2")).toBe(true); + }); + + it("should only process commands at start of line", () => { + approvalManager.comments = [ + { user: { login: "core1" }, body: "I think we should /approve this" }, // should be ignored + { user: { login: "core2" }, body: "/approve" }, // should be processed + { user: { login: "maintainer1" }, body: "Please /reject this change" }, // should be ignored + { user: { login: "maintainer2" }, body: "/reject" }, // should be processed + ]; + + approvalManager.processComments(); + + expect(approvalManager.coreApprovals.has("core1")).toBe(false); + expect(approvalManager.coreApprovals.has("core2")).toBe(true); + expect(approvalManager.maintainerRejections.has("maintainer1")).toBe(false); + expect(approvalManager.maintainerRejections.has("maintainer2")).toBe(true); + }); + + it("should handle multiple commands from same user (last command per comment wins)", () => { + approvalManager.comments = [ + { user: { login: "core1" }, body: "/approve" }, + { user: { login: "core1" }, body: "/reject" }, + ]; + + approvalManager.processComments(); + + // The user should be in rejections, not approvals (last comment wins) + expect(approvalManager.coreApprovals.has("core1")).toBe(false); + expect(approvalManager.coreRejections.has("core1")).toBe(true); + }); + + it("should handle multiple commands within same comment (last command wins)", () => { + approvalManager.comments = [ + { user: { login: "core1" }, body: "/approve\n/reject\n/approve" }, + { user: { login: "maintainer1" }, body: "/reject\n/approve" }, + ]; + + approvalManager.processComments(); + + // Core1 should be approved (last command in comment) + expect(approvalManager.coreApprovals.has("core1")).toBe(true); + expect(approvalManager.coreRejections.has("core1")).toBe(false); + + // Maintainer1 should be approved (last command in comment) + expect(approvalManager.maintainerApprovals.has("maintainer1")).toBe(true); + expect(approvalManager.maintainerRejections.has("maintainer1")).toBe(false); + }); + + it("should reset approval sets before processing", () => { + // Set initial state + approvalManager.coreApprovals.add("core1"); + approvalManager.maintainerApprovals.add("maintainer1"); + + // No comments this time + approvalManager.comments = []; + + approvalManager.processComments(); + + expect(approvalManager.coreApprovals.size).toBe(0); + expect(approvalManager.maintainerApprovals.size).toBe(0); + }); + }); + + describe("Pipeline Proposal Scenarios", () => { + beforeEach(() => { + approvalManager.coreTeamMembers = ["core1", "core2", "core3"]; + approvalManager.maintainerTeamMembers = ["maintainer1", "maintainer2"]; + }); + + it("should approve with 2 core members", () => { + approvalManager.comments = [ + { user: { login: "core1" }, body: "/approve" }, + { user: { login: "core2" }, body: "/approve" }, + ]; + + approvalManager.processComments(); + + expect(approvalManager.coreApprovals.size).toBe(2); + // Pipeline approval logic: 2 core OR 1 core + 1 maintainer + const isApproved = + approvalManager.coreApprovals.size >= 2 || + (approvalManager.coreApprovals.size >= 1 && approvalManager.maintainerApprovals.size >= 1); + expect(isApproved).toBe(true); + }); + + it("should approve with 1 core + 1 maintainer", () => { + approvalManager.comments = [ + { user: { login: "core1" }, body: "/approve" }, + { user: { login: "maintainer1" }, body: "/approve" }, + ]; + + approvalManager.processComments(); + + expect(approvalManager.coreApprovals.size).toBe(1); + expect(approvalManager.maintainerApprovals.size).toBe(1); + // Pipeline approval logic: 2 core OR 1 core + 1 maintainer + const isApproved = + approvalManager.coreApprovals.size >= 2 || + (approvalManager.coreApprovals.size >= 1 && approvalManager.maintainerApprovals.size >= 1); + expect(isApproved).toBe(true); + }); + + it("should not approve with only 1 core member", () => { + approvalManager.comments = [{ user: { login: "core1" }, body: "/approve" }]; + + approvalManager.processComments(); + + expect(approvalManager.coreApprovals.size).toBe(1); + expect(approvalManager.maintainerApprovals.size).toBe(0); + // Pipeline approval logic: 2 core OR 1 core + 1 maintainer + const isApproved = + approvalManager.coreApprovals.size >= 2 || + (approvalManager.coreApprovals.size >= 1 && approvalManager.maintainerApprovals.size >= 1); + expect(isApproved).toBe(false); + }); + + it("should not approve with only maintainers", () => { + approvalManager.comments = [ + { user: { login: "maintainer1" }, body: "/approve" }, + { user: { login: "maintainer2" }, body: "/approve" }, + ]; + + approvalManager.processComments(); + + expect(approvalManager.coreApprovals.size).toBe(0); + expect(approvalManager.maintainerApprovals.size).toBe(2); + // Pipeline approval logic: 2 core OR 1 core + 1 maintainer + const isApproved = + approvalManager.coreApprovals.size >= 2 || + (approvalManager.coreApprovals.size >= 1 && approvalManager.maintainerApprovals.size >= 1); + expect(isApproved).toBe(false); + }); + }); + + describe("RFC Proposal Scenarios", () => { + beforeEach(() => { + approvalManager.coreTeamMembers = ["core1", "core2", "core3", "core4", "core5"]; + approvalManager.maintainerTeamMembers = ["maintainer1", "maintainer2"]; + }); + + it("should approve RFC with quorum of core members", () => { + const quorum = Math.ceil(approvalManager.coreTeamMembers.length / 2); // 3 for 5 members + + approvalManager.comments = [ + { user: { login: "core1" }, body: "/approve" }, + { user: { login: "core2" }, body: "/approve" }, + { user: { login: "core3" }, body: "/approve" }, + ]; + + approvalManager.processComments(); + + expect(approvalManager.coreApprovals.size).toBe(3); + expect(approvalManager.coreApprovals.size >= quorum).toBe(true); + }); + + it("should not approve RFC without quorum", () => { + const quorum = Math.ceil(approvalManager.coreTeamMembers.length / 2); // 3 for 5 members + + approvalManager.comments = [ + { user: { login: "core1" }, body: "/approve" }, + { user: { login: "core2" }, body: "/approve" }, + ]; + + approvalManager.processComments(); + + expect(approvalManager.coreApprovals.size).toBe(2); + expect(approvalManager.coreApprovals.size >= quorum).toBe(false); + }); + + it("should calculate quorum correctly for different team sizes", () => { + // Test different team sizes + expect(Math.ceil(1 / 2)).toBe(1); // 1 member needs 1 approval + expect(Math.ceil(2 / 2)).toBe(1); // 2 members need 1 approval + expect(Math.ceil(3 / 2)).toBe(2); // 3 members need 2 approvals + expect(Math.ceil(4 / 2)).toBe(2); // 4 members need 2 approvals + expect(Math.ceil(5 / 2)).toBe(3); // 5 members need 3 approvals + expect(Math.ceil(6 / 2)).toBe(3); // 6 members need 3 approvals + expect(Math.ceil(7 / 2)).toBe(4); // 7 members need 4 approvals + }); + + it("should ignore maintainer approvals for RFC (core team only)", () => { + approvalManager.comments = [ + { user: { login: "core1" }, body: "/approve" }, + { user: { login: "core2" }, body: "/approve" }, + { user: { login: "maintainer1" }, body: "/approve" }, + { user: { login: "maintainer2" }, body: "/approve" }, + ]; + + approvalManager.processComments(); + + expect(approvalManager.coreApprovals.size).toBe(2); + expect(approvalManager.maintainerApprovals.size).toBe(2); + + // For RFC, only core approvals matter + const quorum = Math.ceil(approvalManager.coreTeamMembers.length / 2); // 3 for 5 members + expect(approvalManager.coreApprovals.size >= quorum).toBe(false); + }); + }); + + describe("Edge Cases and Error Handling", () => { + it("should handle empty comments array", () => { + approvalManager.coreTeamMembers = ["core1", "core2"]; + approvalManager.maintainerTeamMembers = ["maintainer1"]; + approvalManager.comments = []; + + approvalManager.processComments(); + + expect(approvalManager.coreApprovals.size).toBe(0); + expect(approvalManager.maintainerApprovals.size).toBe(0); + expect(approvalManager.awaitingCore).toEqual(["core1", "core2"]); + expect(approvalManager.awaitingMaintainers).toEqual(["maintainer1"]); + }); + + it("should handle comments with no body", () => { + approvalManager.coreTeamMembers = ["core1"]; + approvalManager.comments = [ + { user: { login: "core1" }, body: null }, + { user: { login: "core1" }, body: undefined }, + { user: { login: "core1" }, body: "" }, + ]; + + expect(() => approvalManager.processComments()).not.toThrow(); + }); + + it("should handle malformed comment bodies", () => { + approvalManager.coreTeamMembers = ["core1"]; + approvalManager.comments = [ + { user: { login: "core1" }, body: "//approve" }, // should not match (double slash) + { user: { login: "core1" }, body: "approve" }, // should not match (no slash) + { user: { login: "core1" }, body: "text/approve" }, // should not match (no space before slash) + ]; + + approvalManager.processComments(); + + expect(approvalManager.coreApprovals.size).toBe(0); + expect(approvalManager.coreRejections.size).toBe(0); + }); + + it("should handle comments with mixed line endings", () => { + approvalManager.coreTeamMembers = ["core1", "core2"]; + approvalManager.comments = [ + { user: { login: "core1" }, body: "Some text\r/approve\rMore text" }, + { user: { login: "core2" }, body: "Some text\n/reject\nMore text" }, + ]; + + approvalManager.processComments(); + + expect(approvalManager.coreApprovals.has("core1")).toBe(true); + expect(approvalManager.coreRejections.has("core2")).toBe(true); + }); + + it("should handle user being in both core and maintainer teams", () => { + approvalManager.coreTeamMembers = ["user1", "core2"]; + approvalManager.maintainerTeamMembers = ["user1", "maintainer2"]; // user1 is in both + approvalManager.comments = [{ user: { login: "user1" }, body: "/approve" }]; + + approvalManager.processComments(); + + // User should be counted as core member (checked first) + expect(approvalManager.coreApprovals.has("user1")).toBe(true); + expect(approvalManager.maintainerApprovals.has("user1")).toBe(false); + }); + }); +}); diff --git a/.github/workflows/lib/package.json b/.github/workflows/lib/package.json new file mode 100644 index 0000000..988da5b --- /dev/null +++ b/.github/workflows/lib/package.json @@ -0,0 +1,30 @@ +{ + "name": "github-approval-automation", + "version": "1.0.0", + "description": "Tests for GitHub approval automation workflows", + "main": "approval.js", + "scripts": { + "test": "jest", + "test:watch": "jest --watch", + "test:coverage": "jest --coverage" + }, + "devDependencies": { + "jest": "^29.0.0" + }, + "jest": { + "testEnvironment": "node", + "collectCoverageFrom": [ + "approval.js" + ], + "testMatch": [ + "**/*.test.js" + ], + "coverageDirectory": "coverage", + "coverageReporters": [ + "text", + "lcov", + "html", + "json-summary" + ] + } +} diff --git a/.github/workflows/lib/workflow-integration.test.js b/.github/workflows/lib/workflow-integration.test.js new file mode 100644 index 0000000..70c3b75 --- /dev/null +++ b/.github/workflows/lib/workflow-integration.test.js @@ -0,0 +1,356 @@ +const ApprovalManager = require("./approval.js"); + +// Mock GitHub API for integration tests +const mockGithub = { + request: jest.fn(), + paginate: jest.fn(), + rest: { + issues: { + listComments: jest.fn(), + update: jest.fn(), + updateComment: jest.fn(), + createComment: jest.fn(), + }, + }, +}; + +describe("Workflow Integration Tests", () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + describe("Pipeline Proposal Workflow", () => { + let approvalManager; + const mockOrg = "nf-core"; + const mockRepo = "proposals"; + const mockIssueNumber = 42; + + beforeEach(async () => { + // Mock team members + mockGithub.request + .mockResolvedValueOnce({ + data: [{ login: "core_alice" }, { login: "core_bob" }, { login: "core_charlie" }], + }) + .mockResolvedValueOnce({ + data: [{ login: "maintainer_dave" }, { login: "maintainer_eve" }], + }); + + // Mock empty comments initially + mockGithub.paginate.mockResolvedValue([]); + + approvalManager = await new ApprovalManager(mockGithub, mockOrg, mockRepo, mockIssueNumber).initialize(); + }); + + it("should approve pipeline with 2 core team members", async () => { + // Simulate comments with approvals + approvalManager.comments = [ + { + id: 1, + user: { login: "core_alice" }, + body: "/approve\n\nThis pipeline looks great!", + }, + { + id: 2, + user: { login: "core_bob" }, + body: "LGTM\n/approve", + }, + ]; + + approvalManager.processComments(); + + // Check approval logic for pipeline proposals + const isApproved = + approvalManager.coreApprovals.size >= 2 || + (approvalManager.coreApprovals.size >= 1 && approvalManager.maintainerApprovals.size >= 1); + + expect(isApproved).toBe(true); + expect(approvalManager.coreApprovals.size).toBe(2); + expect(approvalManager.maintainerApprovals.size).toBe(0); + + // Test status generation + const status = "✅ Approved"; + + // Verify the approval manager would update the issue correctly + await approvalManager.updateIssueStatus(status); + expect(mockGithub.rest.issues.update).toHaveBeenCalledWith({ + owner: mockOrg, + repo: mockRepo, + issue_number: mockIssueNumber, + labels: ["accepted"], + }); + }); + + it("should approve pipeline with 1 core + 1 maintainer", async () => { + approvalManager.comments = [ + { + id: 1, + user: { login: "core_alice" }, + body: "/approve", + }, + { + id: 2, + user: { login: "maintainer_dave" }, + body: "/approve", + }, + ]; + + approvalManager.processComments(); + + const isApproved = + approvalManager.coreApprovals.size >= 2 || + (approvalManager.coreApprovals.size >= 1 && approvalManager.maintainerApprovals.size >= 1); + + expect(isApproved).toBe(true); + expect(approvalManager.coreApprovals.size).toBe(1); + expect(approvalManager.maintainerApprovals.size).toBe(1); + }); + + it("should not approve pipeline with insufficient votes", async () => { + approvalManager.comments = [ + { + id: 1, + user: { login: "core_alice" }, + body: "/approve", + }, + { + id: 2, + user: { login: "external_user" }, + body: "/approve", // External user vote doesn't count + }, + ]; + + approvalManager.processComments(); + + const isApproved = + approvalManager.coreApprovals.size >= 2 || + (approvalManager.coreApprovals.size >= 1 && approvalManager.maintainerApprovals.size >= 1); + + expect(isApproved).toBe(false); + expect(approvalManager.coreApprovals.size).toBe(1); + expect(approvalManager.maintainerApprovals.size).toBe(0); + }); + + it("should handle rejection in pipeline workflow", async () => { + approvalManager.comments = [ + { + id: 1, + user: { login: "core_alice" }, + body: "/approve", + }, + { + id: 2, + user: { login: "core_bob" }, + body: "/reject\n\nThis needs more work", + }, + ]; + + approvalManager.processComments(); + + // Even with rejections, check if there are enough approvals + const isApproved = + approvalManager.coreApprovals.size >= 2 || + (approvalManager.coreApprovals.size >= 1 && approvalManager.maintainerApprovals.size >= 1); + + const hasRejections = approvalManager.coreRejections.size > 0 || approvalManager.maintainerRejections.size > 0; + + expect(isApproved).toBe(false); + expect(hasRejections).toBe(true); + expect(approvalManager.coreApprovals.size).toBe(1); + expect(approvalManager.coreRejections.size).toBe(1); + }); + }); + + describe("RFC Proposal Workflow", () => { + let approvalManager; + const mockOrg = "nf-core"; + const mockRepo = "proposals"; + const mockIssueNumber = 84; + + beforeEach(async () => { + // Mock core team with 5 members (quorum = 3) + mockGithub.request + .mockResolvedValueOnce({ + data: [ + { login: "core_alice" }, + { login: "core_bob" }, + { login: "core_charlie" }, + { login: "core_diana" }, + { login: "core_eve" }, + ], + }) + .mockResolvedValueOnce({ + data: [{ login: "maintainer_frank" }, { login: "maintainer_grace" }], + }); + + mockGithub.paginate.mockResolvedValue([]); + + approvalManager = await new ApprovalManager(mockGithub, mockOrg, mockRepo, mockIssueNumber).initialize(); + }); + + it("should approve RFC with core team quorum", async () => { + const quorum = Math.ceil(approvalManager.coreTeamMembers.length / 2); // 3 for 5 members + + approvalManager.comments = [ + { id: 1, user: { login: "core_alice" }, body: "/approve" }, + { id: 2, user: { login: "core_bob" }, body: "/approve" }, + { id: 3, user: { login: "core_charlie" }, body: "/approve" }, + ]; + + approvalManager.processComments(); + + const isApproved = approvalManager.coreApprovals.size >= quorum; + + expect(isApproved).toBe(true); + expect(approvalManager.coreApprovals.size).toBe(3); + expect(quorum).toBe(3); + + await approvalManager.updateIssueStatus("✅ Approved"); + expect(mockGithub.rest.issues.update).toHaveBeenCalledWith({ + owner: mockOrg, + repo: mockRepo, + issue_number: mockIssueNumber, + labels: ["accepted"], + }); + }); + + it("should not approve RFC without quorum", async () => { + const quorum = Math.ceil(approvalManager.coreTeamMembers.length / 2); // 3 for 5 members + + approvalManager.comments = [ + { id: 1, user: { login: "core_alice" }, body: "/approve" }, + { id: 2, user: { login: "core_bob" }, body: "/approve" }, + // Only 2 approvals, need 3 + ]; + + approvalManager.processComments(); + + const isApproved = approvalManager.coreApprovals.size >= quorum; + + expect(isApproved).toBe(false); + expect(approvalManager.coreApprovals.size).toBe(2); + expect(quorum).toBe(3); + }); + + it("should ignore maintainer votes in RFC workflow", async () => { + const quorum = Math.ceil(approvalManager.coreTeamMembers.length / 2); // 3 for 5 members + + approvalManager.comments = [ + { id: 1, user: { login: "core_alice" }, body: "/approve" }, + { id: 2, user: { login: "core_bob" }, body: "/approve" }, + { id: 3, user: { login: "maintainer_frank" }, body: "/approve" }, + { id: 4, user: { login: "maintainer_grace" }, body: "/approve" }, + // Only 2 core approvals, maintainer votes don't count for RFC + ]; + + approvalManager.processComments(); + + const isApproved = approvalManager.coreApprovals.size >= quorum; + + expect(isApproved).toBe(false); + expect(approvalManager.coreApprovals.size).toBe(2); + expect(approvalManager.maintainerApprovals.size).toBe(2); // Tracked but not used for RFC approval + expect(quorum).toBe(3); + }); + + it("should handle RFC rejection with core team voting", async () => { + approvalManager.comments = [ + { id: 1, user: { login: "core_alice" }, body: "/approve" }, + { id: 2, user: { login: "core_bob" }, body: "/approve" }, + { id: 3, user: { login: "core_charlie" }, body: "/reject\n\nI disagree with this approach" }, + ]; + + approvalManager.processComments(); + + const quorum = Math.ceil(approvalManager.coreTeamMembers.length / 2); // 3 for 5 members + const isApproved = approvalManager.coreApprovals.size >= quorum; + const hasRejections = approvalManager.coreRejections.size > 0; + + expect(isApproved).toBe(false); + expect(hasRejections).toBe(true); + expect(approvalManager.coreApprovals.size).toBe(2); + expect(approvalManager.coreRejections.size).toBe(1); + }); + }); + + describe("Status Comment Generation", () => { + let approvalManager; + + beforeEach(async () => { + mockGithub.request + .mockResolvedValueOnce({ data: [{ login: "core1" }, { login: "core2" }] }) + .mockResolvedValueOnce({ data: [{ login: "maintainer1" }] }); + + mockGithub.paginate.mockResolvedValue([]); + + approvalManager = await new ApprovalManager(mockGithub, "org", "repo", 123).initialize(); + }); + + it("should update existing status comment when content changes", async () => { + const existingComment = { + id: 456, + body: "## Approval status: 🕐 Pending\n\nOld content", + }; + approvalManager.comments = [existingComment]; + + const newStatusBody = "## Approval status: ✅ Approved\n\nNew content"; + + await approvalManager.updateStatusComment(newStatusBody); + + expect(mockGithub.rest.issues.updateComment).toHaveBeenCalledWith({ + owner: "org", + repo: "repo", + comment_id: 456, + body: newStatusBody, + }); + }); + + it("should create new status comment when none exists", async () => { + approvalManager.comments = []; // No existing status comment + + const statusBody = "## Approval status: 🕐 Pending\n\nInitial status"; + + await approvalManager.updateStatusComment(statusBody); + + expect(mockGithub.rest.issues.createComment).toHaveBeenCalledWith({ + owner: "org", + repo: "repo", + issue_number: 123, + body: statusBody, + }); + }); + }); + + describe("Label Management", () => { + let approvalManager; + + beforeEach(async () => { + mockGithub.request.mockResolvedValueOnce({ data: [] }).mockResolvedValueOnce({ data: [] }); + + mockGithub.paginate.mockResolvedValue([]); + + approvalManager = await new ApprovalManager(mockGithub, "org", "repo", 123).initialize(); + }); + + it("should set correct labels for different statuses", async () => { + const statusLabelMap = [ + ["✅ Approved", ["accepted"]], + ["❌ Rejected", ["turned-down"]], + ["⏰ Timed Out", ["timed-out"]], + ["🕐 Pending", ["proposed"]], + ]; + + for (const [status, expectedLabels] of statusLabelMap) { + mockGithub.rest.issues.update.mockClear(); + + await approvalManager.updateIssueStatus(status); + + expect(mockGithub.rest.issues.update).toHaveBeenCalledWith({ + owner: "org", + repo: "repo", + issue_number: 123, + labels: expectedLabels, + }); + } + }); + }); +}); diff --git a/.github/workflows/pipeline_proposals.yml b/.github/workflows/pipeline_proposals.yml index 4030524..12af504 100644 --- a/.github/workflows/pipeline_proposals.yml +++ b/.github/workflows/pipeline_proposals.yml @@ -9,7 +9,7 @@ on: jobs: pipeline_approval: # Only run for pipeline proposal issues - if: startsWith(github.event.issue.title, 'New Pipeline') + if: startsWith(github.event.issue.title, 'New pipeline') runs-on: ubuntu-latest steps: @@ -18,235 +18,88 @@ jobs: with: github-token: ${{ secrets.nf_core_bot_auth_token }} script: | + const ApprovalManager = require('./.github/workflows/lib/approval.js'); + const issueNumber = context.issue.number; const org = context.repo.owner; const repo = context.repo.repo; + // Initialize approval manager + const approvalManager = await new ApprovalManager(github, org, repo, issueNumber).initialize(); + // Ignore comments on closed issues if (context.eventName === 'issue_comment' && context.payload.issue.state === 'closed') { console.log('Comment event on closed issue, ignoring.'); return; } - // --------------------------------------------- - // Fetch members of the core and maintainer teams - // --------------------------------------------- - async function getTeamMembers(teamSlug) { - try { - const res = await github.request('GET /orgs/{org}/teams/{team_slug}/members', { - org, - team_slug: teamSlug, - per_page: 100 - }); - console.log(`Fetched ${res.data.length} ${teamSlug} team members.`); - return res.data.map(m => m.login); - } catch (err) { - console.error(`Failed to fetch ${teamSlug} team members:`, err); - throw err; - } - } - const coreTeamMembers = await getTeamMembers('core'); - const maintainerTeamMembers = await getTeamMembers('maintainers'); - console.log('Core team members:', coreTeamMembers); - console.log('Maintainer team members:', maintainerTeamMembers); + function generateStatusBody(status) { - // Helper for list formatting and complete status body - function formatUserList(users) { - return users.length ? users.map(u => `[@${u}](https://github.com/${u})`).join(', ') : '-'; - } + const coreApprovers = [...approvalManager.coreApprovals]; + const maintainerApprovers = [...approvalManager.maintainerApprovals]; + const rejecters = [...approvalManager.coreRejections, ...approvalManager.maintainerRejections]; + const awaitingCore = [...approvalManager.awaitingCore]; + const awaitingMaintainers = [...approvalManager.awaitingMaintainers]; - function generateStatusBody(status, coreApprovalsSet, maintainerApprovalsSet, rejectionsSet, awaitingCore, awaitingMaintainers) { - const coreApprovers = [...coreApprovalsSet]; - const maintainerApprovers = [...maintainerApprovalsSet]; - const rejecters = [...rejectionsSet]; - let body = `## Pipeline proposal approval status: ${status}\n\n`; + let body = `## Approval status: ${status}\n\n`; body += `Required approvals: Either 2 core team members OR 1 core team member + 1 maintainer\n\n`; if (coreApprovers.length > 0 || maintainerApprovers.length > 0 || rejecters.length > 0 || awaitingCore.length > 0 || awaitingMaintainers.length > 0) { body += `|Review Status|Team members|\n|--|--|\n`; if (coreApprovers.length > 0) { - body += `| ✅ Approved (Core) | ${formatUserList(coreApprovers)} |\n`; + body += `| ✅ Approved (Core) | ${approvalManager.formatUserList(coreApprovers)} |\n`; } if (maintainerApprovers.length > 0) { - body += `| ✅ Approved (Maintainer) | ${formatUserList(maintainerApprovers)} |\n`; + body += `| ✅ Approved (Maintainer) | ${approvalManager.formatUserList(maintainerApprovers)} |\n`; } if (rejecters.length > 0) { - body += `| ❌ Rejected | ${formatUserList(rejecters)} |\n`; + body += `| ❌ Rejected | ${approvalManager.formatUserList(rejecters)} |\n`; } if (awaitingCore.length > 0) { - body += `| 🕐 Pending (Core) | ${formatUserList(awaitingCore)} |\n`; + body += `| 🕐 Pending (Core) | ${approvalManager.formatUserList(awaitingCore)} |\n`; } if (awaitingMaintainers.length > 0) { - body += `| 🕐 Pending (Maintainer) | ${formatUserList(awaitingMaintainers)} |\n`; + body += `| 🕐 Pending (Maintainer) | ${approvalManager.formatUserList(awaitingMaintainers)} |\n`; } } return body; } - // Helper function to update issue status and labels - async function updateIssueStatus(status) { - const labels = []; - - switch (status) { - case '✅ Approved': - labels.push('accepted'); - break; - case '❌ Rejected': - labels.push('turned-down'); - break; - case '⏰ Timed Out': - labels.push('timed-out'); - break; - default: - labels.push('proposed'); - } - - // Update labels - await github.rest.issues.update({ - owner: org, - repo, - issue_number: issueNumber, - labels: labels - }); - } - // Handle label changes if (context.eventName === 'issues' && (context.payload.action === 'labeled' || context.payload.action === 'unlabeled')) { const label = context.payload.label.name; if (label === 'timed-out') { console.log('Timed-out label detected, updating status'); - const statusBody = generateStatusBody('⏰ Timed Out', new Set(), new Set(), new Set(), [], []); - - // Find and update the status comment - const comments = await github.paginate(github.rest.issues.listComments, { - owner: org, - repo, - issue_number: issueNumber, - per_page: 100 - }); - - let statusComment = comments.find(c => c.body.startsWith('## Pipeline proposal approval status:')); - if (statusComment) { - await github.rest.issues.updateComment({ - owner: org, - repo, - comment_id: statusComment.id, - body: statusBody - }); - } else { - await github.rest.issues.createComment({ - owner: org, - repo, - issue_number: issueNumber, - body: statusBody - }); - } + const statusBody = generateStatusBody('⏰ Timed Out'); + await approvalManager.updateStatusComment(statusBody); return; } } - // ------------------------------------------------- - // If this workflow was triggered by issue creation - // ------------------------------------------------- + // Handle new issue creation if (context.eventName === 'issues' && context.payload.action === 'opened') { - const body = generateStatusBody('🕐 Pending', new Set(), new Set(), new Set(), coreTeamMembers, maintainerTeamMembers); + const body = generateStatusBody('🕐 Pending'); console.log('Creating initial comment for review status'); - - await github.rest.issues.createComment({ - owner: org, - repo, - issue_number: issueNumber, - body - }); - - // Set initial status to proposed - await updateIssueStatus('🕐 Pending'); + await approvalManager.updateStatusComment(body); + await approvalManager.updateIssueStatus('🕐 Pending'); return; } - // --------------------------------------------------------------------- - // Collect comments and compute votes (shared for comment & closed events) - // --------------------------------------------------------------------- - - // Collect all comments on the issue - const comments = await github.paginate(github.rest.issues.listComments, { - owner: org, - repo, - issue_number: issueNumber, - per_page: 100 - }); - - const coreApprovals = new Set(); - const maintainerApprovals = new Set(); - const rejections = new Set(); - - for (const comment of comments) { - const commenter = comment.user.login; - const isCoreMember = coreTeamMembers.includes(commenter); - const isMaintainer = maintainerTeamMembers.includes(commenter); - - if (!isCoreMember && !isMaintainer) continue; // Only team members count - - // Count approvals / rejections based on line starting with /approve or /reject - const lines = comment.body.split(/\r?\n/); - for (const rawLine of lines) { - const line = rawLine.trim(); - if (/^\/approve\b/i.test(line)) { - if (isCoreMember) { - coreApprovals.add(commenter); - } else if (isMaintainer) { - maintainerApprovals.add(commenter); - } - } else if (/^\/reject\b/i.test(line)) { - rejections.add(commenter); - } - } - } - console.log(`Core approvals (${coreApprovals.size}):`, [...coreApprovals]); - console.log(`Maintainer approvals (${maintainerApprovals.size}):`, [...maintainerApprovals]); - console.log(`Rejections (${rejections.size}):`, [...rejections]); - const awaitingCore = coreTeamMembers.filter(u => !coreApprovals.has(u) && !rejections.has(u)); - const awaitingMaintainers = maintainerTeamMembers.filter(u => !maintainerApprovals.has(u) && !rejections.has(u)); // Determine status let status = '🕐 Pending'; - if (context.eventName === 'issues' && context.payload.action === 'closed' && context.payload.issue.state_reason === 'not_planned' && rejections.size > 0) { + if (context.eventName === 'issues' && context.payload.action === 'closed' && context.payload.issue.state_reason === 'not_planned' && (approvalManager.coreRejections.size > 0 || approvalManager.maintainerRejections.size > 0)) { status = '❌ Rejected'; - } else if ((coreApprovals.size >= 2) || (coreApprovals.size >= 1 && maintainerApprovals.size >= 1)) { + } else if ((approvalManager.coreApprovals.size >= 2) || (approvalManager.coreApprovals.size >= 1 && approvalManager.maintainerApprovals.size >= 1)) { status = '✅ Approved'; } - const statusBody = generateStatusBody(status, coreApprovals, maintainerApprovals, rejections, awaitingCore, awaitingMaintainers); + const statusBody = generateStatusBody(status); console.log('New status body to post:\n', statusBody); - // Try to locate the existing status comment (starts with our header) - let statusComment = comments.find(c => c.body.startsWith('## Pipeline proposal approval status:')); - - if (statusComment) { - if (statusComment.body.trim() === statusBody.trim()) { - console.log('Status comment already up to date - no update required.'); - } else { - console.log('Updating existing status comment.'); - await github.rest.issues.updateComment({ - owner: org, - repo, - comment_id: statusComment.id, - body: statusBody - }); - } - } else { - // Fallback: create a new status comment if missing (shouldn't normally happen) - await github.rest.issues.createComment({ - owner: org, - repo, - issue_number: issueNumber, - body: statusBody - }); - } - - // Update issue status and labels - await updateIssueStatus(status); + await approvalManager.updateStatusComment(statusBody); + await approvalManager.updateIssueStatus(status); diff --git a/.github/workflows/rfc_approval.yml b/.github/workflows/rfc_approval.yml index 5f077d0..cd84af9 100644 --- a/.github/workflows/rfc_approval.yml +++ b/.github/workflows/rfc_approval.yml @@ -2,7 +2,7 @@ name: RFC approval automation on: issues: - types: [opened, closed] + types: [opened, closed, labeled, unlabeled] issue_comment: types: [created, edited] @@ -18,149 +18,77 @@ jobs: with: github-token: ${{ secrets.nf_core_bot_auth_token }} script: | + const ApprovalManager = require('./.github/workflows/lib/approval.js'); + const issueNumber = context.issue.number; const org = context.repo.owner; const repo = context.repo.repo; + // Initialize approval manager + const approvalManager = await new ApprovalManager(github, org, repo, issueNumber).initialize(); + // Ignore comments on closed issues if (context.eventName === 'issue_comment' && context.payload.issue.state === 'closed') { console.log('Comment event on closed issue, ignoring.'); return; } - // --------------------------------------------- - // Fetch members of the core team - // --------------------------------------------- - async function getTeamMembers() { - try { - const res = await github.request('GET /orgs/{org}/teams/{team_slug}/members', { - org, - team_slug: 'core', - per_page: 100 - }); - console.log(`Fetched ${res.data.length} core team members.`); - return res.data.map(m => m.login); - } catch (err) { - console.error('Failed to fetch core team members:', err); - throw err; - } - } - - const teamMembers = await getTeamMembers(); - console.log('Core team members:', teamMembers); - const quorum = Math.ceil(teamMembers.length / 2); + const quorum = Math.ceil(approvalManager.coreTeamMembers.length / 2); console.log(`Quorum set to ${quorum}.`); - // Helper for list formatting and complete status body - function formatUserList(users) { - return users.length ? users.map(u => `[@${u}](https://github.com/${u})`).join(', ') : '-'; - } - - function generateStatusBody(status, approvalsSet, rejectionsSet, awaitingArr) { - const approvers = [...approvalsSet]; - const rejecters = [...rejectionsSet]; - const awaiting = awaitingArr; - let body = `## RFC approval status: ${status}\n\nRFC has approvals from ${approvalsSet.size}/${quorum} required @core-team quorum.\n\n`; + function generateStatusBody(status) { + const approvers = [...approvalManager.coreApprovals]; + const rejecters = [...approvalManager.coreRejections]; + const awaiting = approvalManager.awaitingCore; + let body = `## Approval status: ${status}\n\nRFC has approvals from ${approvalManager.coreApprovals.size}/${quorum} required @core-team quorum.\n\n`; if (approvers.length > 0 || rejecters.length > 0 || awaiting.length > 0) { body += `|Review Status|Core Team members|\n|--|--|\n`; if (approvers.length > 0) { - body += `| ✅ Approved | ${formatUserList(approvers)} |\n`; + body += `| ✅ Approved | ${approvalManager.formatUserList(approvers)} |\n`; } if (rejecters.length > 0) { - body += `| ❌ Rejected | ${formatUserList(rejecters)} |\n`; + body += `| ❌ Rejected | ${approvalManager.formatUserList(rejecters)} |\n`; } if (awaiting.length > 0) { - body += `| 🕐 Pending | ${formatUserList(awaiting)} |\n`; + body += `| 🕐 Pending | ${approvalManager.formatUserList(awaiting)} |\n`; } } return body; } - // ------------------------------------------------- - // If this workflow was triggered by issue creation - // ------------------------------------------------- - if (context.eventName === 'issues' && context.payload.action === 'opened') { - const body = generateStatusBody('🕐 Pending', new Set(), new Set(), teamMembers); - console.log('Creating initial comment for review status'); - - await github.rest.issues.createComment({ - owner: org, - repo, - issue_number: issueNumber, - body - }); - return; // Nothing more to do for newly-opened issues + // Handle label changes + if (context.eventName === 'issues' && (context.payload.action === 'labeled' || context.payload.action === 'unlabeled')) { + const label = context.payload.label.name; + if (label === 'timed-out') { + console.log('Timed-out label detected, updating status'); + const statusBody = generateStatusBody('⏰ Timed Out'); + await approvalManager.updateStatusComment(statusBody); + return; + } } - // --------------------------------------------------------------------- - // Collect comments and compute votes (shared for comment & closed events) - // --------------------------------------------------------------------- - - // Collect all comments on the issue - const comments = await github.paginate(github.rest.issues.listComments, { - owner: org, - repo, - issue_number: issueNumber, - per_page: 100 - }); - - const approvals = new Set(); - const rejections = new Set(); - - for (const comment of comments) { - const commenter = comment.user.login; - if (!teamMembers.includes(commenter)) continue; // Only core team members count - - // Count approvals / rejections based on line starting with /approve or /reject - const lines = comment.body.split(/\r?\n/); - for (const rawLine of lines) { - const line = rawLine.trim(); - if (/^\/approve\b/i.test(line)) { - approvals.add(commenter); - } else if (/^\/reject\b/i.test(line)) { - rejections.add(commenter); - } - } + // Handle new issue creation + if (context.eventName === 'issues' && context.payload.action === 'opened') { + const body = generateStatusBody('🕐 Pending'); + console.log('Creating initial comment for review status'); + await approvalManager.updateStatusComment(body); + await approvalManager.updateIssueStatus('🕐 Pending'); + return; } - console.log(`Approvals (${approvals.size}):`, [...approvals]); - console.log(`Rejections (${rejections.size}):`, [...rejections]); - const awaiting = teamMembers.filter(u => !approvals.has(u) && !rejections.has(u)); // Determine status let status = '🕐 Pending'; - if (context.eventName === 'issues' && context.payload.action === 'closed' && context.payload.issue.state_reason === 'not_planned' && rejections.size > 0) { + if (context.eventName === 'issues' && context.payload.action === 'closed' && context.payload.issue.state_reason === 'not_planned' && (approvalManager.coreRejections.size > 0)) { status = '❌ Rejected'; - } else if (approvals.size >= quorum && rejections.size === 0) { + } else if (approvalManager.coreApprovals.size >= quorum) { status = '✅ Approved'; } - const statusBody = generateStatusBody(status, approvals, rejections, awaiting); + const statusBody = generateStatusBody(status); console.log('New status body to post:\n', statusBody); - // Try to locate the existing status comment (starts with our header) - let statusComment = comments.find(c => c.body.startsWith('## RFC approval status:')); - - if (statusComment) { - if (statusComment.body.trim() === statusBody.trim()) { - console.log('Status comment already up to date - no update required.'); - } else { - console.log('Updating existing status comment.'); - await github.rest.issues.updateComment({ - owner: org, - repo, - comment_id: statusComment.id, - body: statusBody - }); - } - } else { - // Fallback: create a new status comment if missing (shouldn't normally happen) - await github.rest.issues.createComment({ - owner: org, - repo, - issue_number: issueNumber, - body: statusBody - }); - } + await approvalManager.updateStatusComment(statusBody); + await approvalManager.updateIssueStatus(status); diff --git a/.github/workflows/test-approval-automation.yml b/.github/workflows/test-approval-automation.yml new file mode 100644 index 0000000..c1777b5 --- /dev/null +++ b/.github/workflows/test-approval-automation.yml @@ -0,0 +1,204 @@ +name: Test Approval Automation + +on: + pull_request: + types: [opened, synchronize, reopened] + paths: + - ".github/workflows/lib/**" + - ".github/workflows/pipeline_proposals.yml" + - ".github/workflows/rfc_approval.yml" + - ".github/workflows/test-approval-automation.yml" + push: + branches: [main, master] + paths: + - ".github/workflows/lib/**" + - ".github/workflows/pipeline_proposals.yml" + - ".github/workflows/rfc_approval.yml" + - ".github/workflows/test-approval-automation.yml" + +jobs: + test-approval-lib: + name: Test Approval Library + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: "18" + cache: "npm" + cache-dependency-path: ".github/workflows/lib/package-lock.json" + + - name: Install dependencies + working-directory: .github/workflows/lib + run: npm ci + + - name: Run tests + working-directory: .github/workflows/lib + run: npm test + + - name: Run tests with coverage + working-directory: .github/workflows/lib + run: npm run test:coverage + + - name: Upload coverage reports + uses: codecov/codecov-action@v3 + if: success() + with: + file: .github/workflows/lib/coverage/lcov.info + flags: approval-automation + name: approval-automation-coverage + fail_ci_if_error: false + + - name: Comment coverage on PR + if: github.event_name == 'pull_request' && success() + uses: actions/github-script@v7 + with: + script: | + const fs = require('fs'); + const path = require('path'); + + // Read coverage summary + const coveragePath = path.join('.github/workflows/lib/coverage/coverage-summary.json'); + + if (fs.existsSync(coveragePath)) { + const coverage = JSON.parse(fs.readFileSync(coveragePath, 'utf8')); + const total = coverage.total; + + const comment = `## 📊 Test Coverage Report + + | Metric | Coverage | Status | + |--------|----------|---------| + | Statements | ${total.statements.pct}% | ${total.statements.pct >= 90 ? '✅' : total.statements.pct >= 75 ? '⚠️' : '❌'} | + | Branches | ${total.branches.pct}% | ${total.branches.pct >= 90 ? '✅' : total.branches.pct >= 75 ? '⚠️' : '❌'} | + | Functions | ${total.functions.pct}% | ${total.functions.pct >= 90 ? '✅' : total.functions.pct >= 75 ? '⚠️' : '❌'} | + | Lines | ${total.lines.pct}% | ${total.lines.pct >= 90 ? '✅' : total.lines.pct >= 75 ? '⚠️' : '❌'} | + + **Tests:** ${total.statements.covered} statements covered out of ${total.statements.total} + + ${total.statements.pct >= 90 ? '🎉 Excellent coverage!' : total.statements.pct >= 75 ? '👍 Good coverage!' : '⚠️ Coverage could be improved'}`; + + // Find existing coverage comment + const { data: comments } = await github.rest.issues.listComments({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + }); + + const existingComment = comments.find(comment => + comment.body.includes('📊 Test Coverage Report') + ); + + if (existingComment) { + await github.rest.issues.updateComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: existingComment.id, + body: comment + }); + } else { + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + body: comment + }); + } + } + + lint-workflows: + name: Lint GitHub Actions Workflows + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: "18" + + - name: Install actionlint + run: | + bash <(curl https://raw.githubusercontent.com/rhymond/actionlint/main/scripts/download-actionlint.bash) + sudo mv ./actionlint /usr/local/bin/ + + - name: Lint workflow files + run: actionlint .github/workflows/*.yml + + validate-approval-logic: + name: Validate Approval Logic + runs-on: ubuntu-latest + needs: test-approval-lib + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: "18" + cache: "npm" + cache-dependency-path: ".github/workflows/lib/package-lock.json" + + - name: Install dependencies + working-directory: .github/workflows/lib + run: npm ci + + - name: Validate pipeline approval logic + working-directory: .github/workflows/lib + run: | + node -e " + const ApprovalManager = require('./approval.js'); + + // Test pipeline approval logic + const mockGithub = { request: () => Promise.resolve({ data: [] }), paginate: () => Promise.resolve([]) }; + const manager = new ApprovalManager(mockGithub, 'test', 'test', 1); + + // Test scenarios + manager.coreApprovals = new Set(['core1', 'core2']); + manager.maintainerApprovals = new Set(); + const pipeline2Core = (manager.coreApprovals.size >= 2) || (manager.coreApprovals.size >= 1 && manager.maintainerApprovals.size >= 1); + console.log('Pipeline 2 core approval:', pipeline2Core); + + manager.coreApprovals = new Set(['core1']); + manager.maintainerApprovals = new Set(['maintainer1']); + const pipeline1Core1Maintainer = (manager.coreApprovals.size >= 2) || (manager.coreApprovals.size >= 1 && manager.maintainerApprovals.size >= 1); + console.log('Pipeline 1 core + 1 maintainer approval:', pipeline1Core1Maintainer); + + if (!pipeline2Core || !pipeline1Core1Maintainer) { + process.exit(1); + } + " + + - name: Validate RFC approval logic + working-directory: .github/workflows/lib + run: | + node -e " + const ApprovalManager = require('./approval.js'); + + // Test RFC approval logic + const mockGithub = { request: () => Promise.resolve({ data: [] }), paginate: () => Promise.resolve([]) }; + const manager = new ApprovalManager(mockGithub, 'test', 'test', 1); + + // Test RFC quorum logic + manager.coreTeamMembers = ['core1', 'core2', 'core3', 'core4', 'core5']; + const quorum = Math.ceil(manager.coreTeamMembers.length / 2); + + manager.coreApprovals = new Set(['core1', 'core2', 'core3']); + const rfcApproved = manager.coreApprovals.size >= quorum; + console.log('RFC quorum approval (3/5):', rfcApproved, 'quorum needed:', quorum); + + manager.coreApprovals = new Set(['core1', 'core2']); + const rfcNotApproved = manager.coreApprovals.size >= quorum; + console.log('RFC insufficient approval (2/5):', rfcNotApproved); + + if (!rfcApproved || rfcNotApproved) { + process.exit(1); + } + " diff --git a/README.md b/README.md index 2ba9e35..2aedfbf 100644 --- a/README.md +++ b/README.md @@ -9,53 +9,105 @@ This repository is used to track all community proposals for nf-core. ## Proposer Documentation -To make a new proposal for a pipeline, please create a new issue in the repository following the template provided: +To make a new proposal, please create a new issue in the repository following the appropriate template: - [New pipeline](https://github.com/nf-core/proposals/issues/new?template=new_pipeline.yml) - -To make a new proposal for a special interest group, please create a new issue in the repository following the template provided: - +- [New RFC](https://github.com/nf-core/proposals/issues/new?template=new_rfc.yml) - [New special interest group](https://github.com/nf-core/proposals/issues/new?template=new_special_interest_group.yml) ## Curator Documentation ### Pipelines -The curator workflow is as follows: - -- [ ] Once a issue is made, a Github Actions workflow will: - - Add the 'proposed' label - - Create a status comment tracking approvals -- [ ] Facilitate discussion on the the Issue thread, following the guidance [here](https://nf-co.re/docs/checklists/community_governance/core_team#new-pipeline-proposals-and-onboarding). -- [ ] Acceptance requires a minimum of OKs from: - - Two members of the core team. - - One member of the core team and one member of the maintainers team. -- [ ] Core and maintainer members can use the following commands in comments: - - `/approve` - to approve the proposal - - `/reject` - to reject the proposal -- [ ] The automation will: - - Track approvals and rejections - - Update the status comment - - Update labels based on the current status -- [ ] If a proposal is accepted: - - Update the status to 'accepted' - - Close the issue as 'completed' -- [ ] If a proposal is turned down: - - Update the status to 'turned-down' - - Close the issue as 'not planned' -- [ ] If a proposal is not completed or abandoned after a year: - - Add the 'timed-out' label - - Close the issue as 'not planned' -- [ ] Complete the remaining new-pipeline onboarding tasks listed [here](https://nf-co.re/docs/checklists/community_governance/core_team#new-pipeline-proposals-and-onboarding) +**Automated workflow:** + +- [x] Issue creation triggers automation that: + - Adds the 'proposed' label + - Creates a status comment tracking approvals +- [x] Team members use `/approve` or `/reject` commands in comments +- [x] Automation updates status comment and labels based on approvals +- [x] Acceptance requires either: + - Two core team members + - One core team member + one maintainer + +**Manual steps required:** + +- [ ] Facilitate discussion following [guidance here](https://nf-co.re/docs/checklists/community_governance/core_team#new-pipeline-proposals-and-onboarding) +- [ ] If accepted: Close issue as 'completed' +- [ ] If rejected: Close issue as 'not planned' +- [ ] For timed-out proposals (after 1 year): Add 'timed-out' label and close as 'not planned' +- [ ] Complete remaining onboarding tasks listed [here](https://nf-co.re/docs/checklists/community_governance/core_team#new-pipeline-proposals-and-onboarding) + +### RFCs + +**Automated workflow:** + +- [x] Issue creation triggers automation that: + - Adds the 'proposed' label + - Creates a status comment tracking approvals +- [x] Team members use `/approve` or `/reject` commands in comments +- [x] Automation updates status comment and labels based on approvals +- [x] Acceptance requires core team quorum (majority of core team members) + +**Manual steps required:** + +- [ ] Facilitate discussion on the Issue thread +- [ ] If accepted: Close issue as 'completed' +- [ ] If rejected: Close issue as 'not planned' +- [ ] For timed-out proposals (after 1 year): Add 'timed-out' label and close as 'not planned' ### Special Interest Groups -The curator workflow is as follows: +**Manual workflow:** + +- [ ] Update 'Project' status to 'proposed' (right sidebar under 'new-special-interest-group-proposals') +- [ ] Facilitate discussion following [guidance here](https://nf-co.re/blog/2024/special_interest_groups) +- [ ] Acceptance requires two core team members +- [ ] If accepted: Update label and status to 'accepted', close as 'completed' +- [ ] If rejected: Update label and status to 'turned-down', close as 'not planned' +- [ ] For timed-out proposals (after 1 year): Update label and status to 'timed-out', close as 'not planned' + +## Developer Documentation + +### Approval Automation Testing + +The repository includes automated testing for the approval workflows to ensure reliability and correctness. + +#### Test Suite + +The approval automation is thoroughly tested with: + +- **Unit Tests** (`approval.test.js`) - Tests individual ApprovalManager class methods +- **Integration Tests** (`workflow-integration.test.js`) - End-to-end workflow scenario testing + +#### Automated Testing on PRs + +A GitHub Actions workflow (`.github/workflows/test-approval-automation.yml`) automatically runs on pull requests that modify: + +- `.github/workflows/lib/**` (approval automation code) +- `.github/workflows/pipeline_proposals.yml` (pipeline approval workflow) +- `.github/workflows/rfc_approval.yml` (RFC approval workflow) + +The workflow includes: + +1. **Test Execution** - Runs the full test suite +2. **Coverage Reporting** - Generates and uploads coverage reports +3. **PR Comments** - Posts coverage summary on pull requests +4. **Workflow Linting** - Validates GitHub Actions syntax +5. **Logic Validation** - Verifies approval logic correctness + +#### Running Tests Locally + +```bash +cd .github/workflows/lib + +# Install dependencies +npm install + +# Run tests +npm test + +# Run with coverage +npm test:coverage -- [ ] Once a issue is made, update the 'Project' status to 'proposed' (right hand side bar, under 'new--proposals') -- [ ] Facilitate discussion on the the Issue thread, following the guidance [here](https://nf-co.re/blog/2024/special_interest_groups). -- [ ] Acceptance requires a minimum of OKs from: - - Two members of the core team. -- [ ] If a proposal is accepted, update both the label AND status to 'accepted', and when closing select 'Close as completed' -- [ ] If a proposal is turned down, update both the label AND status to 'turned-down', and when closing select 'Close as not planned' -- [ ] If a proposal is not completed or abandoned after a year, update both the label AND status to 'timed-out', and when closing select 'Close as not planned' +``` From c89e8da3be7008a6f6a4bad6fd805070910e9f73 Mon Sep 17 00:00:00 2001 From: Ken Brewer Date: Sun, 20 Jul 2025 11:44:01 +0100 Subject: [PATCH 4/7] =?UTF-8?q?=E2=9C=A8=20feat:=20add=20approval=20automa?= =?UTF-8?q?tion=20for=20special=20interest=20groups?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/sig_approval.yml | 95 ++++++++++++++++++++++++++++++ 1 file changed, 95 insertions(+) create mode 100644 .github/workflows/sig_approval.yml diff --git a/.github/workflows/sig_approval.yml b/.github/workflows/sig_approval.yml new file mode 100644 index 0000000..b1eebf8 --- /dev/null +++ b/.github/workflows/sig_approval.yml @@ -0,0 +1,95 @@ +name: SIG approval automation + +on: + issues: + types: [opened, closed, labeled, unlabeled] + issue_comment: + types: [created, edited] + +jobs: + sig_approval: + # Only run for SIG proposal issues + if: startsWith(github.event.issue.title, 'New special interest group') + runs-on: ubuntu-latest + + steps: + - name: Handle SIG approval logic + uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea #v7.0.1 + with: + github-token: ${{ secrets.nf_core_bot_auth_token }} + script: | + const ApprovalManager = require('./.github/workflows/lib/approval.js'); + + const issueNumber = context.issue.number; + const org = context.repo.owner; + const repo = context.repo.repo; + + // Initialize approval manager + const approvalManager = await new ApprovalManager(github, org, repo, issueNumber).initialize(); + + // Ignore comments on closed issues + if (context.eventName === 'issue_comment' && context.payload.issue.state === 'closed') { + console.log('Comment event on closed issue, ignoring.'); + return; + } + + const requiredCoreApprovals = 2; + console.log(`Required core team approvals: ${requiredCoreApprovals}`); + + function generateStatusBody(status) { + const approvers = [...approvalManager.coreApprovals]; + const rejecters = [...approvalManager.coreRejections]; + const awaiting = approvalManager.awaitingCore; + + let body = `## Approval status: ${status}\n\nSIG proposal requires approvals from ${requiredCoreApprovals} core team members.\n\n`; + body += `Current approvals: ${approvalManager.coreApprovals.size}/${requiredCoreApprovals}\n\n`; + + if (approvers.length > 0 || rejecters.length > 0 || awaiting.length > 0) { + body += `|Review Status|Core Team members|\n|--|--|\n`; + if (approvers.length > 0) { + body += `| ✅ Approved | ${approvalManager.formatUserList(approvers)} |\n`; + } + if (rejecters.length > 0) { + body += `| ❌ Rejected | ${approvalManager.formatUserList(rejecters)} |\n`; + } + if (awaiting.length > 0) { + body += `| 🕐 Pending | ${approvalManager.formatUserList(awaiting)} |\n`; + } + } + return body; + } + + // Handle label changes + if (context.eventName === 'issues' && (context.payload.action === 'labeled' || context.payload.action === 'unlabeled')) { + const label = context.payload.label.name; + if (label === 'timed-out') { + console.log('Timed-out label detected, updating status'); + const statusBody = generateStatusBody('⏰ Timed Out'); + await approvalManager.updateStatusComment(statusBody); + return; + } + } + + // Handle new issue creation + if (context.eventName === 'issues' && context.payload.action === 'opened') { + const body = generateStatusBody('🕐 Pending'); + console.log('Creating initial comment for review status'); + await approvalManager.updateStatusComment(body); + await approvalManager.updateIssueStatus('🕐 Pending'); + return; + } + + // Determine status + let status = '🕐 Pending'; + + if (context.eventName === 'issues' && context.payload.action === 'closed' && context.payload.issue.state_reason === 'not_planned' && (approvalManager.coreRejections.size > 0)) { + status = '❌ Rejected'; + } else if (approvalManager.coreApprovals.size >= requiredCoreApprovals) { + status = '✅ Approved'; + } + + const statusBody = generateStatusBody(status); + console.log('New status body to post:\n', statusBody); + + await approvalManager.updateStatusComment(statusBody); + await approvalManager.updateIssueStatus(status); From 12645cffe60666da497b9d07529c517abdb41029 Mon Sep 17 00:00:00 2001 From: Ken Brewer Date: Sun, 20 Jul 2025 11:53:43 +0100 Subject: [PATCH 5/7] =?UTF-8?q?=F0=9F=92=9A=20ci:=20switch=20actionlint=20?= =?UTF-8?q?to=20precommit=20hook?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../workflows/test-approval-automation.yml | 23 +------------------ .pre-commit-config.yaml | 5 ++++ 2 files changed, 6 insertions(+), 22 deletions(-) diff --git a/.github/workflows/test-approval-automation.yml b/.github/workflows/test-approval-automation.yml index c1777b5..4e0388f 100644 --- a/.github/workflows/test-approval-automation.yml +++ b/.github/workflows/test-approval-automation.yml @@ -45,7 +45,7 @@ jobs: run: npm run test:coverage - name: Upload coverage reports - uses: codecov/codecov-action@v3 + uses: codecov/codecov-action@v4 if: success() with: file: .github/workflows/lib/coverage/lcov.info @@ -109,27 +109,6 @@ jobs: } } - lint-workflows: - name: Lint GitHub Actions Workflows - runs-on: ubuntu-latest - - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: "18" - - - name: Install actionlint - run: | - bash <(curl https://raw.githubusercontent.com/rhymond/actionlint/main/scripts/download-actionlint.bash) - sudo mv ./actionlint /usr/local/bin/ - - - name: Lint workflow files - run: actionlint .github/workflows/*.yml - validate-approval-logic: name: Validate Approval Logic runs-on: ubuntu-latest diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 326d4d8..94c07ca 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -11,3 +11,8 @@ repos: hooks: - id: editorconfig-checker alias: ec + + - repo: https://github.com/rhysd/actionlint + rev: "v1.7.7" + hooks: + - id: actionlint From 441e2089f7eb915b67d5aba2abdfbdf456946a72 Mon Sep 17 00:00:00 2001 From: Ken Brewer Date: Sun, 20 Jul 2025 17:07:32 +0100 Subject: [PATCH 6/7] =?UTF-8?q?=F0=9F=92=9A=20ci:=20remove=20package-lock.?= =?UTF-8?q?json=20from=20.gitignore=20and=20commit=20lockfile?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Removes package-lock.json from .gitignore to fix GitHub Action cache dependency path - Commits package-lock.json to ensure consistent dependency versions - Resolves 'Some specified paths were not resolved, unable to cache dependencies' error --- .github/workflows/lib/.gitignore | 1 - .github/workflows/lib/package-lock.json | 3640 +++++++++++++++++++++++ 2 files changed, 3640 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/lib/package-lock.json diff --git a/.github/workflows/lib/.gitignore b/.github/workflows/lib/.gitignore index 821c051..5605da4 100644 --- a/.github/workflows/lib/.gitignore +++ b/.github/workflows/lib/.gitignore @@ -1,6 +1,5 @@ # Dependencies node_modules/ -package-lock.json # Coverage reports coverage/ diff --git a/.github/workflows/lib/package-lock.json b/.github/workflows/lib/package-lock.json new file mode 100644 index 0000000..4a71a31 --- /dev/null +++ b/.github/workflows/lib/package-lock.json @@ -0,0 +1,3640 @@ +{ + "name": "github-approval-automation", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "github-approval-automation", + "version": "1.0.0", + "devDependencies": { + "jest": "^29.0.0" + } + }, + "node_modules/@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.27.1", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.0.tgz", + "integrity": "sha512-60X7qkglvrap8mn1lh2ebxXdZYtUcpd7gsmy9kLaBJ4i/WdY8PqTSdxyA8qraikqKQK5C1KRBKXqznrVapyNaw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.0.tgz", + "integrity": "sha512-UlLAnTPrFdNGoFtbSXwcGFQBtQZJCNjaN6hQNP3UPvuNXT1i82N26KL3dZeIpNalWywr9IuQuncaAfUaS1g6sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.0", + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-module-transforms": "^7.27.3", + "@babel/helpers": "^7.27.6", + "@babel/parser": "^7.28.0", + "@babel/template": "^7.27.2", + "@babel/traverse": "^7.28.0", + "@babel/types": "^7.28.0", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.0.tgz", + "integrity": "sha512-lJjzvrbEeWrhB4P3QBsH7tey117PjLZnDbLiQEKjQ/fNJTjuq4HSqgFA+UNSwZT8D7dxxbnuSBMsa1lrWzKlQg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.0", + "@babel/types": "^7.28.0", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", + "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.27.2", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", + "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.27.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.27.3.tgz", + "integrity": "sha512-dSOvYwvyLsWBeIRyOeHXp5vPj5l1I011r52FM1+r1jCERv+aFXYk4whgQccYEGYxK2H3ZAIA8nuPkQ0HaUo3qg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1", + "@babel/traverse": "^7.27.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz", + "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", + "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.27.6", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.27.6.tgz", + "integrity": "sha512-muE8Tt8M22638HU31A3CgfSUciwz1fhATfoVai05aPXGor//CdWDCbnlY1yvBPo07njuVOCNGCSp/GTt12lIug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.27.2", + "@babel/types": "^7.27.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.0.tgz", + "integrity": "sha512-jVZGvOxOuNSsuQuLRTh13nU0AogFlw32w/MT+LV6D3sP5WdbW61E77RnkbaO2dUvmPAYrBDJXGn5gGS6tH4j8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-syntax-async-generators": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", + "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-bigint": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", + "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-properties": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", + "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.12.13" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-static-block": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", + "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-attributes": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.27.1.tgz", + "integrity": "sha512-oFT0FrKHgF53f4vOsZGi2Hh3I35PfSmVs4IBFLFj4dnafP+hIWDLg3VyKmUHfLoLHlyxY4C7DGtmHuJgn+IGww==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-meta": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", + "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", + "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.27.1.tgz", + "integrity": "sha512-y8YTNIeKoyhGd9O0Jiyzyyqk8gdjnumGTQPsz0xOZOQ2RmkVJeZ1vmmfIvFEKqucBG6axJGBZDE/7iI5suUI/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", + "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", + "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-numeric-separator": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", + "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", + "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", + "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-private-property-in-object": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", + "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-top-level-await": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", + "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-typescript": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.27.1.tgz", + "integrity": "sha512-xfYCBMxveHrRMnAWl1ZlPXOZjzkN82THFvLhQhFXFt81Z5HnN+EtUkZhv/zcKpmT3fzmWZB0ywiBrbC3vogbwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", + "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/parser": "^7.27.2", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.0.tgz", + "integrity": "sha512-mGe7UK5wWyh0bKRfupsUchrQGqvDbZDbKJw+kcRGSmdHVYrv+ltd0pnpDTVpiTqnaBru9iEvA8pz8W46v0Amwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.0", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.28.0", + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.0", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.28.1", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.1.tgz", + "integrity": "sha512-x0LvFTekgSX+83TI28Y9wYPUfzrnl2aT5+5QLnO6v7mSJYtEEevuDRN0F0uSHRk1G1IWZC43o00Y0xDDrpBGPQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bcoe/v8-coverage": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", + "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@istanbuljs/load-nyc-config": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", + "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "get-package-type": "^0.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/console": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-29.7.0.tgz", + "integrity": "sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/core": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/core/-/core-29.7.0.tgz", + "integrity": "sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/reporters": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-changed-files": "^29.7.0", + "jest-config": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-resolve-dependencies": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "jest-watcher": "^29.7.0", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/environment": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-29.7.0.tgz", + "integrity": "sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "expect": "^29.7.0", + "jest-snapshot": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/expect-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.7.0.tgz", + "integrity": "sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-get-type": "^29.6.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/fake-timers": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-29.7.0.tgz", + "integrity": "sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@sinonjs/fake-timers": "^10.0.2", + "@types/node": "*", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/globals": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-29.7.0.tgz", + "integrity": "sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/types": "^29.6.3", + "jest-mock": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/reporters": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-29.7.0.tgz", + "integrity": "sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@bcoe/v8-coverage": "^0.2.3", + "@jest/console": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "@types/node": "*", + "chalk": "^4.0.0", + "collect-v8-coverage": "^1.0.0", + "exit": "^0.1.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-instrument": "^6.0.0", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^4.0.0", + "istanbul-reports": "^3.1.3", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "slash": "^3.0.0", + "string-length": "^4.0.1", + "strip-ansi": "^6.0.0", + "v8-to-istanbul": "^9.0.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/schemas": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sinclair/typebox": "^0.27.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/source-map": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-29.6.3.tgz", + "integrity": "sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.18", + "callsites": "^3.0.0", + "graceful-fs": "^4.2.9" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/test-result": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-29.7.0.tgz", + "integrity": "sha512-Fdx+tv6x1zlkJPcWXmMDAG2HBnaR9XPSd5aDWQVsfrZmLVT3lU1cwyxLgRmXR9yrq4NBoEm9BMsfgFzTQAbJYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "collect-v8-coverage": "^1.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/test-sequencer": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-29.7.0.tgz", + "integrity": "sha512-GQwJ5WZVrKnOJuiYiAF52UNUJXgTZx1NHjFSEB0qEMmSZKAkdMoIzw/Cj6x6NF4AvV23AUqDpFzQkN/eYCYTxw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/test-result": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/transform": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.7.0.tgz", + "integrity": "sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "babel-plugin-istanbul": "^6.1.1", + "chalk": "^4.0.0", + "convert-source-map": "^2.0.0", + "fast-json-stable-stringify": "^2.1.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "micromatch": "^4.0.4", + "pirates": "^4.0.4", + "slash": "^3.0.0", + "write-file-atomic": "^4.0.2" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.12", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.12.tgz", + "integrity": "sha512-OuLGC46TjB5BbN1dH8JULVVZY4WTdkF7tV9Ys6wLL1rubZnCMstOhNHueU5bLCrnRuDhKPDM4g6sw4Bel5Gzqg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.4.tgz", + "integrity": "sha512-VT2+G1VQs/9oz078bLrYbecdZKs912zQlkelYpuf+SXF+QvZDYJlbx/LSx+meSAwdDFnF8FVXW92AVjjkVmgFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.29", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.29.tgz", + "integrity": "sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@sinclair/typebox": { + "version": "0.27.8", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", + "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@sinonjs/commons": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", + "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/fake-timers": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz", + "integrity": "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@sinonjs/commons": "^3.0.0" + } + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.7.tgz", + "integrity": "sha512-dkO5fhS7+/oos4ciWxyEyjWe48zmG6wbCheo/G2ZnHx4fs3EU6YC6UM8rk56gAjNJ9P3MTH2jo5jb92/K6wbng==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.20.7" + } + }, + "node_modules/@types/graceful-fs": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz", + "integrity": "sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/istanbul-lib-coverage": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", + "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/istanbul-lib-report": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", + "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-coverage": "*" + } + }, + "node_modules/@types/istanbul-reports": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", + "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-report": "*" + } + }, + "node_modules/@types/node": { + "version": "24.0.15", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.0.15.tgz", + "integrity": "sha512-oaeTSbCef7U/z7rDeJA138xpG3NuKc64/rZ2qmUFkFJmnMsAPaluIifqyWd8hSSMxyP9oie3dLAqYPblag9KgA==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.8.0" + } + }, + "node_modules/@types/stack-utils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", + "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/yargs": { + "version": "17.0.33", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.33.tgz", + "integrity": "sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@types/yargs-parser": { + "version": "21.0.3", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", + "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/babel-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", + "integrity": "sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/transform": "^29.7.0", + "@types/babel__core": "^7.1.14", + "babel-plugin-istanbul": "^6.1.1", + "babel-preset-jest": "^29.6.3", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.8.0" + } + }, + "node_modules/babel-plugin-istanbul": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz", + "integrity": "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-instrument": "^5.0.4", + "test-exclude": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-istanbul/node_modules/istanbul-lib-instrument": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz", + "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/core": "^7.12.3", + "@babel/parser": "^7.14.7", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-jest-hoist": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.6.3.tgz", + "integrity": "sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.3.3", + "@babel/types": "^7.3.3", + "@types/babel__core": "^7.1.14", + "@types/babel__traverse": "^7.0.6" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/babel-preset-current-node-syntax": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.1.0.tgz", + "integrity": "sha512-ldYss8SbBlWva1bs28q78Ju5Zq1F+8BrqBZZ0VFhLBvhh6lCpC2o3gDJi/5DRLs9FgYZCnmPYIVFU4lRXCkyUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-bigint": "^7.8.3", + "@babel/plugin-syntax-class-properties": "^7.12.13", + "@babel/plugin-syntax-class-static-block": "^7.14.5", + "@babel/plugin-syntax-import-attributes": "^7.24.7", + "@babel/plugin-syntax-import-meta": "^7.10.4", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.10.4", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5", + "@babel/plugin-syntax-top-level-await": "^7.14.5" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/babel-preset-jest": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-29.6.3.tgz", + "integrity": "sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==", + "dev": true, + "license": "MIT", + "dependencies": { + "babel-plugin-jest-hoist": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.25.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.25.1.tgz", + "integrity": "sha512-KGj0KoOMXLpSNkkEI6Z6mShmQy0bc1I+T7K9N81k4WWMrfz+6fQ6es80B/YLAeRoKvjYE1YSHHOW1qe9xIVzHw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "caniuse-lite": "^1.0.30001726", + "electron-to-chromium": "^1.5.173", + "node-releases": "^2.0.19", + "update-browserslist-db": "^1.1.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/bser": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", + "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "node-int64": "^0.4.0" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001727", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001727.tgz", + "integrity": "sha512-pB68nIHmbN6L/4C6MH1DokyR3bYqFwjaSs/sWDHGj4CTcFtQUQMuJftVwWkXq7mNWOybD3KhUv3oWHoGxgP14Q==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/char-regex": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", + "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/ci-info": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", + "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cjs-module-lexer": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.4.3.tgz", + "integrity": "sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">= 1.0.0", + "node": ">= 0.12.0" + } + }, + "node_modules/collect-v8-coverage": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.2.tgz", + "integrity": "sha512-lHl4d5/ONEbLlJvaJNtsF/Lz+WvB07u2ycqTYbdrq7UypDXailES4valYb2eWiJFxZlVmpGekfqoxQhzyFdT4Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/create-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/create-jest/-/create-jest-29.7.0.tgz", + "integrity": "sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "prompts": "^2.0.1" + }, + "bin": { + "create-jest": "bin/create-jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/dedent": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.6.0.tgz", + "integrity": "sha512-F1Z+5UCFpmQUzJa11agbyPVMbpgT/qA3/SKyJ1jyBgm7dUcUEa8v9JwDkerSQXfakBwFljIxhOJqGkjUwZ9FSA==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "babel-plugin-macros": "^3.1.0" + }, + "peerDependenciesMeta": { + "babel-plugin-macros": { + "optional": true + } + } + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/detect-newline": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", + "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/diff-sequences": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", + "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.187", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.187.tgz", + "integrity": "sha512-cl5Jc9I0KGUoOoSbxvTywTa40uspGJt/BDBoDLoxJRSBpWh4FFXBsjNRHfQrONsV/OoEjDfHUmZQa2d6Ze4YgA==", + "dev": true, + "license": "ISC" + }, + "node_modules/emittery": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz", + "integrity": "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sindresorhus/emittery?sponsor=1" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/exit": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", + "integrity": "sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/expect-utils": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fb-watchman": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", + "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "bser": "2.1.1" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true, + "license": "ISC" + }, + "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/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-package-type": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true, + "license": "MIT" + }, + "node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10.17.0" + } + }, + "node_modules/import-local": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", + "integrity": "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + }, + "bin": { + "import-local-fixture": "fixtures/cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-generator-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", + "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-instrument": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz", + "integrity": "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/core": "^7.23.9", + "@babel/parser": "^7.23.9", + "@istanbuljs/schema": "^0.1.3", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-instrument/node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-source-maps": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", + "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-reports": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.7.tgz", + "integrity": "sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz", + "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/core": "^29.7.0", + "@jest/types": "^29.6.3", + "import-local": "^3.0.2", + "jest-cli": "^29.7.0" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-changed-files": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-29.7.0.tgz", + "integrity": "sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w==", + "dev": true, + "license": "MIT", + "dependencies": { + "execa": "^5.0.0", + "jest-util": "^29.7.0", + "p-limit": "^3.1.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-circus": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-29.7.0.tgz", + "integrity": "sha512-3E1nCMgipcTkCocFwM90XXQab9bS+GMsjdpmPrlelaxwD93Ad8iVEjX/vvHPdLPnFf+L40u+5+iutRdA1N9myw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "co": "^4.6.0", + "dedent": "^1.0.0", + "is-generator-fn": "^2.0.0", + "jest-each": "^29.7.0", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "p-limit": "^3.1.0", + "pretty-format": "^29.7.0", + "pure-rand": "^6.0.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-cli": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-29.7.0.tgz", + "integrity": "sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/core": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "create-jest": "^29.7.0", + "exit": "^0.1.2", + "import-local": "^3.0.2", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "yargs": "^17.3.1" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-config": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-29.7.0.tgz", + "integrity": "sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/test-sequencer": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-jest": "^29.7.0", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "deepmerge": "^4.2.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-circus": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "micromatch": "^4.0.4", + "parse-json": "^5.2.0", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@types/node": "*", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/jest-diff": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", + "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "diff-sequences": "^29.6.3", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-docblock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-29.7.0.tgz", + "integrity": "sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "detect-newline": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-each": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-29.7.0.tgz", + "integrity": "sha512-gns+Er14+ZrEoC5fhOfYCY1LOHHr0TI+rQUHZS8Ttw2l7gl+80eHc/gFf2Ktkw0+SIACDTeWvpFcv3B04VembQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "jest-util": "^29.7.0", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-environment-node": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-29.7.0.tgz", + "integrity": "sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-get-type": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", + "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-haste-map": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.7.0.tgz", + "integrity": "sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/graceful-fs": "^4.1.3", + "@types/node": "*", + "anymatch": "^3.0.3", + "fb-watchman": "^2.0.0", + "graceful-fs": "^4.2.9", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "micromatch": "^4.0.4", + "walker": "^1.0.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "optionalDependencies": { + "fsevents": "^2.3.2" + } + }, + "node_modules/jest-leak-detector": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-29.7.0.tgz", + "integrity": "sha512-kYA8IJcSYtST2BY9I+SMC32nDpBT3J2NvWJx8+JCuCdl/CR1I4EKUJROiP8XtCcxqgTTBGJNdbB1A8XRKbTetw==", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-matcher-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz", + "integrity": "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-message-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz", + "integrity": "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.12.13", + "@jest/types": "^29.6.3", + "@types/stack-utils": "^2.0.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-mock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-29.7.0.tgz", + "integrity": "sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-pnp-resolver": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", + "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "peerDependencies": { + "jest-resolve": "*" + }, + "peerDependenciesMeta": { + "jest-resolve": { + "optional": true + } + } + }, + "node_modules/jest-regex-util": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.6.3.tgz", + "integrity": "sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-resolve": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-29.7.0.tgz", + "integrity": "sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-pnp-resolver": "^1.2.2", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "resolve": "^1.20.0", + "resolve.exports": "^2.0.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-resolve-dependencies": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-29.7.0.tgz", + "integrity": "sha512-un0zD/6qxJ+S0et7WxeI3H5XSe9lTBBR7bOHCHXkKR6luG5mwDDlIzVQ0V5cZCuoTgEdcdwzTghYkTWfubi+nA==", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-regex-util": "^29.6.3", + "jest-snapshot": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runner": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-29.7.0.tgz", + "integrity": "sha512-fsc4N6cPCAahybGBfTRcq5wFR6fpLznMg47sY5aDpsoejOcVYFb07AHuSnR0liMcPTgBsA3ZJL6kFOjPdoNipQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/environment": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "graceful-fs": "^4.2.9", + "jest-docblock": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-leak-detector": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-resolve": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-util": "^29.7.0", + "jest-watcher": "^29.7.0", + "jest-worker": "^29.7.0", + "p-limit": "^3.1.0", + "source-map-support": "0.5.13" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runtime": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-29.7.0.tgz", + "integrity": "sha512-gUnLjgwdGqW7B4LvOIkbKs9WGbn+QLqRQQ9juC6HndeDiezIwhDP+mhMwHWCEcfQ5RUXa6OPnFF8BJh5xegwwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/globals": "^29.7.0", + "@jest/source-map": "^29.6.3", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "cjs-module-lexer": "^1.0.0", + "collect-v8-coverage": "^1.0.0", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "slash": "^3.0.0", + "strip-bom": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-snapshot": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-29.7.0.tgz", + "integrity": "sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.11.6", + "@babel/generator": "^7.7.2", + "@babel/plugin-syntax-jsx": "^7.7.2", + "@babel/plugin-syntax-typescript": "^7.7.2", + "@babel/types": "^7.3.3", + "@jest/expect-utils": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0", + "chalk": "^4.0.0", + "expect": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "natural-compare": "^1.4.0", + "pretty-format": "^29.7.0", + "semver": "^7.5.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-snapshot/node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jest-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", + "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-validate": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-29.7.0.tgz", + "integrity": "sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "camelcase": "^6.2.0", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "leven": "^3.1.0", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-validate/node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/jest-watcher": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-29.7.0.tgz", + "integrity": "sha512-49Fg7WXkU3Vl2h6LbLtMQ/HyB6rXSIX7SqvBLQmssRBGN9I0PNvPmAmCWSOY6SOvrjhI/F7/bGAv9RtnsPA03g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "jest-util": "^29.7.0", + "string-length": "^4.0.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-worker": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.7.0.tgz", + "integrity": "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "jest-util": "^29.7.0", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-worker/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/kleur": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/leven": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, + "license": "MIT" + }, + "node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-dir/node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/makeerror": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", + "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tmpl": "1.0.5" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "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/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/node-int64": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", + "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.19", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", + "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==", + "dev": true, + "license": "MIT" + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-locate/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "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": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/prompts": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", + "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "kleur": "^3.0.3", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/pure-rand": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz", + "integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT" + }, + "node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve": { + "version": "1.22.10", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", + "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", + "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve.exports": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.3.tgz", + "integrity": "sha512-OcXjMsGdhL4XnbShKpAcSqPMzQoYkYyhbEaeSko47MjRP9NfEQMhZkXL1DoFlt9LWQn4YttrdnV6X2OiyzBi+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "dev": true, + "license": "MIT" + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.13", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", + "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/stack-utils": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", + "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/string-length": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", + "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "char-regex": "^1.0.2", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-bom": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", + "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "dev": true, + "license": "ISC", + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tmpl": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", + "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/undici-types": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.8.0.tgz", + "integrity": "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw==", + "dev": true, + "license": "MIT" + }, + "node_modules/update-browserslist-db": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", + "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/v8-to-istanbul": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", + "integrity": "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==", + "dev": true, + "license": "ISC", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.12", + "@types/istanbul-lib-coverage": "^2.0.1", + "convert-source-map": "^2.0.0" + }, + "engines": { + "node": ">=10.12.0" + } + }, + "node_modules/walker": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", + "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "makeerror": "1.0.12" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/write-file-atomic": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.2.tgz", + "integrity": "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==", + "dev": true, + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4", + "signal-exit": "^3.0.7" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "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" + } + } + } +} From 78fe8b9219336fad9fb2476b596f4ec94ca59a3a Mon Sep 17 00:00:00 2001 From: Ken Brewer Date: Wed, 20 Aug 2025 07:37:17 -0700 Subject: [PATCH 7/7] =?UTF-8?q?typo=20=E2=9C=8F=EF=B8=8F:=20=20fix=20refer?= =?UTF-8?q?ence=20to=20core=20team?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Maxime U Garcia --- .github/workflows/rfc_approval.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/rfc_approval.yml b/.github/workflows/rfc_approval.yml index cd84af9..a6c05d4 100644 --- a/.github/workflows/rfc_approval.yml +++ b/.github/workflows/rfc_approval.yml @@ -40,7 +40,7 @@ jobs: const approvers = [...approvalManager.coreApprovals]; const rejecters = [...approvalManager.coreRejections]; const awaiting = approvalManager.awaitingCore; - let body = `## Approval status: ${status}\n\nRFC has approvals from ${approvalManager.coreApprovals.size}/${quorum} required @core-team quorum.\n\n`; + let body = `## Approval status: ${status}\n\nRFC has approvals from ${approvalManager.coreApprovals.size}/${quorum} required @nf-core/core quorum.\n\n`; if (approvers.length > 0 || rejecters.length > 0 || awaiting.length > 0) { body += `|Review Status|Core Team members|\n|--|--|\n`; if (approvers.length > 0) {