diff --git a/.biomeignore b/.biomeignore new file mode 100644 index 0000000..849ddff --- /dev/null +++ b/.biomeignore @@ -0,0 +1 @@ +dist/ diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..d734d4b --- /dev/null +++ b/.editorconfig @@ -0,0 +1,22 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true +indent_style = space +indent_size = 4 + +[*.{yml,yaml}] +indent_size = 2 + +[Makefile] +indent_style = tab + +[*.md] +trim_trailing_whitespace = false + +[*.lua] +indent_style = space +indent_size = 4 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..1867c1f --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,59 @@ +name: CI + +on: + # push: + # branches: [ main, develop ] + pull_request: + branches: [ main ] + +permissions: + contents: read + pull-requests: write + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: false + +jobs: + test: + runs-on: ubuntu-latest + + strategy: + matrix: + node-version: [22.x] + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node-version }} + cache: 'npm' + + - name: Install dependencies + run: npm ci + + - name: Run linting + run: npm run lint + + - name: Check formatting + run: npm run format:check + + - name: Type check + run: npm run type-check + + - name: Run tests + run: npm test + + - name: Build project + run: npm run build + + - name: Upload coverage reports + uses: codecov/codecov-action@v3 + with: + file: ./coverage/lcov.info + flags: unittests + name: codecov-umbrella + fail_ci_if_error: false \ No newline at end of file diff --git a/.github/workflows/event-log-analysis.yml b/.github/workflows/event-log-analysis.yml new file mode 100644 index 0000000..12436ba --- /dev/null +++ b/.github/workflows/event-log-analysis.yml @@ -0,0 +1,293 @@ +name: Event Log Analysis + +on: + workflow_dispatch: + inputs: + analysis_type: + description: 'Type of analysis to run' + required: true + default: 'full' + type: choice + options: + - full + - validation + - statistics + - historical + schedule: + # Run daily at 2 AM UTC + - cron: '0 2 * * *' + push: + paths: + - 'events/game-events.jsonl' + +jobs: + analyze-event-log: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + token: ${{ secrets.GITHUB_TOKEN }} + fetch-depth: 0 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '22' + cache: 'npm' + + - name: Install dependencies + run: npm ci + + - name: Build project + run: npm run build + + - name: Validate event log + run: | + echo "šŸ” Validating event log integrity..." + node -e " + const { EventLogService } = require('./dist/services/EventLogService'); + const config = { + requiredVotes: 3, + requiredMajority: 0.5, + pointsForSuccessfulProposal: 3, + pointsForVotingWithMajority: 2, + pointsForParticipation: 1, + autoMergeEnabled: true, + branchProtectionEnabled: true, + }; + + const service = new EventLogService('events/game-events.jsonl', config); + service.validateEventLog().then(result => { + console.log('šŸ“Š Event Log Validation Results:'); + console.log('- Valid:', result.isValid ? 'āœ…' : 'āŒ'); + console.log('- Total Events:', result.eventCount); + + if (result.errors.length > 0) { + console.log('āŒ Errors found:'); + result.errors.forEach(error => console.log(' -', error)); + process.exit(1); + } + + console.log('āœ… Event log validation passed'); + }).catch(error => { + console.error('āŒ Validation failed:', error); + process.exit(1); + }); + " + + - name: Generate event log statistics + if: github.event.inputs.analysis_type == 'full' || github.event.inputs.analysis_type == 'statistics' || github.event_name == 'schedule' + run: | + echo "šŸ“ˆ Generating event log statistics..." + node -e " + const { EventLogService } = require('./dist/services/EventLogService'); + const { StateProcessor } = require('./dist/services/StateProcessor'); + const fs = require('fs'); + + const config = { + requiredVotes: 3, + requiredMajority: 0.5, + pointsForSuccessfulProposal: 3, + pointsForVotingWithMajority: 2, + pointsForParticipation: 1, + autoMergeEnabled: true, + branchProtectionEnabled: true, + }; + + const eventLogService = new EventLogService('events/game-events.jsonl', config); + const stateProcessor = new StateProcessor(eventLogService); + + Promise.all([ + eventLogService.getEventLogStats(), + stateProcessor.computeCurrentState(), + stateProcessor.getProposalStats(), + stateProcessor.getPlayerLeaderboard() + ]).then(([logStats, gameState, proposalStats, leaderboard]) => { + console.log('šŸ“Š Event Log Statistics:'); + console.log('- Total Events:', logStats.totalEvents); + console.log('- Event Types:', JSON.stringify(logStats.eventsByType, null, 2)); + console.log('- Unique Players:', logStats.uniquePlayers); + console.log('- Unique Proposals:', logStats.uniqueProposals); + console.log('- First Event:', logStats.firstEvent); + console.log('- Last Event:', logStats.lastEvent); + + console.log('\\nšŸŽ® Game State:'); + console.log('- Current Turn:', gameState.current_turn); + console.log('- Active Players:', Object.keys(gameState.players).length); + console.log('- Active Rules:', Object.values(gameState.rules).filter(r => r.status === 'active').length); + + console.log('\\nšŸ“‹ Proposal Statistics:'); + console.log('- Total:', proposalStats.total); + console.log('- Open:', proposalStats.open); + console.log('- Passed:', proposalStats.passed); + console.log('- Failed:', proposalStats.failed); + console.log('- Closed:', proposalStats.closed); + + console.log('\\nšŸ† Top Players:'); + leaderboard.slice(0, 5).forEach((player, i) => { + console.log(\`\${i + 1}. \${player.username}: \${player.points} points\`); + }); + + // Save detailed statistics + const detailedStats = { + timestamp: new Date().toISOString(), + eventLog: logStats, + gameState: { + currentTurn: gameState.current_turn, + playerCount: Object.keys(gameState.players).length, + ruleCount: Object.values(gameState.rules).filter(r => r.status === 'active').length, + eventCount: gameState.event_count + }, + proposals: proposalStats, + topPlayers: leaderboard.slice(0, 10) + }; + + if (!fs.existsSync('events/processed')) { + fs.mkdirSync('events/processed', { recursive: true }); + } + + fs.writeFileSync('events/processed/statistics.json', JSON.stringify(detailedStats, null, 2)); + console.log('\\nšŸ’¾ Detailed statistics saved to events/processed/statistics.json'); + }).catch(console.error); + " + + - name: Historical analysis + if: github.event.inputs.analysis_type == 'full' || github.event.inputs.analysis_type == 'historical' + run: | + echo "šŸ“… Performing historical analysis..." + node -e " + const { EventLogService } = require('./dist/services/EventLogService'); + const { StateProcessor } = require('./dist/services/StateProcessor'); + const fs = require('fs'); + + const config = { + requiredVotes: 3, + requiredMajority: 0.5, + pointsForSuccessfulProposal: 3, + pointsForVotingWithMajority: 2, + pointsForParticipation: 1, + autoMergeEnabled: true, + branchProtectionEnabled: true, + }; + + const eventLogService = new EventLogService('events/game-events.jsonl', config); + const stateProcessor = new StateProcessor(eventLogService); + + eventLogService.readAllEvents().then(events => { + if (events.length === 0) { + console.log('No events found for historical analysis'); + return; + } + + console.log('šŸ“… Historical Analysis:'); + + // Analyze event frequency over time + const eventsByDay = {}; + const playerActivity = {}; + + events.forEach(entry => { + const event = entry.event; + const date = event.timestamp.split('T')[0]; + + eventsByDay[date] = (eventsByDay[date] || 0) + 1; + + // Track player activity + if ('proposer' in event) playerActivity[event.proposer] = (playerActivity[event.proposer] || 0) + 1; + if ('voter' in event) playerActivity[event.voter] = (playerActivity[event.voter] || 0) + 1; + if ('player' in event) playerActivity[event.player] = (playerActivity[event.player] || 0) + 1; + if ('username' in event) playerActivity[event.username] = (playerActivity[event.username] || 0) + 1; + }); + + console.log('\\nšŸ“Š Activity by Day:'); + Object.entries(eventsByDay) + .sort(([a], [b]) => a.localeCompare(b)) + .slice(-7) // Last 7 days + .forEach(([date, count]) => { + console.log(\` \${date}: \${count} events\`); + }); + + console.log('\\nšŸ‘„ Most Active Players:'); + Object.entries(playerActivity) + .sort(([,a], [,b]) => b - a) + .slice(0, 5) + .forEach(([player, activity]) => { + console.log(\` \${player}: \${activity} actions\`); + }); + + // Save historical data + const historicalData = { + timestamp: new Date().toISOString(), + totalEvents: events.length, + timeRange: { + start: events[0]?.event.timestamp, + end: events[events.length - 1]?.event.timestamp + }, + activityByDay: eventsByDay, + playerActivity: playerActivity + }; + + if (!fs.existsSync('events/processed/historical')) { + fs.mkdirSync('events/processed/historical', { recursive: true }); + } + + const filename = \`events/processed/historical/analysis-\${new Date().toISOString().split('T')[0]}.json\`; + fs.writeFileSync(filename, JSON.stringify(historicalData, null, 2)); + console.log(\`\\nšŸ’¾ Historical analysis saved to \${filename}\`); + }).catch(console.error); + " + + - name: Create backup + if: github.event.inputs.analysis_type == 'full' || github.event_name == 'schedule' + run: | + echo "šŸ’¾ Creating event log backup..." + node -e " + const { EventLogService } = require('./dist/services/EventLogService'); + const config = { + requiredVotes: 3, + requiredMajority: 0.5, + pointsForSuccessfulProposal: 3, + pointsForVotingWithMajority: 2, + pointsForParticipation: 1, + autoMergeEnabled: true, + branchProtectionEnabled: true, + }; + + const service = new EventLogService('events/game-events.jsonl', config); + service.createBackup().then(backupPath => { + console.log('āœ… Backup created:', backupPath); + }).catch(error => { + console.error('āŒ Backup failed:', error); + process.exit(1); + }); + " + + - name: Commit analysis results + if: github.event.inputs.analysis_type == 'full' || github.event.inputs.analysis_type == 'statistics' || github.event_name == 'schedule' + run: | + git config --local user.email "action@github.com" + git config --local user.name "GitHub Action" + + # Add analysis results + if [ -d "events/processed" ]; then + git add events/processed/ + fi + + # Commit if there are changes + if ! git diff --cached --quiet; then + git commit -m "Update event log analysis: $(date -u +%Y-%m-%d)" + git push + else + echo "No analysis changes to commit" + fi + + - name: Upload analysis artifacts + uses: actions/upload-artifact@v4 + if: always() + with: + name: event-log-analysis-${{ github.run_number }} + path: | + events/processed/ + events/*.backup-* + retention-days: 30 \ No newline at end of file diff --git a/.github/workflows/pr-checks.yml b/.github/workflows/pr-checks.yml new file mode 100644 index 0000000..03c0446 --- /dev/null +++ b/.github/workflows/pr-checks.yml @@ -0,0 +1,51 @@ +name: PR Status Check + +on: + pull_request: + types: [opened, synchronize, reopened, ready_for_review] + +jobs: + check-proposal-status: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '22' + cache: 'npm' + + - name: Install dependencies + run: npm ci + + - name: Build project + run: npm run build + + - name: Extract issue number + id: extract-issue + env: + PR_TITLE: ${{ github.event.pull_request.title }} + PR_BODY: ${{ github.event.pull_request.body }} + run: node scripts/extract-issue-number.js + + - name: Check proposal status + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITHUB_REPOSITORY: ${{ github.repository }} + GITHUB_REPOSITORY_OWNER: ${{ github.repository_owner }} + GITHUB_REPOSITORY_NAME: ${{ github.event.repository.name }} + run: node scripts/check-proposal-status.js ${{ steps.extract-issue.outputs.issue_number }} + + - name: Update PR status + if: always() + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + if [ "${{ job.status }}" == "success" ]; then + echo "āœ… Proposal requirements met - ready to merge" + else + echo "āŒ Proposal requirements not met - cannot merge" + fi \ No newline at end of file diff --git a/.github/workflows/process-votes.yml b/.github/workflows/process-votes.yml new file mode 100644 index 0000000..8aef06d --- /dev/null +++ b/.github/workflows/process-votes.yml @@ -0,0 +1,153 @@ +name: Process Nomic Votes + +on: + issue_comment: + types: [created] + issues: + types: [opened, closed] + +jobs: + process-vote: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + token: ${{ secrets.GITHUB_TOKEN }} + fetch-depth: 0 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '22' + cache: 'npm' + + - name: Install dependencies + run: npm ci + + - name: Build project + run: npm run build + + - name: Validate event log integrity + run: | + echo "Validating event log integrity..." + node -e " + const { EventLogService } = require('./dist/services/EventLogService'); + const config = { + requiredVotes: 3, + requiredMajority: 0.5, + pointsForSuccessfulProposal: 3, + pointsForVotingWithMajority: 2, + pointsForParticipation: 1, + autoMergeEnabled: true, + branchProtectionEnabled: true, + }; + const service = new EventLogService('events/game-events.jsonl', config); + service.validateEventLog().then(result => { + if (!result.isValid) { + console.error('Event log validation failed:', result.errors); + process.exit(1); + } + console.log('Event log is valid. Events:', result.eventCount); + }).catch(console.error); + " + + - name: Process vote + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITHUB_REPOSITORY: ${{ github.repository }} + GITHUB_EVENT_PATH: ${{ github.event_path }} + GITHUB_EVENT_NAME: ${{ github.event_name }} + run: npm start + + - name: Compute and save current state + run: | + echo "Computing current game state from events..." + node -e " + const { EventLogService } = require('./dist/services/EventLogService'); + const { StateProcessor } = require('./dist/services/StateProcessor'); + const fs = require('fs'); + + const config = { + requiredVotes: 3, + requiredMajority: 0.5, + pointsForSuccessfulProposal: 3, + pointsForVotingWithMajority: 2, + pointsForParticipation: 1, + autoMergeEnabled: true, + branchProtectionEnabled: true, + }; + + const eventLogService = new EventLogService('events/game-events.jsonl', config); + const stateProcessor = new StateProcessor(eventLogService); + + stateProcessor.computeCurrentState().then(state => { + // Ensure processed directory exists + if (!fs.existsSync('events/processed')) { + fs.mkdirSync('events/processed', { recursive: true }); + } + + // Save current state + fs.writeFileSync('events/processed/current-state.json', JSON.stringify(state, null, 2)); + console.log('Current state saved to events/processed/current-state.json'); + + // Print summary + console.log('Game State Summary:'); + console.log('- Players:', Object.keys(state.players).length); + console.log('- Proposals:', Object.keys(state.proposals).length); + console.log('- Active Rules:', Object.values(state.rules).filter(r => r.status === 'active').length); + console.log('- Current Turn:', state.current_turn); + console.log('- Events Processed:', state.event_count); + }).catch(console.error); + " + + - name: Check for auto-merge + if: github.event_name == 'issue_comment' + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITHUB_REPOSITORY: ${{ github.repository }} + GITHUB_REPOSITORY_OWNER: ${{ github.repository_owner }} + GITHUB_REPOSITORY_NAME: ${{ github.event.repository.name }} + run: node scripts/check-proposal-status.js ${{ github.event.issue.number }} + + - name: Auto-merge if ready + if: github.event_name == 'issue_comment' && steps.check-for-auto-merge.outcome == 'success' + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITHUB_REPOSITORY: ${{ github.repository }} + GITHUB_REPOSITORY_OWNER: ${{ github.repository_owner }} + GITHUB_REPOSITORY_NAME: ${{ github.event.repository.name }} + run: node scripts/auto-merge-proposal.js ${{ github.event.issue.number }} + + - name: Commit event log changes + if: github.event_name == 'issue_comment' || github.event_name == 'issues' + run: | + git config --local user.email "action@github.com" + git config --local user.name "GitHub Action" + + # Add event log and processed state if they have changes + if [ -f "events/game-events.jsonl" ]; then + git add events/game-events.jsonl + fi + + if [ -f "events/processed/current-state.json" ]; then + git add events/processed/current-state.json + fi + + # Add updated scoreboard and rules summary + if [ -f "SCOREBOARD.md" ]; then + git add SCOREBOARD.md + fi + + if [ -f "RULES_SUMMARY.md" ]; then + git add RULES_SUMMARY.md + fi + + # Commit if there are changes + if ! git diff --cached --quiet; then + git commit -m "Update game state: ${{ github.event_name }} on issue #${{ github.event.issue.number || 'unknown' }}" + git push + else + echo "No changes to commit" + fi \ No newline at end of file diff --git a/.github/workflows/update-rules-summary.yml b/.github/workflows/update-rules-summary.yml new file mode 100644 index 0000000..10a14f7 --- /dev/null +++ b/.github/workflows/update-rules-summary.yml @@ -0,0 +1,66 @@ +name: Update Rules Summary + +on: + push: + paths: + - 'rules/**' + - 'scripts/generate-rules-summary.js' + # pull_request: + # paths: + # - 'rules/**' + # - 'scripts/generate-rules-summary.js' + +jobs: + update-summary: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + # Need to fetch the full history for the commit step + fetch-depth: 0 + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '22' + cache: 'npm' + + - name: Install dependencies + run: npm ci + + + - name: Generate rules summary + run: node scripts/generate-rules-summary.js + + - name: Check for changes + id: check-changes + run: | + if git diff --quiet RULES_SUMMARY.md; then + echo "no-changes=true" >> $GITHUB_OUTPUT + else + echo "no-changes=false" >> $GITHUB_OUTPUT + fi + + - name: Commit and push changes + if: steps.check-changes.outputs.no-changes == 'false' && github.event_name == 'push' + run: | + git config --local user.email "action@github.com" + git config --local user.name "GitHub Action" + git add RULES_SUMMARY.md + git commit -m "Update rules summary [skip ci]" + git push + + - name: Create Pull Request + if: steps.check-changes.outputs.no-changes == 'false' && github.event_name == 'pull_request' + uses: peter-evans/create-pull-request@v5 + with: + token: ${{ secrets.GITHUB_TOKEN }} + base: ${{ github.event.pull_request.base.ref }} + commit-message: "Update rules summary [skip ci]" + title: "Update rules summary" + body: "Automatically generated rules summary update" + branch: update-rules-summary-${{ github.run_number }} + delete-branch: true diff --git a/.gitignore b/.gitignore index fe03d01..86dcb06 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,91 @@ -npm-debug.log +# Dependencies node_modules/ +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Build outputs +dist/ +build/ +*.tsbuildinfo + +# Environment variables +.env +.env.local +.env.development.local +.env.test.local +.env.production.local + +# IDE files +.vscode/ +.idea/ +*.swp +*.swo +*~ + +# OS files .DS_Store +Thumbs.db + +# Logs +logs +*.log + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Coverage directory used by tools like istanbul +coverage/ +*.lcov + +# nyc test coverage +.nyc_output + +# Dependency directories +jspm_packages/ + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Microbundle cache +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# parcel-bundler cache (https://parceljs.org/) +.cache +.parcel-cache + +# Next.js build output +.next + +# Nuxt.js build / generate output +.nuxt + +# Storybook build outputs +.out +.storybook-out + +# Temporary folders +tmp/ +temp/ + +# Game state files (for development) +game-state.yaml +game-state.yaml.bak diff --git a/.markdownlint.json b/.markdownlint.json index 5ef25df..97988ce 100644 --- a/.markdownlint.json +++ b/.markdownlint.json @@ -1,8 +1,10 @@ { - "MD013": false, - "MD029": { - "style": "ordered" - }, - "MD036": false, - "MD041": false + "default": true, + "MD013": { "line_length": 120 }, + "MD029": { + "style": "ordered" + }, + "MD033": false, + "MD036": false, + "MD041": false } diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..d10a296 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,28 @@ +repos: + - repo: local + hooks: + - id: luacheck + name: luacheck + description: Run luacheck static analyzer + entry: luacheck + language: system + types: [lua] + args: [--codes, --formatter=plain] + files: ^(src|test)/.*\.lua$ + + - id: stylua + name: stylua + description: Run stylua formatter + entry: stylua + language: system + types: [lua] + files: ^(src|test)/.*\.lua$ + + - id: busted + name: busted + description: Run busted tests + entry: busted + language: system + pass_filenames: false + always_run: true + args: [test] \ No newline at end of file diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 921dee6..0000000 --- a/.travis.yml +++ /dev/null @@ -1,2 +0,0 @@ -language: node_js -script: npm run test diff --git a/AUTHORS.md b/AUTHORS.md index eab79b0..cf1c70f 100644 --- a/AUTHORS.md +++ b/AUTHORS.md @@ -1,5 +1,15 @@ -# Authors +# Authors & Contributors Peter Suber Michael Burns Justin Gallardo + +## Contributors + +We welcome contributions from the community! If you contribute to this project, please add your name (or GitHub handle) below in your pull request, or open an issue to be added. + +- [Your Name Here] + +--- + +Thank you to everyone who helps make Nomic better! diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..1d010f7 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,57 @@ +# Contributing to Nomic + +Thank you for your interest in contributing to the Nomic project! + +## Getting Started + +1. **Fork the repository** and clone it locally. +2. **Install dependencies**: + ```sh + npm install + ``` +3. **Run tests**: + ```sh + npm test + ``` +4. **Lint and format code**: + ```sh + npm run lint + npm run format + ``` + +## Project Structure + +- `src/` - Main TypeScript source code +- `src/services/` - Core service classes +- `src/types/` - Type definitions +- `src/test/` - Jest test files +- `rules/` - Game rules in Markdown +- `SCOREBOARD.md` - Current player scores +- `RULES_SUMMARY.md` - Auto-generated summary of rules + +## Submitting Changes + +- Create a new branch for your feature or bugfix. +- Write clear, descriptive commit messages. +- Add or update tests as appropriate. +- Run `npm test` and ensure all tests pass. +- Run `npm run lint` and `npm run format` to ensure code style consistency. +- Open a pull request with a clear description of your changes. + +## Code Style + +- This project uses [Prettier](https://prettier.io/) and [ESLint](https://eslint.org/) for code formatting and linting. +- Run `npm run lint` to check for lint errors. +- Run `npm run format` to automatically format your code. + +## Reporting Issues + +If you find a bug or have a feature request, please open an issue and provide as much detail as possible. + +## Community + +We welcome all contributions and ideas! Please be respectful and follow the [Code of Conduct](CODE_OF_CONDUCT.md) (if present). + +--- + +Thank you for helping make Nomic better! \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..290ae0d --- /dev/null +++ b/Dockerfile @@ -0,0 +1,24 @@ +FROM node:22-alpine + +# Install development tools +RUN apk add --no-cache \ + git \ + curl + +# Set up working directory +WORKDIR /app + +# Copy package files +COPY package*.json ./ + +# Install dependencies +RUN npm ci + +# Copy source code +COPY . . + +# Build the project +RUN npm run build + +# Set default command +CMD ["npm", "run", "check"] \ No newline at end of file diff --git a/LICENSE b/LICENSE index 670154e..19f3418 100644 --- a/LICENSE +++ b/LICENSE @@ -1,116 +1,21 @@ -CC0 1.0 Universal - -Statement of Purpose - -The laws of most jurisdictions throughout the world automatically confer -exclusive Copyright and Related Rights (defined below) upon the creator and -subsequent owner(s) (each and all, an "owner") of an original work of -authorship and/or a database (each, a "Work"). - -Certain owners wish to permanently relinquish those rights to a Work for the -purpose of contributing to a commons of creative, cultural and scientific -works ("Commons") that the public can reliably and without fear of later -claims of infringement build upon, modify, incorporate in other works, reuse -and redistribute as freely as possible in any form whatsoever and for any -purposes, including without limitation commercial purposes. These owners may -contribute to the Commons to promote the ideal of a free culture and the -further production of creative, cultural and scientific works, or to gain -reputation or greater distribution for their Work in part through the use and -efforts of others. - -For these and/or other purposes and motivations, and without any expectation -of additional consideration or compensation, the person associating CC0 with a -Work (the "Affirmer"), to the extent that he or she is an owner of Copyright -and Related Rights in the Work, voluntarily elects to apply CC0 to the Work -and publicly distribute the Work under its terms, with knowledge of his or her -Copyright and Related Rights in the Work and the meaning and intended legal -effect of CC0 on those rights. - -1. Copyright and Related Rights. A Work made available under CC0 may be -protected by copyright and related or neighboring rights ("Copyright and -Related Rights"). Copyright and Related Rights include, but are not limited -to, the following: - - i. the right to reproduce, adapt, distribute, perform, display, communicate, - and translate a Work; - - ii. moral rights retained by the original author(s) and/or performer(s); - - iii. publicity and privacy rights pertaining to a person's image or likeness - depicted in a Work; - - iv. rights protecting against unfair competition in regards to a Work, - subject to the limitations in paragraph 4(a), below; - - v. rights protecting the extraction, dissemination, use and reuse of data in - a Work; - - vi. database rights (such as those arising under Directive 96/9/EC of the - European Parliament and of the Council of 11 March 1996 on the legal - protection of databases, and under any national implementation thereof, - including any amended or successor version of such directive); and - - vii. other similar, equivalent or corresponding rights throughout the world - based on applicable law or treaty, and any national implementations thereof. - -2. Waiver. To the greatest extent permitted by, but not in contravention of, -applicable law, Affirmer hereby overtly, fully, permanently, irrevocably and -unconditionally waives, abandons, and surrenders all of Affirmer's Copyright -and Related Rights and associated claims and causes of action, whether now -known or unknown (including existing as well as future claims and causes of -action), in the Work (i) in all territories worldwide, (ii) for the maximum -duration provided by applicable law or treaty (including future time -extensions), (iii) in any current or future medium and for any number of -copies, and (iv) for any purpose whatsoever, including without limitation -commercial, advertising or promotional purposes (the "Waiver"). Affirmer makes -the Waiver for the benefit of each member of the public at large and to the -detriment of Affirmer's heirs and successors, fully intending that such Waiver -shall not be subject to revocation, rescission, cancellation, termination, or -any other legal or equitable action to disrupt the quiet enjoyment of the Work -by the public as contemplated by Affirmer's express Statement of Purpose. - -3. Public License Fallback. Should any part of the Waiver for any reason be -judged legally invalid or ineffective under applicable law, then the Waiver -shall be preserved to the maximum extent permitted taking into account -Affirmer's express Statement of Purpose. In addition, to the extent the Waiver -is so judged Affirmer hereby grants to each affected person a royalty-free, -non transferable, non sublicensable, non exclusive, irrevocable and -unconditional license to exercise Affirmer's Copyright and Related Rights in -the Work (i) in all territories worldwide, (ii) for the maximum duration -provided by applicable law or treaty (including future time extensions), (iii) -in any current or future medium and for any number of copies, and (iv) for any -purpose whatsoever, including without limitation commercial, advertising or -promotional purposes (the "License"). The License shall be deemed effective as -of the date CC0 was applied by Affirmer to the Work. Should any part of the -License for any reason be judged legally invalid or ineffective under -applicable law, such partial invalidity or ineffectiveness shall not -invalidate the remainder of the License, and in such case Affirmer hereby -affirms that he or she will not (i) exercise any of his or her remaining -Copyright and Related Rights in the Work or (ii) assert any associated claims -and causes of action with respect to the Work, in either case contrary to -Affirmer's express Statement of Purpose. - -4. Limitations and Disclaimers. - - a. No trademark or patent rights held by Affirmer are waived, abandoned, - surrendered, licensed or otherwise affected by this document. - - b. Affirmer offers the Work as-is and makes no representations or warranties - of any kind concerning the Work, express, implied, statutory or otherwise, - including without limitation warranties of title, merchantability, fitness - for a particular purpose, non infringement, or the absence of latent or - other defects, accuracy, or the present or absence of errors, whether or not - discoverable, all to the greatest extent permissible under applicable law. - - c. Affirmer disclaims responsibility for clearing rights of other persons - that may apply to the Work or any use thereof, including without limitation - any person's Copyright and Related Rights in the Work. Further, Affirmer - disclaims responsibility for obtaining any necessary consents, permissions - or other rights required for any use of the Work. - - d. Affirmer understands and acknowledges that Creative Commons is not a - party to this document and has no duty or obligation with respect to this - CC0 or use of the Work. - -For more information, please see - +MIT License + +Copyright (c) 2024 Nomic Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index ea22187..3789f59 100644 --- a/README.md +++ b/README.md @@ -1,86 +1,65 @@ -# Github Nomic +# Nomic Game -[![Join the chat at https://gitter.im/mburns/nomic](https://badges.gitter.im/mburns/nomic.svg)](https://gitter.im/mburns/nomic?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) -[![Build Status](https://travis-ci.org/mburns/nomic.svg?branch=master)](https://travis-ci.org/mburns/nomic) +[![CI](https://github.com/mburns/nomic/actions/workflows/ci.yml/badge.svg)](https://github.com/mburns/nomic/actions/workflows/ci.yml) +[![Tests](https://github.com/mburns/nomic/actions/workflows/test.yaml/badge.svg)](https://github.com/mburns/nomic/actions/workflows/test.yaml) +[![Lint](https://github.com/mburns/nomic/actions/workflows/lint.yaml/badge.svg)](https://github.com/mburns/nomic/actions/workflows/lint.yaml) +[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE) -This is an instance of the game [Nomic](https://en.wikipedia.org/wiki/Nomic) driven by Github interactions: +A GitHub-powered Nomic game engine implemented in TypeScript/Node.js. -* [Players](https://github.com/mburns/nomic/wiki/Player) are Github users that have [forked this repo](#joining-the-game) -* [Rules](https://github.com/mburns/nomic/wiki/rules) are stored as Markdown documents -* [Votes](https://github.com/mburns/nomic/wiki/Voting) are handled as (+1/-1) comments and discussion in [Pull Requests](https://github.com/mburns/nomic/pulls) +## Features -## What is Nomic - -> "[Nomic](http://legacy.earlham.edu/~peters/writing/nomic.htm) is a game in which changing the rules is a move. In that respect it differs from almost every other game. The primary activity of Nomic is proposing changes in the rules, debating the wisdom of changing them in that way, voting on the changes, deciding what can and cannot be done afterwards, and doing it. Even this core of the game, of course, can be changed." -> -- Peter Suber, [The Paradox of Self-Amendment](http://dash.harvard.edu/handle/1/10243418) +- Automated vote processing and score updating based on GitHub issues and comments +- Auto-merging of pull requests when voting conditions are met +- Rule management via Markdown files +- Automated rules summary and scoreboard +- CI/CD with GitHub Actions +- Modern TypeScript codebase with full test coverage ## Getting Started -Gameplay consists of: - -1. [Follow the existing rules](/rules/rule101.md). Questions can be asked via [Issues](https://github.com/mburns/nomic/issues). -2. A Player [proposes a rule-change](https://github.com/mburns/nomic/blob/master/.github/CONTRIBUTING.md) to an existing (or entirely new) rule via Pull Request. -3. [Players](https://github.com/mburns/nomic/wiki/Player) [discuss](/rules/rule111.md) and [vote](/rules/rule105.md) on proposals, earning [points in the process](/SCOREBOARD.md). - -The game is won when the first user reaches [+200 points](/rules/rule208.md) or is [stuck on a turn that is an impossible position](/rules/rule213.md) and it cannot be resolved through discussion or jurisdiction within the game. - -### Joining the game - -1. [Vote](https://github.com/mburns/nomic/blob/master/.github/CONTRIBUTING.md#voting) -2. [Suggests a rule-change](https://github.com/mburns/nomic/blob/master/.github/CONTRIBUTING.md#submit-a-rule-change). - -### Minimum rules worth knowing - -Here are the basic set of rules that describe the parameters of the game. All rules are subject to change (even 'immutable' rules), as that is central to the game. - -1. Players vote on and submit rule-changes to evolve the game -2. Disputes are resolved through [Call For Judgments](https://github.com/mburns/nomic/blob/master/.github/CONTRIBUTING.md#call-for-judgment) by choosing another Player as a nuetral arbitrator. - -#### Ground Rules - -Rule | Mutable | Brief Description ----- | ------- | ----------------- -[101](/rules/rule101.md) | N | All players must always abide by all the rules -[116](/rules/rule116.md) | N | Whatever is not prohibited or regulated by a rule is permitted and unregulated - -#### Player Actions - -Rule | Mutable | Brief Description ----- | ------- | ----------------- -[309](/rules/rule309.md) | Y | Every player is an eligible voter -[307](/rules/rule307.md) | Y | Each player always has exactly one vote -[202](/rules/rule202.md) | Y | One turn consists of: (1) proposing one rule-change and having it voted on, and (2) adding a [computed value](/rules/rule202.md) to your score - -#### Rule-changes - -Rule | Mutable | Brief Description ----- | ------- | ----------------- -[111](/rules/rule111.md) | N | If a rule-change as proposed is unclear then the other players may suggest amendments or argue against the proposal before the vote. -[104](/rules/rule104.md) | N | All rule-changes proposed in the proper way shall be [voted on](https://github.com/mburns/nomic/wiki/Voting) -[205](/rules/rule205.md) | Y | An adopted rule-change takes effect the moment of the completion of the vote that adopted it. +1. **Clone the repository** + ```sh + git clone https://github.com/mburns/nomic.git + cd nomic + ``` +2. **Install dependencies** + ```sh + npm install + ``` +3. **Run tests** + ```sh + npm test + ``` +4. **Lint and format code** + ```sh + npm run lint + npm run format + ``` -#### End-game +## Project Structure -Rule | Mutable | Brief Description ----- | ------- | ----------------- -[208](/rules/rule208.md) | Y | The **winner** of the Round is the first Player to achieve 200 (positive) points. -[213](/rules/rule213.md) | Y | If the rules are changed so that further play is impossible, then the first player unable to complete a turn is the **winner**. -[301](/rules/rule301.md) | Y | The nomic itself does not end and the ruleset remains unchanged. +- `src/` - Main TypeScript source code +- `src/services/` - Core service classes +- `src/types/` - Type definitions +- `src/test/` - Jest test files +- `rules/` - Game rules in Markdown +- `SCOREBOARD.md` - Current player scores +- `RULES_SUMMARY.md` - Auto-generated summary of rules -### Meta game +## Contributing -The [Wiki](https://github.com/mburns/nomic/wiki) and [Issues](https://github.com/mburns/nomic/issues) are intended to be 'out of bounds' or meta-game (in so much as such a thing is possible in Nomic) and used for coordination and clarification of the game's process by Players and spectators alike. +We welcome contributions from the community! Please see [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines on how to get started, code style, and submitting pull requests. -## Influences +## License -Formatting for these documents was inspired by [chef-rfc](https://github.com/chef/chef-rfc). +This project is licensed under the MIT License. See [LICENSE](LICENSE) for details. -This repository was inspired by a [Hacker News comment](https://news.ycombinator.com/item?id=4889988) by [ChrisAcky](http://acky.vze.com/) and follows in the footsteps of other Nomic Github games: +## Community & Support -* [aasmith/nomic](https://github.com/aasmith/nomic) -* [alokmenghrajani/nomic](https://github.com/alokmenghrajani/nomic) -* [fkh/nomic](https://github.com/fkh/nomic) +- Open an issue for bugs, questions, or feature requests +- Join the discussion in pull requests and issues -## Copyright +--- -All information on this site is public domain and may be distributed or copied unless otherwise specified. +Thank you for helping make Nomic better! diff --git a/RULES_SUMMARY.md b/RULES_SUMMARY.md new file mode 100644 index 0000000..976d7e5 --- /dev/null +++ b/RULES_SUMMARY.md @@ -0,0 +1,488 @@ +# Nomic Rules Summary + +This document provides a comprehensive overview of all rules in the Nomic game. This file is automatically generated from the `rules/` directory. + +## Rule Statistics + +| Statistic | Count | +|-------------------|-------| +| Total Rules | 31 | +| Immutable Rules | 16 | +| Mutable Rules | 15 | +| Active Rules | 30 | +| Draft Rules | 1 | +| Unique Tags | 8 | +| Unique Authors | 3 | + +## Tag Index + +- `change` (11) +- `endgame` (5) +- `init` (3) +- `judge` (2) +- `meta` (9) +- `player` (3) +- `point` (4) +- `vote` (9) + +## Detailed Rule List + +### Rule [101](rules/rule101.md): Rule [#40](https://github.com/mburns/nomic/pull/40) + +**Type**: Immutable +**Status**: Accepted +**Author**: Peter Suber +**Tags**: [`meta`](#tag-meta), [`init`](#tag-init) + +All players must always abide by all the rules then in effect, in the form in which they are then in effect. The rules in the [Initial Set](/.initial_set/) are in effect whenever a game begins. The [Initial Set](/.initial_set/) consists of Rules 101-116 (immutable) and 201-213 (mutable). + +--- +### Rule [102](rules/rule102.md): Rule + +**Type**: Immutable +**Status**: Accepted +**Author**: Peter Suber +**Tags**: [`meta`](#tag-meta), [`init`](#tag-init) + +Initially rules in the `100`'s are immutable and rules in the `200`'s are mutable. + +Rules subsequently enacted or transmuted (that is, changed from immutable to mutable or vice versa) may be immutable or mutable regardless of their numbers, and rules in the [Initial Set](/.initial_set/) may be transmuted regardless of their numbers. + +--- +### Rule [103](rules/rule103.md): Rule + +**Type**: Immutable +**Status**: Accepted +**Author**: Peter Suber +**Tags**: [`change`](#tag-change), [`init`](#tag-init) + +A rule-change is any of the following: + +1. the enactment, repeal, or amendment of a mutable rule; +2. the enactment, repeal, or amendment of an amendment of a mutable rule; or +3. the transmutation of an immutable rule into a mutable rule or vice versa. + +(Note: This definition implies that, at least initially, all new rules are mutable; immutable rules, as long as they are immutable, may not be amended or repealed; mutable rules, as long as they are mutable, may be amended or repealed; any rule of any status may be transmuted; no rule is absolutely immune to change.) + +--- +### Rule [104](rules/rule104.md): Rule + +**Type**: Immutable +**Status**: Accepted +**Author**: Peter Suber +**Tags**: [`change`](#tag-change), [`vote`](#tag-vote) + +All rule-changes proposed in the proper way shall be voted on. + +They will be adopted if and only if they receive the required number of votes. + +--- +### Rule [106](rules/rule106.md): Rule + +**Type**: Immutable +**Status**: Accepted +**Author**: Peter Suber +**Tags**: [`change`](#tag-change) + +All proposed rule-changes shall be written down before they are voted on. + +If they are adopted, they shall guide play in the form in which they were voted on. + +--- +### Rule [107](rules/rule107.md): Rule + +**Type**: Immutable +**Status**: Accepted +**Author**: Peter Suber +**Tags**: [`change`](#tag-change), [`vote`](#tag-vote) + +No rule-change may take effect earlier than the moment of the completion of the vote that adopted it, even if its wording explicitly states otherwise. + +No rule-change may have retroactive application. + +--- +### Rule [108](rules/rule108.md): Rule + +**Type**: Immutable +**Status**: Accepted +**Author**: Peter Suber +**Tags**: [`change`](#tag-change) + +Each proposed rule-change shall be given a number for reference. + +The numbers shall begin with `301`, and each rule-change proposed in the proper way shall receive the next successive integer, whether or not the proposal is adopted. + +* If a rule is repealed and reenacted, it receives the number of the proposal to reenact it. +* If a rule is amended or transmuted, it receives the number of the proposal to amend or transmute it. +* If an amendment is amended or repealed, the entire rule of which it is a part receives the number of the proposal to amend or repeal the amendment. + +--- +### Rule [109](rules/rule109.md): Rule + +**Type**: Immutable +**Status**: Accepted +**Author**: Peter Suber +**Tags**: [`change`](#tag-change), [`vote`](#tag-vote) + +Rule-changes that transmute immutable rules into mutable rules may be adopted if and only if the vote is unanimous among the eligible voters. + +Transmutation shall not be implied, but must be stated explicitly in a proposal to take effect. + +--- +### Rule [110](rules/rule110.md): Rule + +**Type**: Immutable +**Status**: Accepted +**Author**: Peter Suber +**Tags**: [`meta`](#tag-meta) + +In a conflict between a mutable and an immutable rule, the immutable rule takes precedence and the mutable rule shall be entirely void. + +For the purposes of this rule a proposal to transmute an immutable rule does not "conflict" with that immutable rule. + +--- +### Rule [111](rules/rule111.md): Rule + +**Type**: Immutable +**Status**: Accepted +**Author**: Peter Suber +**Tags**: [`change`](#tag-change), [`judge`](#tag-judge), [`vote`](#tag-vote) + +If a rule-change as proposed is unclear, ambiguous, paradoxical, or destructive of play, or if it arguably consists of two or more rule-changes compounded or is an amendment that makes no difference, or if it is otherwise of questionable value, then the other players may suggest amendments or argue against the proposal before the vote. + +A reasonable time must be allowed for this debate. The proponent decides the final form in which the proposal is to be voted on and, unless the Judge has been asked to do so, also decides the time to end debate and vote. + +--- +### Rule [112](rules/rule112.md): Rule + +**Type**: Immutable +**Status**: Accepted +**Author**: Peter Suber +**Tags**: [`endgame`](#tag-endgame), [`point`](#tag-point) + +The state of affairs that constitutes winning may not be altered from achieving `N` points to any other state of affairs. + +The magnitude of `N` and the means of earning points may be changed, and rules that establish a winner when play cannot continue may be enacted and (while they are mutable) be amended or repealed. + +--- +### Rule [113](rules/rule113.md): Rule + +**Type**: Immutable +**Status**: Accepted +**Author**: Peter Suber +**Tags**: [`endgame`](#tag-endgame), [`player`](#tag-player) + +A player always has the option to forfeit the game rather than continue to play or incur a game penalty. + +No penalty worse than losing, in the judgment of the player to incur it, may be imposed. + +--- +### Rule [114](rules/rule114.md): Rule + +**Type**: Immutable +**Status**: Accepted +**Author**: Peter Suber +**Tags**: [`change`](#tag-change), [`meta`](#tag-meta) + +There must always be at least one mutable rule. + +The adoption of rule-changes must never become completely impermissible. + +--- +### Rule [115](rules/rule115.md): Rule + +**Type**: Immutable +**Status**: Accepted +**Author**: Peter Suber +**Tags**: [`change`](#tag-change) + +Rule-changes that affect rules needed to allow or apply rule-changes are as permissible as other rule-changes. + +Even rule-changes that amend or repeal their own authority are permissible. + +No rule-change or type of move is impermissible solely on account of the self-reference or self-application of a rule. + +--- +### Rule [116](rules/rule116.md): Rule + +**Type**: Immutable +**Status**: Accepted +**Author**: Peter Suber +**Tags**: [`meta`](#tag-meta) + +Whatever is not prohibited or regulated by a rule is permitted and unregulated, with the sole exception of changing the rules, which is permitted only when a rule or set of rules explicitly or implicitly permits it. + +--- +### Rule [202](rules/rule202.md): Rule + +**Type**: Mutable +**Status**: Accepted +**Author**: Peter Suber +**Tags**: [`point`](#tag-point) + +One turn consists of two parts in this order: + +1. Proposing one rule-change and having it voted on, and +2. Subtracting `291` from the ordinal number of their proposal and multiply the result by the fraction of favorable votes it received, rounded to the nearest integer. + + (This yields a number between `0` and `10` for the first player, with the upper limit increasing by one each turn; more points are awarded for more popular proposals.) + +--- +### Rule [204](rules/rule204.md): Rule + +**Type**: Mutable +**Status**: Accepted +**Author**: Peter Suber +**Tags**: [`point`](#tag-point), [`vote`](#tag-vote) + +If and when rule-changes can be adopted without unanimity, the players who vote against winning proposals shall receive 10 points each. + +--- +### Rule [205](rules/rule205.md): Rule + +**Type**: Mutable +**Status**: Accepted +**Author**: Peter Suber +**Tags**: [`change`](#tag-change) + +An adopted rule-change takes full effect at the moment of the completion of the vote that adopted it. + +--- +### Rule [206](rules/rule206.md): Rule + +**Type**: Mutable +**Status**: Accepted +**Author**: Peter Suber +**Tags**: [`point`](#tag-point) + +When a proposed rule-change is defeated, the player who proposed it loses 10 points. + +--- +### Rule [208](rules/rule208.md): Rule + +**Type**: Mutable +**Status**: Accepted +**Author**: Peter Suber +**Tags**: [`endgame`](#tag-endgame) + +The winner is the first player to achieve `200` (positive) points. + +--- +### Rule [209](rules/rule209.md): Rule + +**Type**: Mutable +**Status**: Accepted +**Author**: Peter Suber +**Tags**: [`meta`](#tag-meta) + +At no time may there be more than `25` mutable rules. + +--- +### Rule [210](rules/rule210.md): Rule + +**Type**: Mutable +**Status**: Accepted +**Author**: Peter Suber +**Tags**: [`meta`](#tag-meta) + +This rule does not apply to games by mail or computer. + +--- +### Rule [211](rules/rule211.md): Rule + +**Type**: Mutable +**Status**: Accepted +**Author**: Peter Suber +**Tags**: [`meta`](#tag-meta) + +If two or more mutable rules conflict with one another, or if two or more immutable rules conflict with one another, then the rule with the lowest ordinal number takes precedence. + +If at least one of the rules in conflict explicitly says of itself that it defers to another rule (or type of rule) or takes precedence over another rule (or type of rule), then such provisions shall supersede the numerical method for determining precedence. + +If two or more rules claim to take precedence over one another or to defer to one another, then the numerical method again governs. + +--- +### Rule [212](rules/rule212.md): Rule + +**Type**: Mutable +**Status**: Accepted +**Author**: Peter Suber +**Tags**: [`judge`](#tag-judge), [`vote`](#tag-vote) + +If players disagree about the legality of a move or the interpretation or application of a rule, then the player preceding the one moving is to be the Judge and decide the question. Disagreement for the purposes of this rule may be created by the insistence of any player. This process is called invoking Judgment. + +When Judgment has been invoked, the next player may not begin his or her turn without the consent of a majority of the other players. + +The Judge's Judgment may be overruled only by a unanimous vote of the other players taken before the next turn is begun. If a Judge's Judgment is overruled, then the player preceding the Judge in the playing order becomes the new Judge for the question, and so on, except that no player is to be Judge during his or her own turn or during the turn of a team-mate. + +Unless a Judge is overruled, one Judge settles all questions arising from the game until the next turn is begun, including questions as to his or her own legitimacy and jurisdiction as Judge. + +New Judges are not bound by the decisions of old Judges. New Judges may, however, settle only those questions on which the players currently disagree and that affect the completion of the turn in which Judgment was invoked. All decisions by Judges shall be in accordance with all the rules then in effect; but when the rules are silent, inconsistent, or unclear on the point at issue, then the Judge shall consider game-custom and the spirit of the game before applying other standards. + +--- +### Rule [213](rules/rule213.md): Rule + +**Type**: Mutable +**Status**: Accepted +**Author**: Peter Suber +**Tags**: [`endgame`](#tag-endgame) + +If the rules are changed so that further play is impossible, or if the legality of a move cannot be determined with finality, or if by the Judge's best reasoning, not overruled, a move appears equally legal and illegal, then the first player unable to complete a turn is the winner. + +**This rule takes precedence over every other rule determining the winner.** + +--- +### Rule [301](rules/rule301.md): Rule + +**Type**: Mutable +**Status**: Draft +**Author**: Michael Burns +**Tags**: [`endgame`](#tag-endgame) + +When the Rules state that a Player or Players win the game, those players win the game and are declared Champion(s); specifically they win the Round that ends with the indicated win. + +The nomic itself does not end and the ruleset remains unchanged. + +## References + +* [Agora nomic](http://www.agoranomic.org/), [rule 2343: Victory Conditions](https://www.eecs.berkeley.edu/~charles/agora/current_flr.txt) + +--- +### Rule [304](rules/rule304.md): Rule + +**Type**: Mutable +**Status**: Accepted +**Author**: Justin Gallardo +**Tags**: [`meta`](#tag-meta), [`change`](#tag-change) + +All rule proposals and amendments must adhere to the commit format set +forth below. + +## Proposals + +```markdown +propose(): Short, imperative description of the new rule. + +A longer description of the rule if it is needed. The short description +should be limited to 72 characters or less. +``` + +Example: + +```markdown +propose(304): Define commit message format. + +This rule defines a standard format for commit messages used when +proposing new rules or amending existing rules. +``` + +## Amendments + +```markdown +amend(): Short, imperative description of the rule change. + +A longer description of the rule if it is needed. The short description +should be limited to 72 characters or less. +``` + +Example: + +```markdown +amend(304): Increase the maximum characters in first line of commit msg. + +This increases the maximum length of the first line in a commit message +to 80 characters up from 72 characters. +``` + +## Judgments + +```markdown +CFJ(): Short, imperative description of the CFJ + +A longer description of the claim or judgment if it is needed. The short description should be limited to 80 characters or less. +``` + +Example: + +```markdown +CFJ(1): Player Foo violated Rule ### + +I claim that Player Foo violated Rule ### when X, Y and Z occurred. +``` + +--- +### Rule [306](rules/rule306.md): Rule + +**Type**: Mutable +**Status**: Accepted +**Author**: Peter Suber +**Tags**: [`player`](#tag-player) + +Players may take their turn at any time, independent of other Players. + +--- +### Rule [307](rules/rule307.md): Rule + +**Type**: Mutable +**Status**: Accepted +**Author**: Peter Suber +**Tags**: [`vote`](#tag-vote) + +Each player always has exactly one vote. + +Upon submission of a rule-change, the proposing Player automatically casts an immutable `+1` vote for their proposal. + +--- +### Rule [308](rules/rule308.md): Rule [#46](https://github.com/mburns/nomic/pull/46) + +**Type**: Mutable +**Status**: Accepted +**Author**: Peter Suber +**Tags**: [`vote`](#tag-vote) + +A rule-change is adopted if and only if the vote is at least two-thirds in the affirmative (`+1`) among eligible voters. + +```javascript +let v = number of votes +let a = number of affirmative votes + +let p = a / v + +if p >= (2/3), proposal is adopted +``` + +## References + +* [CharNomic #109](http://www.tesseract.org/nomic/ruleset.html#NewRules) + +--- +### Rule [309](rules/rule309.md): Rule [#46](https://github.com/mburns/nomic/pull/46) + +**Type**: Immutable +**Status**: Accepted +**Author**: Peter Suber +**Tags**: [`player`](#tag-player), [`vote`](#tag-vote) + +Every player is an eligible voter. + +Eligible voters may choose to participate in votes on rule-changes or abstain. If no action is taken, a voter is considered to have abstained and is neither for or against the proposal. + +Quorum is half of all active registered players. Votes on rule-changes take effect if and only if quorum is reached. + +```javascript +let n = number of active players +let v = number of votes +let q = Floor(n / 2) + 1 + +if v >= q, quorum is reached. +``` + +## References + +* [Chiark Nomic, rule 201](http://www.chiark.greenend.org.uk/~dricher/Nomic/CN/rules.html) + +--- + +--- + +*This file was automatically generated on 2025-06-29T20:49:51.444Z. Any changes to the `rules/` directory will regenerate this file.* diff --git a/SCOREBOARD.md b/SCOREBOARD.md index de63778..641bd00 100644 --- a/SCOREBOARD.md +++ b/SCOREBOARD.md @@ -1,6 +1,40 @@ -# Scoreboard +# Nomic Game Scoreboard -User | Points ----- | ------ -@mburns | 0 -@jirwin | 0 +Welcome to the Nomic game! This scoreboard tracks player participation, proposals, and voting activity. + +## Current Standings + +User | Points | Proposals | Passed | Votes Cast | Last Active +---- | ------ | --------- | ------ | ---------- | ------------ +@mburns | 0 | 0 | 0 | 0 | Never +@jirwin | 0 | 0 | 0 | 0 | Never + +## Game Statistics + +- **Total Players**: 2 +- **Total Proposals**: 0 +- **Active Proposals**: 0 +- **Current Turn**: 1 + +## How to Play + +1. **Create a Proposal**: Open a GitHub Issue with your rule change proposal +2. **Vote on Proposals**: Comment on issues using formats like `VOTE: FOR` or `I vote AGAINST` +3. **Track Progress**: Watch the automated updates as votes are processed +4. **Earn Points**: Get points for successful proposals and voting with the majority + +## Voting Formats + +- `VOTE: FOR` or `VOTE: AGAINST` +- `I vote FOR` or `I vote AGAINST` +- `My vote is FOR` or `My vote is AGAINST` +- `Voting FOR` or `Voting AGAINST` +- `āœ… FOR` or `āŒ AGAINST` + +## Scoring System + +- **Successful Proposal**: 3 points +- **Voting with Majority**: 2 points +- **Participation**: 1 point + +*This scoreboard is automatically updated by the Nomic game system.* diff --git a/biome.json b/biome.json new file mode 100644 index 0000000..f5b6ba7 --- /dev/null +++ b/biome.json @@ -0,0 +1,34 @@ +{ + "$schema": "https://biomejs.dev/schemas/2.0.6/schema.json", + "vcs": { + "enabled": false, + "clientKind": "git", + "useIgnoreFile": false + }, + "files": { + "ignoreUnknown": false + }, + "formatter": { + "enabled": true, + "indentStyle": "tab" + }, + "linter": { + "enabled": true, + "rules": { + "recommended": true + } + }, + "javascript": { + "formatter": { + "quoteStyle": "double" + } + }, + "assist": { + "enabled": true, + "actions": { + "source": { + "organizeImports": "on" + } + } + } +} diff --git a/config.example.yaml b/config.example.yaml new file mode 100644 index 0000000..d7eb434 --- /dev/null +++ b/config.example.yaml @@ -0,0 +1,46 @@ +# Nomic Game Configuration Example +# Copy this file to config.yaml and modify as needed + +game: + # Voting requirements + requiredVotes: 3 # Minimum number of votes needed + requiredMajority: 0.5 # Majority percentage (0.5 = 50%) + + # Scoring system + pointsForSuccessfulProposal: 3 # Points for proposer when proposal passes + pointsForVotingWithMajority: 2 # Points for voting with the winning side + pointsForParticipation: 1 # Points for participating in voting + + # Automation settings + autoMergeEnabled: true # Enable automatic merging of approved proposals + branchProtectionEnabled: true # Enable branch protection rules + +github: + # Repository settings + baseBranch: "main" # Default branch for the repository + + # Labels to use + labels: + proposal: "proposal" # Label for new proposals + passed: "passed" # Label for passed proposals + failed: "failed" # Label for failed proposals + closed: "closed" # Label for closed proposals + +# Advanced settings +advanced: + # Vote parsing patterns (regex) + votePatterns: + - "^VOTE:\\s*(FOR|AGAINST)$" + - "^I vote (FOR|AGAINST)$" + - "^My vote is (FOR|AGAINST)$" + - "^Voting (FOR|AGAINST)$" + - "^āœ… (FOR|AGAINST)$" + - "^āŒ (FOR|AGAINST)$" + + # File paths + gameStateFile: "game-state.yaml" # File to store game state + scoreboardFile: "SCOREBOARD.md" # File to store scoreboard + + # Logging + logLevel: "info" # Log level: debug, info, warn, error + enableDebugLogs: false # Enable detailed debug logging \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..a912a91 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,32 @@ +version: '3' + +services: + app: + build: . + volumes: + - .:/app + command: npm run check + + test: + build: . + volumes: + - .:/app + command: npm test + + lint: + build: . + volumes: + - .:/app + command: npm run lint + + format: + build: . + volumes: + - .:/app + command: npm run format + + coverage: + build: . + volumes: + - .:/app + command: npm run test:coverage \ No newline at end of file diff --git a/jest.config.js b/jest.config.js new file mode 100644 index 0000000..f4bacb5 --- /dev/null +++ b/jest.config.js @@ -0,0 +1,18 @@ +module.exports = { + preset: "ts-jest", + testEnvironment: "node", + roots: ["/src"], + testMatch: ["**/__tests__/**/*.ts", "**/?(*.)+(spec|test).ts"], + transform: { + "^.+\\.ts$": "ts-jest", + }, + collectCoverageFrom: [ + "src/**/*.ts", + "!src/**/*.d.ts", + "!src/**/*.test.ts", + "!src/**/*.spec.ts", + ], + coverageDirectory: "coverage", + coverageReporters: ["text", "lcov", "html"], + setupFilesAfterEnv: ["/src/test/setup.ts"], +}; diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..a7a6ec9 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,4503 @@ +{ + "name": "nomic-game", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "nomic-game", + "version": "1.0.0", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "@octokit/rest": "^20.0.2", + "js-yaml": "^4.1.0", + "zod": "^3.22.4" + }, + "devDependencies": { + "@biomejs/biome": "^2.0.6", + "@types/jest": "^29.5.8", + "@types/js-yaml": "^4.0.9", + "@types/node": "^20.8.10", + "jest": "^29.7.0", + "ts-jest": "^29.1.1", + "ts-node": "^10.9.1", + "typescript": "^5.2.2" + }, + "engines": { + "node": ">=22.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.27.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.27.7.tgz", + "integrity": "sha512-xgu/ySj2mTiUFmdE9yCMfBxLp4DHd5DwmbbD05YAuICfodYT3VvRxbrh81LGQ/8UpSdtMdfKMn3KouYDX59DGQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.27.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.27.7.tgz", + "integrity": "sha512-BU2f9tlKQ5CAthiMIgpzAh4eDTLWo1mqi9jqE2OxMG0E/OM199VJt2q8BztTxpnSW0i1ymdwLXRJnYzvDM5r2w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.27.5", + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-module-transforms": "^7.27.3", + "@babel/helpers": "^7.27.6", + "@babel/parser": "^7.27.7", + "@babel/template": "^7.27.2", + "@babel/traverse": "^7.27.7", + "@babel/types": "^7.27.7", + "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/core/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/@babel/generator": { + "version": "7.27.5", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.27.5.tgz", + "integrity": "sha512-ZGhA37l0e/g2s1Cnzdix0O3aLYm66eF8aufiVteOgnwxgnRP8GoyMj7VWsgWnQbVKXyge7hqrFh2K2TQM6t1Hw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.27.5", + "@babel/types": "^7.27.3", + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25", + "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-compilation-targets/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/@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.27.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.7.tgz", + "integrity": "sha512-qnzXzDXdr/po3bOTbTIQZ7+TxNKxpkN5IifVLXS+r7qwynkZfPyjZfE7hCXbo7IoO9TNcSyibgONsf2HauUd3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.27.7" + }, + "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.27.7", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.27.7.tgz", + "integrity": "sha512-X6ZlfR/O/s5EQ/SnUSLzr+6kGnkg8HXGMzpgsMsrJVcfDtH1vIp6ctCN4eZ1LS5c0+te5Cb6Y514fASjMRJ1nw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.27.5", + "@babel/parser": "^7.27.7", + "@babel/template": "^7.27.2", + "@babel/types": "^7.27.7", + "debug": "^4.3.1", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse/node_modules/globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/types": { + "version": "7.27.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.7.tgz", + "integrity": "sha512-8OLQgDScAOHXnAz2cV+RfzzNMipuLVBz2biuAJFMV9bfkNf393je3VM8CLkjQodW5+iWsSJdSgSWT6rsZoXHPw==", + "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/@biomejs/biome": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@biomejs/biome/-/biome-2.0.6.tgz", + "integrity": "sha512-RRP+9cdh5qwe2t0gORwXaa27oTOiQRQvrFf49x2PA1tnpsyU7FIHX4ZOFMtBC4QNtyWsN7Dqkf5EDbg4X+9iqA==", + "dev": true, + "license": "MIT OR Apache-2.0", + "bin": { + "biome": "bin/biome" + }, + "engines": { + "node": ">=14.21.3" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/biome" + }, + "optionalDependencies": { + "@biomejs/cli-darwin-arm64": "2.0.6", + "@biomejs/cli-darwin-x64": "2.0.6", + "@biomejs/cli-linux-arm64": "2.0.6", + "@biomejs/cli-linux-arm64-musl": "2.0.6", + "@biomejs/cli-linux-x64": "2.0.6", + "@biomejs/cli-linux-x64-musl": "2.0.6", + "@biomejs/cli-win32-arm64": "2.0.6", + "@biomejs/cli-win32-x64": "2.0.6" + } + }, + "node_modules/@biomejs/cli-darwin-arm64": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-2.0.6.tgz", + "integrity": "sha512-AzdiNNjNzsE6LfqWyBvcL29uWoIuZUkndu+wwlXW13EKcBHbbKjNQEZIJKYDc6IL+p7bmWGx3v9ZtcRyIoIz5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-darwin-x64": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-x64/-/cli-darwin-x64-2.0.6.tgz", + "integrity": "sha512-wJjjP4E7bO4WJmiQaLnsdXMa516dbtC6542qeRkyJg0MqMXP0fvs4gdsHhZ7p9XWTAmGIjZHFKXdsjBvKGIJJQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-arm64": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64/-/cli-linux-arm64-2.0.6.tgz", + "integrity": "sha512-ZSVf6TYo5rNMUHIW1tww+rs/krol7U5A1Is/yzWyHVZguuB0lBnIodqyFuwCNqG9aJGyk7xIMS8HG0qGUPz0SA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-arm64-musl": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.0.6.tgz", + "integrity": "sha512-CVPEMlin3bW49sBqLBg2x016Pws7eUXA27XYDFlEtponD0luYjg2zQaMJ2nOqlkKG9fqzzkamdYxHdMDc2gZFw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-x64": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64/-/cli-linux-x64-2.0.6.tgz", + "integrity": "sha512-geM1MkHTV1Kh2Cs/Xzot9BOF3WBacihw6bkEmxkz4nSga8B9/hWy5BDiOG3gHDGIBa8WxT0nzsJs2f/hPqQIQw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-x64-musl": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64-musl/-/cli-linux-x64-musl-2.0.6.tgz", + "integrity": "sha512-mKHE/e954hR/hSnAcJSjkf4xGqZc/53Kh39HVW1EgO5iFi0JutTN07TSjEMg616julRtfSNJi0KNyxvc30Y4rQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-win32-arm64": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-arm64/-/cli-win32-arm64-2.0.6.tgz", + "integrity": "sha512-290V4oSFoKaprKE1zkYVsDfAdn0An5DowZ+GIABgjoq1ndhvNxkJcpxPsiYtT7slbVe3xmlT0ncdfOsN7KruzA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-win32-x64": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-x64/-/cli-win32-x64-2.0.6.tgz", + "integrity": "sha512-bfM1Bce0d69Ao7pjTjUS+AWSZ02+5UHdiAP85Th8e9yV5xzw6JrHXbL5YWlcEKQ84FIZMdDc7ncuti1wd2sdbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@cspotcode/source-map-support/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "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/load-nyc-config/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/@istanbuljs/load-nyc-config/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/@istanbuljs/load-nyc-config/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/@istanbuljs/load-nyc-config/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/@istanbuljs/load-nyc-config/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/@istanbuljs/load-nyc-config/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/@istanbuljs/load-nyc-config/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/@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.8", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz", + "integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/set-array": "^1.2.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "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/set-array": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@octokit/auth-token": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-4.0.0.tgz", + "integrity": "sha512-tY/msAuJo6ARbK6SPIxZrPBms3xPbfwBrulZe0Wtr/DIY9lje2HeV1uoebShn6mx7SjCHif6EjMvoREj+gZ+SA==", + "license": "MIT", + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/core": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/@octokit/core/-/core-5.2.1.tgz", + "integrity": "sha512-dKYCMuPO1bmrpuogcjQ8z7ICCH3FP6WmxpwC03yjzGfZhj9fTJg6+bS1+UAplekbN2C+M61UNllGOOoAfGCrdQ==", + "license": "MIT", + "dependencies": { + "@octokit/auth-token": "^4.0.0", + "@octokit/graphql": "^7.1.0", + "@octokit/request": "^8.4.1", + "@octokit/request-error": "^5.1.1", + "@octokit/types": "^13.0.0", + "before-after-hook": "^2.2.0", + "universal-user-agent": "^6.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/endpoint": { + "version": "9.0.6", + "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-9.0.6.tgz", + "integrity": "sha512-H1fNTMA57HbkFESSt3Y9+FBICv+0jFceJFPWDePYlR/iMGrwM5ph+Dd4XRQs+8X+PUFURLQgX9ChPfhJ/1uNQw==", + "license": "MIT", + "dependencies": { + "@octokit/types": "^13.1.0", + "universal-user-agent": "^6.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/graphql": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-7.1.1.tgz", + "integrity": "sha512-3mkDltSfcDUoa176nlGoA32RGjeWjl3K7F/BwHwRMJUW/IteSa4bnSV8p2ThNkcIcZU2umkZWxwETSSCJf2Q7g==", + "license": "MIT", + "dependencies": { + "@octokit/request": "^8.4.1", + "@octokit/types": "^13.0.0", + "universal-user-agent": "^6.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/openapi-types": { + "version": "24.2.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-24.2.0.tgz", + "integrity": "sha512-9sIH3nSUttelJSXUrmGzl7QUBFul0/mB8HRYl3fOlgHbIWG+WnYDXU3v/2zMtAvuzZ/ed00Ei6on975FhBfzrg==", + "license": "MIT" + }, + "node_modules/@octokit/plugin-paginate-rest": { + "version": "11.4.4-cjs.2", + "resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-11.4.4-cjs.2.tgz", + "integrity": "sha512-2dK6z8fhs8lla5PaOTgqfCGBxgAv/le+EhPs27KklPhm1bKObpu6lXzwfUEQ16ajXzqNrKMujsFyo9K2eaoISw==", + "license": "MIT", + "dependencies": { + "@octokit/types": "^13.7.0" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "@octokit/core": "5" + } + }, + "node_modules/@octokit/plugin-request-log": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@octokit/plugin-request-log/-/plugin-request-log-4.0.1.tgz", + "integrity": "sha512-GihNqNpGHorUrO7Qa9JbAl0dbLnqJVrV8OXe2Zm5/Y4wFkZQDfTreBzVmiRfJVfE4mClXdihHnbpyyO9FSX4HA==", + "license": "MIT", + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "@octokit/core": "5" + } + }, + "node_modules/@octokit/plugin-rest-endpoint-methods": { + "version": "13.3.2-cjs.1", + "resolved": "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-13.3.2-cjs.1.tgz", + "integrity": "sha512-VUjIjOOvF2oELQmiFpWA1aOPdawpyaCUqcEBc/UOUnj3Xp6DJGrJ1+bjUIIDzdHjnFNO6q57ODMfdEZnoBkCwQ==", + "license": "MIT", + "dependencies": { + "@octokit/types": "^13.8.0" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "@octokit/core": "^5" + } + }, + "node_modules/@octokit/request": { + "version": "8.4.1", + "resolved": "https://registry.npmjs.org/@octokit/request/-/request-8.4.1.tgz", + "integrity": "sha512-qnB2+SY3hkCmBxZsR/MPCybNmbJe4KAlfWErXq+rBKkQJlbjdJeS85VI9r8UqeLYLvnAenU8Q1okM/0MBsAGXw==", + "license": "MIT", + "dependencies": { + "@octokit/endpoint": "^9.0.6", + "@octokit/request-error": "^5.1.1", + "@octokit/types": "^13.1.0", + "universal-user-agent": "^6.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/request-error": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-5.1.1.tgz", + "integrity": "sha512-v9iyEQJH6ZntoENr9/yXxjuezh4My67CBSu9r6Ve/05Iu5gNgnisNWOsoJHTP6k0Rr0+HQIpnH+kyammu90q/g==", + "license": "MIT", + "dependencies": { + "@octokit/types": "^13.1.0", + "deprecation": "^2.0.0", + "once": "^1.4.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/rest": { + "version": "20.1.2", + "resolved": "https://registry.npmjs.org/@octokit/rest/-/rest-20.1.2.tgz", + "integrity": "sha512-GmYiltypkHHtihFwPRxlaorG5R9VAHuk/vbszVoRTGXnAsY60wYLkh/E2XiFmdZmqrisw+9FaazS1i5SbdWYgA==", + "license": "MIT", + "dependencies": { + "@octokit/core": "^5.0.2", + "@octokit/plugin-paginate-rest": "11.4.4-cjs.2", + "@octokit/plugin-request-log": "^4.0.0", + "@octokit/plugin-rest-endpoint-methods": "13.3.2-cjs.1" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/types": { + "version": "13.10.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.10.0.tgz", + "integrity": "sha512-ifLaO34EbbPj0Xgro4G5lP5asESjwHracYJvVaPIyXMuiuXLlhic3S47cBdTb+jfODkTE5YtGCLt3Ay3+J97sA==", + "license": "MIT", + "dependencies": { + "@octokit/openapi-types": "^24.2.0" + } + }, + "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/@tsconfig/node10": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz", + "integrity": "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "dev": true, + "license": "MIT" + }, + "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/jest": { + "version": "29.5.14", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.14.tgz", + "integrity": "sha512-ZN+4sdnLUbo8EVvVc2ao0GFW6oVrQRPn4K2lglySj7APvSrgzxHiNNK99us4WDMi57xxA2yggblIAMNhXOotLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "expect": "^29.0.0", + "pretty-format": "^29.0.0" + } + }, + "node_modules/@types/js-yaml": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-4.0.9.tgz", + "integrity": "sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "20.19.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.2.tgz", + "integrity": "sha512-9pLGGwdzOUBDYi0GNjM97FIA+f92fqSke6joWeBjWXllfNxZBs7qeMF7tvtOIsbY45xkWkxrdwUfUf3MnQa9gA==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.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/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.4", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", + "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "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-escapes/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/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/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true, + "license": "MIT" + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "license": "Python-2.0" + }, + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "dev": true, + "license": "MIT" + }, + "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-istanbul/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/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/before-after-hook": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-2.2.3.tgz", + "integrity": "sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ==", + "license": "Apache-2.0" + }, + "node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "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/bs-logger": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/bs-logger/-/bs-logger-0.2.6.tgz", + "integrity": "sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-json-stable-stringify": "2.x" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/bser": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", + "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.30001726", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001726.tgz", + "integrity": "sha512-VQAUIUzBiZ/UnlM28fSp2CRF3ivUn1BWEvxMcVTNwpw91Py1pGbPIyIKtd+tzct9C3ouceCVdGAXxZOpZAsgdw==", + "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/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true, + "license": "MIT" + }, + "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/deprecation": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/deprecation/-/deprecation-2.3.1.tgz", + "integrity": "sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ==", + "license": "ISC" + }, + "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": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "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/ejs": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", + "integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "jake": "^10.8.5" + }, + "bin": { + "ejs": "bin/cli.js" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.177", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.177.tgz", + "integrity": "sha512-7EH2G59nLsEMj97fpDuvVcYi6lwTcM1xuWw3PssD8xzboAW7zj7iB3COEEEATUfjLHrs5uKBLQT03V/8URx06g==", + "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/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/filelist": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz", + "integrity": "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "minimatch": "^5.0.1" + } + }, + "node_modules/filelist/node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "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/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/glob/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/glob/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/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-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/jake": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.2.tgz", + "integrity": "sha512-2P4SQ0HrLQ+fw6llpLnOaGAvN2Zu6778SJMrCUwns4fOoG9ayrTiZk3VV8sCPkVZF8ab0zksVpS8FDY5pRCNBA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "async": "^3.2.3", + "chalk": "^4.0.2", + "filelist": "^1.0.4", + "minimatch": "^3.1.2" + }, + "bin": { + "jake": "bin/cli.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jake/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/jake/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/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-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": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "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/lodash.memoize": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", + "integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==", + "dev": true, + "license": "MIT" + }, + "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-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true, + "license": "ISC" + }, + "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/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==", + "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-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/pkg-dir/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/pkg-dir/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/pkg-dir/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/pkg-dir/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/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-cwd/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": "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/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/stack-utils/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/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/test-exclude/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/test-exclude/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/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/ts-jest": { + "version": "29.4.0", + "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.4.0.tgz", + "integrity": "sha512-d423TJMnJGu80/eSgfQ5w/R+0zFJvdtTxwtF9KzFFunOpSeD+79lHJQIiAhluJoyGRbvj9NZJsl9WjCUo0ND7Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "bs-logger": "^0.2.6", + "ejs": "^3.1.10", + "fast-json-stable-stringify": "^2.1.0", + "json5": "^2.2.3", + "lodash.memoize": "^4.1.2", + "make-error": "^1.3.6", + "semver": "^7.7.2", + "type-fest": "^4.41.0", + "yargs-parser": "^21.1.1" + }, + "bin": { + "ts-jest": "cli.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || ^18.0.0 || >=20.0.0" + }, + "peerDependencies": { + "@babel/core": ">=7.0.0-beta.0 <8", + "@jest/transform": "^29.0.0 || ^30.0.0", + "@jest/types": "^29.0.0 || ^30.0.0", + "babel-jest": "^29.0.0 || ^30.0.0", + "jest": "^29.0.0 || ^30.0.0", + "jest-util": "^29.0.0 || ^30.0.0", + "typescript": ">=4.3 <6" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "@jest/transform": { + "optional": true + }, + "@jest/types": { + "optional": true + }, + "babel-jest": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jest-util": { + "optional": true + } + } + }, + "node_modules/ts-jest/node_modules/type-fest": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", + "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ts-node": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, + "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/typescript": { + "version": "5.8.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", + "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/universal-user-agent": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-6.0.1.tgz", + "integrity": "sha512-yCzhz6FN2wU1NiiQRogkTQszlQSlpWaw8SvVegAc+bDxbzHgh1vX8uIe8OYyMH6DwH+sdTJsgMl36+mSMdRJIQ==", + "license": "ISC" + }, + "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-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true, + "license": "MIT" + }, + "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==", + "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/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zod": { + "version": "3.25.67", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.67.tgz", + "integrity": "sha512-idA2YXwpCdqUSKRCACDE6ItZD9TZzy3OZMtpfLoh6oPR47lipysRrJfjzMqFxQ3uJuUPyUeWe1r9vLH33xO/Qw==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + } + } +} diff --git a/package.json b/package.json index 395922b..f0a7684 100644 --- a/package.json +++ b/package.json @@ -1,29 +1,53 @@ { - "name": "nomic", - "version": "1.0.0", - "description": "Nomic is a game in which changing the rules is a move.", - "main": "index.js", - "scripts": { - "test": "markdownlint --config .markdownlint.json *.md" - }, - "repository": { - "type": "git", - "url": "git+https://github.com/mburns/nomic.git" - }, - "keywords": [ - "nomic" - ], - "author": "Michael Burns ", - "contributors": [{ - "name": "Justin Gallardo", - "email": "justin.gallardo@gmail.com" - }], - "license": "CC0-1.0", - "bugs": { - "url": "https://github.com/mburns/nomic/issues" - }, - "homepage": "https://github.com/mburns/nomic#readme", - "devDependencies": { - "markdownlint-cli": "0.0.3" - } + "name": "nomic-game", + "version": "1.0.0", + "description": "A Nomic game implementation with automated voting and scoring", + "main": "dist/index.js", + "scripts": { + "build": "tsc", + "start": "node dist/index.js", + "dev": "ts-node src/index.ts", + "test": "jest", + "test:watch": "jest --watch", + "test:coverage": "jest --coverage", + "lint": "biome check .", + "lint:fix": "biome check --write .", + "format": "biome format .", + "format:fix": "biome format --write .", + "type-check": "tsc --noEmit", + "clean": "rm -rf dist", + "prebuild": "npm run clean", + "postinstall": "npm run build", + "generate-rules": "node scripts/generate-rules-summary.js", + "install-deps": "npm ci", + "dev-setup": "npm ci && npm run build", + "check": "npm run lint && npm run format:fix && npm test", + "all": "npm run install-deps && npm run check" + }, + "keywords": [ + "nomic", + "game", + "voting", + "rules" + ], + "author": "", + "license": "MIT", + "devDependencies": { + "@biomejs/biome": "^2.0.6", + "@types/jest": "^29.5.8", + "@types/js-yaml": "^4.0.9", + "@types/node": "^20.8.10", + "jest": "^29.7.0", + "ts-jest": "^29.1.1", + "ts-node": "^10.9.1", + "typescript": "^5.2.2" + }, + "dependencies": { + "@octokit/rest": "^20.0.2", + "js-yaml": "^4.1.0", + "zod": "^3.22.4" + }, + "engines": { + "node": ">=22.0.0" + } } diff --git a/scripts/auto-merge-proposal.js b/scripts/auto-merge-proposal.js new file mode 100644 index 0000000..da13259 --- /dev/null +++ b/scripts/auto-merge-proposal.js @@ -0,0 +1,28 @@ +const { NomicGame, defaultGameConfig } = require("../dist/index.js"); + +async function autoMergeProposal() { + const issueNumber = process.argv[2]; + + if (!issueNumber) { + console.error("Usage: node auto-merge-proposal.js "); + process.exit(1); + } + + const githubConfig = { + token: process.env.GITHUB_TOKEN, + owner: process.env.GITHUB_REPOSITORY_OWNER, + repo: process.env.GITHUB_REPOSITORY_NAME, + baseBranch: "main", + }; + + try { + const game = new NomicGame(githubConfig, defaultGameConfig); + await game.autoMergeProposal(parseInt(issueNumber)); + console.log("Successfully auto-merged proposal"); + } catch (error) { + console.error("Failed to auto-merge:", error); + process.exit(1); + } +} + +autoMergeProposal(); diff --git a/scripts/check-proposal-status.js b/scripts/check-proposal-status.js new file mode 100644 index 0000000..e58e82c --- /dev/null +++ b/scripts/check-proposal-status.js @@ -0,0 +1,41 @@ +const { NomicGame, defaultGameConfig } = require("../dist/index.js"); + +async function checkProposalStatus() { + const issueNumber = process.argv[2]; + + if (!issueNumber) { + console.error("Usage: node check-proposal-status.js "); + process.exit(1); + } + + const githubConfig = { + token: process.env.GITHUB_TOKEN, + owner: process.env.GITHUB_REPOSITORY_OWNER, + repo: process.env.GITHUB_REPOSITORY_NAME, + baseBranch: "main", + }; + + try { + const game = new NomicGame(githubConfig, defaultGameConfig); + const result = await game.checkProposalStatus(parseInt(issueNumber)); + + console.log("Proposal Status:", result.status); + console.log("Can Merge:", result.canMerge); + if (result.reason) { + console.log("Reason:", result.reason); + } + + if (result.canMerge) { + console.log("āœ… Proposal is ready to merge!"); + process.exit(0); + } else { + console.log("āŒ Proposal is not ready to merge"); + process.exit(1); + } + } catch (error) { + console.error("Error checking proposal status:", error); + process.exit(1); + } +} + +checkProposalStatus(); diff --git a/scripts/extract-issue-number.js b/scripts/extract-issue-number.js new file mode 100644 index 0000000..dc3b613 --- /dev/null +++ b/scripts/extract-issue-number.js @@ -0,0 +1,31 @@ +const fs = require("node:fs"); + +function extractIssueNumber() { + const prTitle = process.env.PR_TITLE || ""; + const prBody = process.env.PR_BODY || ""; + + // Extract issue number from PR title (assuming format: "Proposal #123: Title") + let issueNumber = prTitle.match(/#(\d+)/)?.[1]; + + if (!issueNumber) { + // Try to extract from PR body + issueNumber = prBody.match(/#(\d+)/)?.[1]; + } + + if (!issueNumber) { + console.error("No issue number found in PR title or body"); + console.error("PR_TITLE:", prTitle); + console.error("PR_BODY:", prBody); + process.exit(1); + } + + console.log("Issue number:", issueNumber); + + // Write to GitHub Actions output file + const outputFile = process.env.GITHUB_OUTPUT; + if (outputFile) { + fs.appendFileSync(outputFile, `issue_number=${issueNumber}\n`); + } +} + +extractIssueNumber(); diff --git a/scripts/generate-rules-summary.js b/scripts/generate-rules-summary.js new file mode 100644 index 0000000..0a07181 --- /dev/null +++ b/scripts/generate-rules-summary.js @@ -0,0 +1,193 @@ +const fs = require("node:fs"); +const path = require("node:path"); + +function parseRuleFile(filePath) { + const content = fs.readFileSync(filePath, "utf-8"); + + // Extract frontmatter + const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/); + if (!frontmatterMatch) { + return null; + } + + const frontmatter = frontmatterMatch[1]; + const ruleContent = content.replace(/^---\n[\s\S]*?\n---\n/, ""); + + // Parse frontmatter + const metadata = {}; + frontmatter.split("\n").forEach((line) => { + const [key, ...valueParts] = line.split(":"); + if (key && valueParts.length > 0) { + metadata[key.trim()] = valueParts.join(":").trim(); + } + }); + + // Extract title from content + const titleMatch = ruleContent.match(/^# (.*)$/m); + const title = titleMatch ? titleMatch[1] : path.basename(filePath, ".md"); + + // Extract rule text (everything after the title, before copyright) + let ruleText = ruleContent.replace(/^# .*$/m, "").trim(); + const copyrightIndex = ruleText.indexOf("# Copyright"); + if (copyrightIndex !== -1) { + ruleText = ruleText.substring(0, copyrightIndex).trim(); + } + + return { + id: metadata.RULE || path.basename(filePath, ".md"), + title, + author: metadata.Author || "Unknown", + status: metadata.Status || "Unknown", + type: metadata.Type || "Unknown", + tags: metadata.Tags ? metadata.Tags.split(",").map((t) => t.trim()) : [], + content: ruleText, + filename: path.basename(filePath), + }; +} + +function linkToRule(rule) { + return `[${rule.id}](rules/${rule.filename})`; +} + +function codeTag(tag) { + return `\`${tag}\``; +} + +function linkTag(tag) { + return `[\`${tag}\`](#tag-${tag.replace(/[^a-zA-Z0-9]/g, "").toLowerCase()})`; +} + +function getRecentUpdateBadge(ruleId) { + const recentRules = { + 308: " [#46](https://github.com/mburns/nomic/pull/46)", + 309: " [#46](https://github.com/mburns/nomic/pull/46)", + 101: " [#40](https://github.com/mburns/nomic/pull/40)", + }; + return recentRules[ruleId] || ""; +} + +function generateRulesSummary() { + const rulesDir = path.join(__dirname, "..", "rules"); + const outputFile = path.join(__dirname, "..", "RULES_SUMMARY.md"); + + if (!fs.existsSync(rulesDir)) { + console.error("Rules directory not found:", rulesDir); + process.exit(1); + } + + const ruleFiles = fs + .readdirSync(rulesDir) + .filter((file) => file.endsWith(".md")) + .sort((a, b) => { + // Extract rule numbers for sorting + const aNum = parseInt(a.match(/rule(\d+)\.md/)?.[1] || "0"); + const bNum = parseInt(b.match(/rule(\d+)\.md/)?.[1] || "0"); + return aNum - bNum; + }); + + const rules = []; + const errors = []; + + ruleFiles.forEach((file) => { + try { + const rule = parseRuleFile(path.join(rulesDir, file)); + if (rule) { + rules.push(rule); + } else { + errors.push(`Failed to parse ${file}: No frontmatter found`); + } + } catch (error) { + errors.push(`Failed to parse ${file}: ${error.message}`); + } + }); + + // Collect tag info + const tagMap = {}; + rules.forEach((rule) => { + rule.tags.forEach((tag) => { + if (!tagMap[tag]) tagMap[tag] = []; + tagMap[tag].push(rule); + }); + }); + const allTags = Object.keys(tagMap).sort(); + + // Statistics + const stats = { + total: rules.length, + immutable: rules.filter((r) => r.type === "Immutable").length, + mutable: rules.filter((r) => r.type === "Mutable").length, + active: rules.filter((r) => r.status === "Accepted").length, + draft: rules.filter((r) => r.status === "Draft").length, + tags: allTags.length, + authors: new Set(rules.map((r) => r.author)).size, + }; + + // Generate summary content + let summary = `# Nomic Rules Summary + +This document provides a comprehensive overview of all rules in the Nomic game. This file is automatically generated from the \`rules/\` directory. + +## Rule Statistics + +| Statistic | Count | +|-------------------|-------| +| Total Rules | ${stats.total} | +| Immutable Rules | ${stats.immutable} | +| Mutable Rules | ${stats.mutable} | +| Active Rules | ${stats.active} | +| Draft Rules | ${stats.draft} | +| Unique Tags | ${stats.tags} | +| Unique Authors | ${stats.authors} | + +## Tag Index + +${allTags.length === 0 ? "No tags found." : allTags.map((tag) => `- ${codeTag(tag)} (${tagMap[tag].length})`).join("\n")} + +## Detailed Rule List + +`; + + // Add each rule with full details + rules.forEach((rule) => { + summary += `### Rule ${linkToRule(rule)}: ${rule.title}${getRecentUpdateBadge(rule.id)} + +**Type**: ${rule.type} +**Status**: ${rule.status} +**Author**: ${rule.author} +**Tags**: ${rule.tags.length ? rule.tags.map(linkTag).join(", ") : "None"} + +${rule.content} + +--- +`; + }); + + // Add any parsing errors + if (errors.length > 0) { + summary += `\n## Parsing Errors + +The following files could not be parsed: + +${errors.map((error) => `- ${error}`).join("\n")} +`; + } + + // Add footer + summary += ` +--- + +*This file was automatically generated on ${new Date().toISOString()}. Any changes to the \`rules/\` directory will regenerate this file.* +`; + + // Write the summary file + fs.writeFileSync(outputFile, summary); + console.log(`Generated rules summary: ${outputFile}`); + console.log(`Processed ${rules.length} rules`); + + if (errors.length > 0) { + console.warn(`Warnings: ${errors.length} files had parsing issues`); + } +} + +// Run the generator +generateRulesSummary(); diff --git a/scripts/migrate-to-event-log.js b/scripts/migrate-to-event-log.js new file mode 100644 index 0000000..fa0e093 --- /dev/null +++ b/scripts/migrate-to-event-log.js @@ -0,0 +1,28 @@ +#!/usr/bin/env node + +const { runMigration } = require('../dist/scripts/migrate-to-event-log'); + +async function main() { + const args = process.argv.slice(2); + + console.log('šŸ”„ Starting migration from YAML state to event log...'); + console.log('Args:', args); + + const options = { + dryRun: args.includes('--dry-run'), + backupExisting: !args.includes('--no-backup'), + }; + + console.log('Migration options:', options); + + try { + await runMigration(options); + } catch (error) { + console.error('Migration failed:', error); + process.exit(1); + } +} + +if (require.main === module) { + main(); +} \ No newline at end of file diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..6518f18 --- /dev/null +++ b/src/index.ts @@ -0,0 +1,591 @@ +import { EventLogService } from "./services/EventLogService"; +import { StateProcessor } from "./services/StateProcessor"; +import { GitHubService } from "./services/GitHubService"; +import { + formatProposalStatus, + formatVoteInstructions, + parseVoteFromComment, + generateVoteTable, +} from "./services/VoteProcessor"; +import type { GameConfig, GitHubConfig } from "./types"; +import type { ComputedGameState } from "./types/events"; + +export class NomicGame { + public githubService: GitHubService; + private eventLogService: EventLogService; + private stateProcessor: StateProcessor; + private config: GameConfig; + + constructor(githubConfig: GitHubConfig, gameConfig: GameConfig) { + this.githubService = new GitHubService(githubConfig); + this.eventLogService = new EventLogService("events/game-events.jsonl", gameConfig); + this.stateProcessor = new StateProcessor(this.eventLogService); + this.config = gameConfig; + } + + async processIssueComment(eventPath: string): Promise { + try { + const event = this.githubService.parseEvent(eventPath); + const issue = event.issue; + + // Get all comments for this issue + const comments = await this.githubService.getIssueComments(issue.number); + + if (comments.length === 0) { + console.log(`No comments found for issue #${issue.number}`); + return; + } + + // Process the latest comment + const latestComment = comments[comments.length - 1]; + const vote = parseVoteFromComment(latestComment); + + if (!vote) { + console.log( + `No valid vote found in comment for issue #${issue.number}`, + ); + return; + } + + // Check if proposal exists, if not create it + const currentState = await this.stateProcessor.computeCurrentState(); + const proposalKey = issue.number.toString(); + + if (!currentState.proposals[proposalKey]) { + // Create proposal_created event + const proposalCreatedEvent = { + type: "proposal_created" as const, + issue_number: issue.number, + title: issue.title, + proposer: issue.user.login, + body: issue.body, + }; + await this.eventLogService.appendEvent(proposalCreatedEvent); + + // Ensure proposer is registered as a player + if (!currentState.players[issue.user.login]) { + const playerJoinedEvent = { + type: "player_joined" as const, + username: issue.user.login, + joined_via: "proposal" as const, + first_action_issue: issue.number, + }; + await this.eventLogService.appendEvent(playerJoinedEvent); + } + } + + // Ensure voter is registered as a player + if (!currentState.players[vote.voter]) { + const voterJoinedEvent = { + type: "player_joined" as const, + username: vote.voter, + joined_via: "vote" as const, + first_action_issue: issue.number, + }; + await this.eventLogService.appendEvent(voterJoinedEvent); + } + + // Create vote_cast event + const voteCastEvent = { + type: "vote_cast" as const, + issue_number: issue.number, + voter: vote.voter, + vote: vote.vote, + comment_id: latestComment.id, + }; + await this.eventLogService.appendEvent(voteCastEvent); + + // Check if proposal should be resolved + const mergeCheck = await this.stateProcessor.canMergeProposal(issue.number, { + requiredVotes: this.config.requiredVotes, + requiredMajority: this.config.requiredMajority, + }); + + let statusChanged = false; + const updatedState = await this.stateProcessor.computeCurrentState(); + const proposal = updatedState.proposals[proposalKey]; + + if (proposal) { + // Determine if proposal should be resolved + const votes = Object.values(proposal.votes); + const totalVotes = votes.length; + const forVotes = votes.filter(v => v === "FOR").length; + const againstVotes = votes.filter(v => v === "AGAINST").length; + + if (totalVotes >= this.config.requiredVotes) { + const majority = forVotes / totalVotes; + const newStatus = majority >= this.config.requiredMajority ? "passed" : "failed"; + + if (proposal.status === "open" && newStatus !== "open") { + // Create proposal_resolved event + await this.eventLogService.appendEvent({ + type: "proposal_resolved" as const, + issue_number: issue.number, + status: newStatus, + final_votes: { + for: forVotes, + against: againstVotes, + total: totalVotes, + }, + }); + + // Award points + await this.awardPoints(issue.number, newStatus, proposal.proposer, proposal.votes); + + // Advance turn + await this.eventLogService.appendEvent({ + type: "turn_advanced" as const, + new_turn: updatedState.current_turn + 1, + triggered_by_issue: issue.number, + }); + + statusChanged = true; + } + } + } + + // Update issue status and labels + await this.updateIssueStatus(issue.number); + + // Update scoreboard + await this.updateScoreboard(); + + // Update GitHub status check + await this.updateProposalStatusCheck(issue.number); + + // Update PR body with vote information + await this.updatePullRequestBody(issue.number); + + // If status changed, add a comment with the new status + if (statusChanged) { + const finalState = await this.stateProcessor.computeCurrentState(); + const finalProposal = finalState.proposals[proposalKey]; + if (finalProposal) { + const statusMessage = formatProposalStatus({ + ...finalProposal, + mergeCheck, + }); + await this.githubService.addComment(issue.number, statusMessage); + } + } + + console.log( + `Processed ${vote.vote} vote from @${vote.voter} for issue #${issue.number}`, + ); + } catch (error) { + console.error("Error processing issue comment:", error); + throw error; + } + } + + private async awardPoints( + issueNumber: number, + status: "passed" | "failed", + proposer: string, + votes: Record + ): Promise { + // Award points to proposer + if (status === "passed") { + await this.eventLogService.appendEvent({ + type: "points_awarded" as const, + player: proposer, + points: this.config.pointsForSuccessfulProposal, + reason: "Successful proposal", + related_issue: issueNumber, + }); + } else { + await this.eventLogService.appendEvent({ + type: "points_awarded" as const, + player: proposer, + points: this.config.pointsForParticipation, + reason: "Proposal participation", + related_issue: issueNumber, + }); + } + + // Award points to voters + for (const [voter, vote] of Object.entries(votes)) { + if (voter === proposer) continue; // Proposer already got points + + const votedWithMajority = + (status === "passed" && vote === "FOR") || + (status === "failed" && vote === "AGAINST"); + + const points = votedWithMajority + ? this.config.pointsForVotingWithMajority + : this.config.pointsForParticipation; + + const reason = votedWithMajority + ? "Voting with majority" + : "Voting participation"; + + await this.eventLogService.appendEvent({ + type: "points_awarded" as const, + player: voter, + points, + reason, + related_issue: issueNumber, + }); + } + } + + async processIssueOpened(eventPath: string): Promise { + try { + const event = this.githubService.parseEvent(eventPath); + const issue = event.issue; + + // Create proposal_created event + await this.eventLogService.appendEvent({ + type: "proposal_created" as const, + issue_number: issue.number, + title: issue.title, + proposer: issue.user.login, + body: issue.body, + }); + + // Ensure proposer is registered as a player + const currentState = await this.stateProcessor.computeCurrentState(); + if (!currentState.players[issue.user.login]) { + await this.eventLogService.appendEvent({ + type: "player_joined" as const, + username: issue.user.login, + joined_via: "proposal" as const, + first_action_issue: issue.number, + }); + } + + // Add voting instructions to the issue + const instructions = formatVoteInstructions(); + await this.githubService.addComment(issue.number, instructions); + + // Add proposal label + await this.githubService.addLabels(issue.number, ["proposal"]); + + // Create initial pending status check + await this.updateProposalStatusCheck(issue.number); + + console.log(`Added voting instructions to issue #${issue.number}`); + } catch (error) { + console.error("Error processing issue opened:", error); + throw error; + } + } + + async processIssueClosed(eventPath: string): Promise { + try { + const event = this.githubService.parseEvent(eventPath); + const issue = event.issue; + + // Create proposal_resolved event if not already resolved + const currentState = await this.stateProcessor.computeCurrentState(); + const proposal = currentState.proposals[issue.number.toString()]; + + if (proposal && proposal.status === "open") { + const votes = Object.values(proposal.votes); + const forVotes = votes.filter(v => v === "FOR").length; + const againstVotes = votes.filter(v => v === "AGAINST").length; + + await this.eventLogService.appendEvent({ + type: "proposal_resolved" as const, + issue_number: issue.number, + status: "closed" as const, + final_votes: { + for: forVotes, + against: againstVotes, + total: forVotes + againstVotes, + }, + }); + } + + // Remove proposal label + await this.githubService.removeLabel(issue.number, "proposal"); + + console.log(`Closed proposal for issue #${issue.number}`); + } catch (error) { + console.error("Error processing issue closed:", error); + throw error; + } + } + + async checkProposalStatus(issueNumber: number): Promise<{ + canMerge: boolean; + reason?: string; + status: string; + }> { + const mergeCheck = await this.stateProcessor.canMergeProposal(issueNumber, { + requiredVotes: this.config.requiredVotes, + requiredMajority: this.config.requiredMajority, + }); + + const proposal = await this.stateProcessor.getProposalState(issueNumber); + + return { + canMerge: mergeCheck.canMerge, + reason: mergeCheck.reason, + status: proposal?.status || "unknown", + }; + } + + async autoMergeProposal(issueNumber: number): Promise { + const mergeCheck = await this.stateProcessor.canMergeProposal(issueNumber, { + requiredVotes: this.config.requiredVotes, + requiredMajority: this.config.requiredMajority, + }); + + if (!mergeCheck.canMerge) { + throw new Error(`Cannot merge proposal: ${mergeCheck.reason}`); + } + + // Find the associated PR + const _issue = await this.githubService.getIssue(issueNumber); + + // In a real implementation, you'd need to find the PR associated with this issue + // For now, we'll assume the PR number is the same as the issue number + try { + await this.githubService.mergePullRequest(issueNumber); + console.log(`Auto-merged PR #${issueNumber}`); + } catch (error) { + console.error(`Failed to auto-merge PR #${issueNumber}:`, error); + throw error; + } + } + + private async updateIssueStatus(issueNumber: number): Promise { + const proposal = await this.stateProcessor.getProposalState(issueNumber); + if (!proposal) return; + + // Update labels based on status + const currentLabels = ["proposal"]; + + if (proposal.status === "passed") { + currentLabels.push("passed"); + await this.githubService.removeLabel(issueNumber, "failed"); + } else if (proposal.status === "failed") { + currentLabels.push("failed"); + await this.githubService.removeLabel(issueNumber, "passed"); + } else { + await this.githubService.removeLabel(issueNumber, "passed"); + await this.githubService.removeLabel(issueNumber, "failed"); + } + + // Update the labels + await this.githubService.addLabels(issueNumber, currentLabels); + } + + private async updateScoreboard(): Promise { + const leaderboard = await this.stateProcessor.getPlayerLeaderboard(); + const gameState = await this.stateProcessor.computeCurrentState(); + const proposalStats = await this.stateProcessor.getProposalStats(); + + // Generate scoreboard content + let scoreboard = "# Nomic Game Scoreboard\n\n"; + scoreboard += "Welcome to the Nomic game! This scoreboard tracks player participation, proposals, and voting activity.\n\n"; + scoreboard += "## Current Standings\n\n"; + scoreboard += "User | Points | Proposals | Passed | Votes Cast | Last Active\n"; + scoreboard += "---- | ------ | --------- | ------ | ---------- | ------------\n"; + + for (const player of leaderboard) { + const lastActive = player.last_active ? new Date(player.last_active).toLocaleDateString() : "Never"; + scoreboard += `@${player.username} | ${player.points} | ${player.proposals_submitted} | ${player.proposals_passed} | ${player.votes_cast} | ${lastActive}\n`; + } + + scoreboard += "\n## Game Statistics\n\n"; + scoreboard += `- **Total Players**: ${leaderboard.length}\n`; + scoreboard += `- **Total Proposals**: ${proposalStats.total}\n`; + scoreboard += `- **Active Proposals**: ${proposalStats.open}\n`; + scoreboard += `- **Current Turn**: ${gameState.current_turn}\n`; + + scoreboard += "\n## How to Play\n\n"; + scoreboard += "1. **Create a Proposal**: Open a GitHub Issue with your rule change proposal\n"; + scoreboard += "2. **Vote on Proposals**: Comment on issues using formats like `VOTE: FOR` or `I vote AGAINST`\n"; + scoreboard += "3. **Track Progress**: Watch the automated updates as votes are processed\n"; + scoreboard += "4. **Earn Points**: Get points for successful proposals and voting with the majority\n"; + + scoreboard += "\n## Voting Formats\n\n"; + scoreboard += "- `VOTE: FOR` or `VOTE: AGAINST`\n"; + scoreboard += "- `I vote FOR` or `I vote AGAINST`\n"; + scoreboard += "- `My vote is FOR` or `My vote is AGAINST`\n"; + scoreboard += "- `Voting FOR` or `Voting AGAINST`\n"; + scoreboard += "- `āœ… FOR` or `āŒ AGAINST`\n"; + + scoreboard += "\n## Scoring System\n\n"; + scoreboard += `- **Successful Proposal**: ${this.config.pointsForSuccessfulProposal} points\n`; + scoreboard += `- **Voting with Majority**: ${this.config.pointsForVotingWithMajority} points\n`; + scoreboard += `- **Participation**: ${this.config.pointsForParticipation} point\n`; + + scoreboard += "\n*This scoreboard is automatically updated by the Nomic game system.*\n"; + + // Update the scoreboard file in the repository + const existingFile = await this.githubService.getFile("SCOREBOARD.md"); + + if (existingFile) { + await this.githubService.updateFile( + "SCOREBOARD.md", + "Update scoreboard", + scoreboard, + existingFile.sha, + ); + } else { + await this.githubService.createFile( + "SCOREBOARD.md", + "Create scoreboard", + scoreboard, + ); + } + } + + async getGameStats(): Promise<{ + totalPlayers: number; + totalProposals: number; + activeProposals: number; + currentTurn: number; + }> { + const state = await this.stateProcessor.computeCurrentState(); + const proposalStats = await this.stateProcessor.getProposalStats(); + + return { + totalPlayers: Object.keys(state.players).length, + totalProposals: proposalStats.total, + activeProposals: proposalStats.open, + currentTurn: state.current_turn, + }; + } + + private async updateProposalStatusCheck(issueNumber: number): Promise { + try { + const mergeCheck = await this.stateProcessor.canMergeProposal(issueNumber, { + requiredVotes: this.config.requiredVotes, + requiredMajority: this.config.requiredMajority, + }); + const proposal = await this.stateProcessor.getProposalState(issueNumber); + + if (!proposal) { + console.log(`No proposal found for issue #${issueNumber}`); + return; + } + + // Get the commit SHA for the status check + const sha = await this.githubService.getCommitSha(issueNumber); + if (!sha) { + console.log(`Could not get commit SHA for issue #${issueNumber}`); + return; + } + + const context = `nomic/proposal-${issueNumber}`; + let state: "pending" | "success" | "failure"; + let description: string; + + if (mergeCheck.canMerge) { + state = "success"; + description = `Proposal ready to merge (${mergeCheck.forVotes}/${mergeCheck.currentVotes} votes, ${(mergeCheck.majority * 100).toFixed(1)}% majority)`; + } else if (proposal.status === "failed") { + state = "failure"; + description = "Proposal failed"; + } else if (proposal.status === "closed") { + state = "failure"; + description = "Proposal closed"; + } else { + state = "pending"; + description = mergeCheck.reason || "Waiting for votes"; + } + + await this.githubService.createStatusCheck(sha, context, state, description); + } catch (error) { + console.error(`Failed to update status check for issue #${issueNumber}:`, error); + } + } + + private async updatePullRequestBody(issueNumber: number): Promise { + try { + const proposal = await this.stateProcessor.getProposalState(issueNumber); + if (!proposal) return; + + const mergeCheck = await this.stateProcessor.canMergeProposal(issueNumber, { + requiredVotes: this.config.requiredVotes, + requiredMajority: this.config.requiredMajority, + }); + + // Generate vote table + const voteTable = generateVoteTable(proposal.votes); + + // Create status section + const statusSection = formatProposalStatus({ + ...proposal, + mergeCheck, + }); + + // Try to update the PR body + const pr = await this.githubService.getPullRequest(issueNumber); + if (pr) { + let newBody = pr.body || ""; + + // Remove existing vote table and status if present + newBody = newBody.replace(/## Vote Status[\s\S]*?(?=##|$)/g, ""); + newBody = newBody.replace(/## Voting Results[\s\S]*?(?=##|$)/g, ""); + + // Add new vote information + newBody += "\n\n## Vote Status\n\n"; + newBody += statusSection; + newBody += "\n\n## Voting Results\n\n"; + newBody += voteTable; + + await this.githubService.updatePullRequest(issueNumber, { + body: newBody.trim(), + }); + } + } catch (error) { + console.error(`Failed to update PR body for issue #${issueNumber}:`, error); + } + } +} + +export async function main(): Promise { + const githubConfig = { + token: process.env.GITHUB_TOKEN || "", + owner: process.env.GITHUB_REPOSITORY_OWNER || "", + repo: process.env.GITHUB_REPOSITORY?.split("/")[1] || "", + baseBranch: "main", + }; + + const gameConfig = { + requiredVotes: 3, + requiredMajority: 0.5, + pointsForSuccessfulProposal: 3, + pointsForVotingWithMajority: 2, + pointsForParticipation: 1, + autoMergeEnabled: true, + branchProtectionEnabled: true, + }; + + const game = new NomicGame(githubConfig, gameConfig); + const eventName = process.env.GITHUB_EVENT_NAME; + const eventPath = process.env.GITHUB_EVENT_PATH || ""; + + console.log(`Processing GitHub event: ${eventName}`); + + try { + switch (eventName) { + case "issue_comment": + await game.processIssueComment(eventPath); + break; + case "issues": { + const event = game.githubService.parseEvent(eventPath); + if (event.action === "opened") { + await game.processIssueOpened(eventPath); + } else if (event.action === "closed") { + await game.processIssueClosed(eventPath); + } + break; + } + default: + console.log(`Unhandled event type: ${eventName}`); + } + } catch (error) { + console.error("Error processing event:", error); + process.exit(1); + } +} + +if (require.main === module) { + main().catch(console.error); +} diff --git a/src/scripts/migrate-to-event-log.ts b/src/scripts/migrate-to-event-log.ts new file mode 100644 index 0000000..99c70f2 --- /dev/null +++ b/src/scripts/migrate-to-event-log.ts @@ -0,0 +1,336 @@ +import * as fs from "node:fs"; +import * as yaml from "js-yaml"; +import { EventLogService } from "../services/EventLogService"; +import { StateProcessor } from "../services/StateProcessor"; +import { GameState, GameStateSchema, type GameConfig } from "../types"; + +interface MigrationOptions { + yamlStatePath: string; + eventLogPath: string; + gameConfig: GameConfig; + dryRun: boolean; + backupExisting: boolean; +} + +export class StateToEventLogMigrator { + private options: MigrationOptions; + private eventLogService: EventLogService; + private stateProcessor: StateProcessor; + + constructor(options: MigrationOptions) { + this.options = options; + this.eventLogService = new EventLogService(options.eventLogPath, options.gameConfig); + this.stateProcessor = new StateProcessor(this.eventLogService); + } + + async migrate(): Promise<{ + success: boolean; + eventsCreated: number; + errors: string[]; + warnings: string[]; + }> { + const errors: string[] = []; + const warnings: string[] = []; + let eventsCreated = 0; + + try { + console.log("Starting migration from YAML state to event log..."); + + // Check if source file exists + if (!fs.existsSync(this.options.yamlStatePath)) { + return { + success: false, + eventsCreated: 0, + errors: [`Source YAML file not found: ${this.options.yamlStatePath}`], + warnings: [], + }; + } + + // Backup existing event log if requested + if (this.options.backupExisting) { + try { + await this.eventLogService.createBackup(); + } catch (error) { + warnings.push(`Failed to create backup: ${error}`); + } + } + + // Load and validate YAML state + const yamlContent = fs.readFileSync(this.options.yamlStatePath, "utf-8"); + const rawState = yaml.load(yamlContent) as unknown; + const gameState = GameStateSchema.parse(rawState); + + console.log(`Loaded game state with ${Object.keys(gameState.players).length} players and ${Object.keys(gameState.proposals).length} proposals`); + + // Create game_started event + if (!this.options.dryRun) { + const initialRules = gameState.gameRules.length > 0 ? gameState.gameRules : [ + "101", "102", "103", "104", "106", "107", "108", "109", "110", "111", "112", "113", "114", "115", "116", + "202", "204", "205", "206", "208", "209", "210", "211", "212", "213" + ]; + + const gameStartedEvent = { + type: "game_started" as const, + initial_rules: initialRules, + config: { + required_votes: this.options.gameConfig.requiredVotes, + required_majority: this.options.gameConfig.requiredMajority, + points_for_successful_proposal: this.options.gameConfig.pointsForSuccessfulProposal, + points_for_voting_with_majority: this.options.gameConfig.pointsForVotingWithMajority, + points_for_participation: this.options.gameConfig.pointsForParticipation, + }, + }; + + await this.eventLogService.appendEvent(gameStartedEvent); + eventsCreated++; + } + + // Create player_joined events for all players + for (const [username, player] of Object.entries(gameState.players)) { + if (!this.options.dryRun) { + const playerJoinedEvent = { + type: "player_joined" as const, + username, + joined_via: "proposal" as const, // Assume they joined via proposal + }; + await this.eventLogService.appendEvent(playerJoinedEvent); + eventsCreated++; + } + } + + // Process proposals in chronological order + const proposalEntries = Object.entries(gameState.proposals).sort(([, a], [, b]) => + new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime() + ); + + for (const [issueNumberStr, proposal] of proposalEntries) { + const issueNumber = parseInt(issueNumberStr, 10); + + // Create proposal_created event + if (!this.options.dryRun) { + const proposalCreatedEvent = { + type: "proposal_created" as const, + issue_number: issueNumber, + title: proposal.title, + proposer: proposal.proposer, + }; + await this.eventLogService.appendEvent(proposalCreatedEvent); + eventsCreated++; + } + + // Create vote_cast events + for (const [voter, vote] of Object.entries(proposal.votes)) { + if (!this.options.dryRun) { + const voteCastEvent = { + type: "vote_cast" as const, + issue_number: issueNumber, + voter, + vote: vote as "FOR" | "AGAINST", + }; + await this.eventLogService.appendEvent(voteCastEvent); + eventsCreated++; + } + } + + // Create proposal_resolved event if not open + if (proposal.status !== "open") { + const votes = Object.values(proposal.votes); + const forVotes = votes.filter(v => v === "FOR").length; + const againstVotes = votes.filter(v => v === "AGAINST").length; + + if (!this.options.dryRun) { + const proposalResolvedEvent = { + type: "proposal_resolved" as const, + issue_number: issueNumber, + status: proposal.status as "passed" | "failed" | "closed", + final_votes: { + for: forVotes, + against: againstVotes, + total: forVotes + againstVotes, + }, + }; + await this.eventLogService.appendEvent(proposalResolvedEvent); + eventsCreated++; + } + } + } + + // Create points_awarded events based on player points + for (const [username, player] of Object.entries(gameState.players)) { + if (player.points > 0) { + if (!this.options.dryRun) { + const pointsAwardedEvent = { + type: "points_awarded" as const, + player: username, + points: player.points, + reason: "Migrated from existing game state", + }; + await this.eventLogService.appendEvent(pointsAwardedEvent); + eventsCreated++; + } + } + } + + // Create turn_advanced event if not at turn 1 + if (gameState.currentTurn > 1) { + if (!this.options.dryRun) { + const turnAdvancedEvent = { + type: "turn_advanced" as const, + new_turn: gameState.currentTurn, + triggered_by_issue: 0, // Unknown which issue triggered it + }; + await this.eventLogService.appendEvent(turnAdvancedEvent); + eventsCreated++; + } + } + + // Validate the migrated state + if (!this.options.dryRun) { + const validation = await this.validateMigration(gameState); + errors.push(...validation.errors); + warnings.push(...validation.warnings); + } + + console.log(`Migration completed. Created ${eventsCreated} events.`); + + return { + success: errors.length === 0, + eventsCreated, + errors, + warnings, + }; + + } catch (error) { + console.error("Migration failed:", error); + return { + success: false, + eventsCreated, + errors: [`Migration failed: ${error}`], + warnings, + }; + } + } + + private async validateMigration(originalState: GameState): Promise<{ + errors: string[]; + warnings: string[]; + }> { + const errors: string[] = []; + const warnings: string[] = []; + + try { + // Compute state from events + const computedState = await this.stateProcessor.computeCurrentState(); + + // Compare player counts + const originalPlayerCount = Object.keys(originalState.players).length; + const computedPlayerCount = Object.keys(computedState.players).length; + if (originalPlayerCount !== computedPlayerCount) { + errors.push(`Player count mismatch: original ${originalPlayerCount}, computed ${computedPlayerCount}`); + } + + // Compare proposal counts + const originalProposalCount = Object.keys(originalState.proposals).length; + const computedProposalCount = Object.keys(computedState.proposals).length; + if (originalProposalCount !== computedProposalCount) { + errors.push(`Proposal count mismatch: original ${originalProposalCount}, computed ${computedProposalCount}`); + } + + // Compare player points (within tolerance due to rounding) + for (const [username, originalPlayer] of Object.entries(originalState.players)) { + const computedPlayer = computedState.players[username]; + if (!computedPlayer) { + errors.push(`Player ${username} missing from computed state`); + continue; + } + + const pointsDiff = Math.abs(originalPlayer.points - computedPlayer.points); + if (pointsDiff > 0.01) { // Allow small floating point differences + warnings.push(`Player ${username} points differ: original ${originalPlayer.points}, computed ${computedPlayer.points}`); + } + } + + // Compare proposal statuses + for (const [issueNumberStr, originalProposal] of Object.entries(originalState.proposals)) { + const computedProposal = computedState.proposals[issueNumberStr]; + if (!computedProposal) { + errors.push(`Proposal ${issueNumberStr} missing from computed state`); + continue; + } + + if (originalProposal.status !== computedProposal.status) { + errors.push(`Proposal ${issueNumberStr} status differs: original ${originalProposal.status}, computed ${computedProposal.status}`); + } + } + + // Compare current turn + if (originalState.currentTurn !== computedState.current_turn) { + warnings.push(`Current turn differs: original ${originalState.currentTurn}, computed ${computedState.current_turn}`); + } + + } catch (error) { + errors.push(`Validation failed: ${error}`); + } + + return { errors, warnings }; + } +} + +// CLI interface +export async function runMigration(options: Partial = {}): Promise { + const defaultConfig: GameConfig = { + requiredVotes: 3, + requiredMajority: 0.5, + pointsForSuccessfulProposal: 3, + pointsForVotingWithMajority: 2, + pointsForParticipation: 1, + autoMergeEnabled: true, + branchProtectionEnabled: true, + }; + + const migrationOptions: MigrationOptions = { + yamlStatePath: options.yamlStatePath || "game-state.yaml", + eventLogPath: options.eventLogPath || "events/game-events.jsonl", + gameConfig: options.gameConfig || defaultConfig, + dryRun: options.dryRun || false, + backupExisting: options.backupExisting !== false, // Default to true + }; + + const migrator = new StateToEventLogMigrator(migrationOptions); + const result = await migrator.migrate(); + + if (result.success) { + console.log("āœ… Migration completed successfully!"); + console.log(` Events created: ${result.eventsCreated}`); + if (result.warnings.length > 0) { + console.log("āš ļø Warnings:"); + for (const warning of result.warnings) { + console.log(` - ${warning}`); + } + } + } else { + console.error("āŒ Migration failed!"); + for (const error of result.errors) { + console.error(` - ${error}`); + } + if (result.warnings.length > 0) { + console.log("āš ļø Warnings:"); + for (const warning of result.warnings) { + console.log(` - ${warning}`); + } + } + process.exit(1); + } +} + +// Run migration if called directly +if (require.main === module) { + const args = process.argv.slice(2); + const dryRun = args.includes("--dry-run"); + const noBackup = args.includes("--no-backup"); + + runMigration({ + dryRun, + backupExisting: !noBackup, + }).catch(console.error); +} \ No newline at end of file diff --git a/src/services/EventLogService.ts b/src/services/EventLogService.ts new file mode 100644 index 0000000..d87872c --- /dev/null +++ b/src/services/EventLogService.ts @@ -0,0 +1,288 @@ +import * as fs from "node:fs"; +import * as path from "node:path"; +import { randomUUID } from "node:crypto"; +import { + GameEvent, + GameEventSchema, + EventLogEntry, +} from "../types/events"; +import type { GameConfig } from "../types"; + +export class EventLogService { + private eventLogPath: string; + private config: GameConfig; + + constructor(eventLogPath: string = "events/game-events.jsonl", config: GameConfig) { + this.eventLogPath = eventLogPath; + this.config = config; + this.ensureEventLogExists(); + } + + /** + * Append a new event to the event log + */ + async appendEvent( + eventData: Omit, + metadata?: EventLogEntry["metadata"] + ): Promise { + const event: GameEvent = { + ...eventData, + id: randomUUID(), + timestamp: new Date().toISOString(), + } as GameEvent; + + // Validate the event + const validatedEvent = GameEventSchema.parse(event); + + const logEntry: EventLogEntry = { + event: validatedEvent, + metadata, + }; + + // Append to JSONL file + const jsonLine = JSON.stringify(logEntry) + "\n"; + await fs.promises.appendFile(this.eventLogPath, jsonLine, "utf-8"); + + console.log(`Event appended: ${event.type} (${event.id})`); + return validatedEvent; + } + + /** + * Read all events from the event log + */ + async readAllEvents(): Promise { + try { + const content = await fs.promises.readFile(this.eventLogPath, "utf-8"); + const lines = content.trim().split("\n").filter(line => line.trim()); + + return lines.map(line => { + try { + return JSON.parse(line) as EventLogEntry; + } catch (error) { + console.error(`Failed to parse event log line: ${line}`, error); + throw new Error(`Invalid event log format: ${error}`); + } + }); + } catch (error) { + if ((error as NodeJS.ErrnoException).code === "ENOENT") { + return []; + } + throw error; + } + } + + /** + * Read events from a specific point in time + */ + async readEventsSince(timestamp: string): Promise { + const allEvents = await this.readAllEvents(); + return allEvents.filter(entry => entry.event.timestamp > timestamp); + } + + /** + * Read events of a specific type + */ + async readEventsByType( + eventType: T + ): Promise { + const allEvents = await this.readAllEvents(); + return allEvents.filter(entry => entry.event.type === eventType); + } + + /** + * Read events for a specific issue/proposal + */ + async readEventsForIssue(issueNumber: number): Promise { + const allEvents = await this.readAllEvents(); + return allEvents.filter(entry => { + const event = entry.event; + return ( + ("issue_number" in event && event.issue_number === issueNumber) || + ("proposal_issue" in event && event.proposal_issue === issueNumber) || + ("related_issue" in event && event.related_issue === issueNumber) || + ("triggered_by_issue" in event && event.triggered_by_issue === issueNumber) || + ("first_action_issue" in event && event.first_action_issue === issueNumber) + ); + }); + } + + /** + * Read events for a specific player + */ + async readEventsForPlayer(username: string): Promise { + const allEvents = await this.readAllEvents(); + return allEvents.filter(entry => { + const event = entry.event; + return ( + ("proposer" in event && event.proposer === username) || + ("voter" in event && event.voter === username) || + ("player" in event && event.player === username) || + ("username" in event && event.username === username) || + ("author" in event && event.author === username) || + ("repealed_by" in event && event.repealed_by === username) || + ("amended_by" in event && event.amended_by === username) || + ("transmuted_by" in event && event.transmuted_by === username) + ); + }); + } + + /** + * Get the latest event + */ + async getLatestEvent(): Promise { + const events = await this.readAllEvents(); + return events.length > 0 ? events[events.length - 1] : null; + } + + /** + * Get event count + */ + async getEventCount(): Promise { + const events = await this.readAllEvents(); + return events.length; + } + + /** + * Validate the integrity of the event log + */ + async validateEventLog(): Promise<{ + isValid: boolean; + errors: string[]; + eventCount: number; + }> { + const errors: string[] = []; + let eventCount = 0; + + try { + const content = await fs.promises.readFile(this.eventLogPath, "utf-8"); + const lines = content.trim().split("\n").filter(line => line.trim()); + + for (let i = 0; i < lines.length; i++) { + const lineNumber = i + 1; + try { + const entry = JSON.parse(lines[i]) as EventLogEntry; + + // Validate event structure + GameEventSchema.parse(entry.event); + + // Check for duplicate IDs (this would be expensive for large logs) + // In production, you might want to use a more efficient approach + + eventCount++; + } catch (error) { + errors.push(`Line ${lineNumber}: Invalid event format - ${error}`); + } + } + } catch (error) { + if ((error as NodeJS.ErrnoException).code === "ENOENT") { + // File doesn't exist, that's okay + return { isValid: true, errors: [], eventCount: 0 }; + } + errors.push(`Failed to read event log: ${error}`); + } + + return { + isValid: errors.length === 0, + errors, + eventCount, + }; + } + + /** + * Create a backup of the event log + */ + async createBackup(): Promise { + const timestamp = new Date().toISOString().replace(/[:.]/g, "-"); + const backupPath = `${this.eventLogPath}.backup-${timestamp}`; + + try { + await fs.promises.copyFile(this.eventLogPath, backupPath); + console.log(`Event log backup created: ${backupPath}`); + return backupPath; + } catch (error) { + console.error(`Failed to create backup: ${error}`); + throw error; + } + } + + /** + * Initialize the game with a game_started event + */ + async initializeGame(initialRules: string[]): Promise { + const eventData = { + type: "game_started" as const, + initial_rules: initialRules, + config: { + required_votes: this.config.requiredVotes, + required_majority: this.config.requiredMajority, + points_for_successful_proposal: this.config.pointsForSuccessfulProposal, + points_for_voting_with_majority: this.config.pointsForVotingWithMajority, + points_for_participation: this.config.pointsForParticipation, + }, + }; + + const gameStartedEvent = await this.appendEvent(eventData); + return gameStartedEvent; + } + + /** + * Ensure the event log directory and file exist + */ + private ensureEventLogExists(): void { + const dir = path.dirname(this.eventLogPath); + + // Create directory if it doesn't exist + if (!fs.existsSync(dir)) { + fs.mkdirSync(dir, { recursive: true }); + } + + // Create file if it doesn't exist + if (!fs.existsSync(this.eventLogPath)) { + fs.writeFileSync(this.eventLogPath, "", "utf-8"); + } + } + + /** + * Get statistics about the event log + */ + async getEventLogStats(): Promise<{ + totalEvents: number; + eventsByType: Record; + firstEvent?: string; + lastEvent?: string; + uniquePlayers: number; + uniqueProposals: number; + }> { + const events = await this.readAllEvents(); + const eventsByType: Record = {}; + const players = new Set(); + const proposals = new Set(); + + for (const entry of events) { + const event = entry.event; + + // Count by type + eventsByType[event.type] = (eventsByType[event.type] || 0) + 1; + + // Track unique players + if ("proposer" in event) players.add(event.proposer); + if ("voter" in event) players.add(event.voter); + if ("player" in event) players.add(event.player); + if ("username" in event) players.add(event.username); + if ("author" in event) players.add(event.author); + + // Track unique proposals + if ("issue_number" in event) proposals.add(event.issue_number); + if ("proposal_issue" in event) proposals.add(event.proposal_issue); + } + + return { + totalEvents: events.length, + eventsByType, + firstEvent: events.length > 0 ? events[0].event.timestamp : undefined, + lastEvent: events.length > 0 ? events[events.length - 1].event.timestamp : undefined, + uniquePlayers: players.size, + uniqueProposals: proposals.size, + }; + } +} \ No newline at end of file diff --git a/src/services/GameStateService.ts b/src/services/GameStateService.ts new file mode 100644 index 0000000..cd2da7c --- /dev/null +++ b/src/services/GameStateService.ts @@ -0,0 +1,302 @@ +import * as fs from "node:fs"; +import * as yaml from "js-yaml"; +import { + type GameConfig, + type GameState, + GameStateSchema, + type PlayerScore, + type ProposalVotes, + type VoteType, +} from "../types"; + +export class GameStateService { + private statePath: string; + private config: GameConfig; + + constructor(statePath: string = "game-state.yaml", config: GameConfig) { + this.statePath = statePath; + this.config = config; + } + + async loadGameState(): Promise { + try { + if (!fs.existsSync(this.statePath)) { + return this.createInitialGameState(); + } + + const content = fs.readFileSync(this.statePath, "utf-8"); + const data = yaml.load(content) as unknown; + return GameStateSchema.parse(data); + } catch (error) { + console.error("Error loading game state:", error); + return this.createInitialGameState(); + } + } + + async saveGameState(state: GameState): Promise { + try { + const content = yaml.dump(state); + fs.writeFileSync(this.statePath, content, "utf-8"); + } catch (error) { + console.error("Error saving game state:", error); + throw error; + } + } + + private createInitialGameState(): GameState { + return { + players: {}, + proposals: {}, + gameRules: [], + currentTurn: 1, + lastUpdated: new Date().toISOString(), + }; + } + + async processVote( + issueNumber: number, + voter: string, + vote: VoteType, + issueTitle: string, + proposer: string, + ): Promise<{ + state: GameState; + proposalUpdated: boolean; + statusChanged: boolean; + }> { + const state = await this.loadGameState(); + const proposalKey = issueNumber.toString(); + + // Initialize proposal if it doesn't exist + if (!state.proposals[proposalKey]) { + state.proposals[proposalKey] = { + issueNumber, + title: issueTitle, + proposer, + votes: {}, + status: "open", + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + requiredVotes: this.config.requiredVotes, + requiredMajority: this.config.requiredMajority, + }; + } + + const proposal = state.proposals[proposalKey]; + const oldStatus = proposal.status; + + // Update vote + proposal.votes[voter] = vote; + proposal.updatedAt = new Date().toISOString(); + + // Check if proposal should be resolved + const newStatus = this.calculateProposalStatus(proposal); + proposal.status = newStatus; + + // Update player stats + this.updatePlayerStats(state, voter, proposal, oldStatus !== newStatus); + + // Update game state + state.lastUpdated = new Date().toISOString(); + if (newStatus === "passed" || newStatus === "failed") { + state.currentTurn += 1; + } + + await this.saveGameState(state); + + return { + state, + proposalUpdated: true, + statusChanged: oldStatus !== newStatus, + }; + } + + private calculateProposalStatus( + proposal: ProposalVotes, + ): "open" | "passed" | "failed" | "closed" { + const totalVotes = Object.keys(proposal.votes).length; + + if (totalVotes < proposal.requiredVotes) { + return "open"; + } + + const forVotes = Object.values(proposal.votes).filter( + (v) => v === "FOR", + ).length; + const againstVotes = Object.values(proposal.votes).filter( + (v) => v === "AGAINST", + ).length; + + const majority = forVotes / (forVotes + againstVotes); + + if (majority >= proposal.requiredMajority) { + return "passed"; + } else { + return "failed"; + } + } + + private updatePlayerStats( + state: GameState, + voter: string, + proposal: ProposalVotes, + statusChanged: boolean, + ): void { + // Initialize player if doesn't exist + if (!state.players[voter]) { + state.players[voter] = { + username: voter, + points: 0, + proposalsSubmitted: 0, + proposalsPassed: 0, + votesCast: 0, + lastActive: new Date().toISOString(), + }; + } + + const player = state.players[voter]; + player.lastActive = new Date().toISOString(); + player.votesCast += 1; + + // Award points if status changed + if (statusChanged) { + const forVotes = Object.values(proposal.votes).filter( + (v) => v === "FOR", + ).length; + const againstVotes = Object.values(proposal.votes).filter( + (v) => v === "AGAINST", + ).length; + const _totalVotes = forVotes + againstVotes; + + if (proposal.status === "passed") { + // Award points for successful proposal + if (voter === proposal.proposer) { + player.points += this.config.pointsForSuccessfulProposal; + player.proposalsSubmitted += 1; + player.proposalsPassed += 1; + } else { + // Award points for voting with majority + const voterVote = proposal.votes[voter]; + if (voterVote === "FOR") { + player.points += this.config.pointsForVotingWithMajority; + } else { + player.points += this.config.pointsForParticipation; + } + } + } else if (proposal.status === "failed") { + // Award points for failed proposal + if (voter === proposal.proposer) { + player.points += this.config.pointsForParticipation; + player.proposalsSubmitted += 1; + } else { + // Award points for voting with majority + const voterVote = proposal.votes[voter]; + if (voterVote === "AGAINST") { + player.points += this.config.pointsForVotingWithMajority; + } else { + player.points += this.config.pointsForParticipation; + } + } + } + } + } + + async closeProposal(issueNumber: number): Promise { + const state = await this.loadGameState(); + const proposalKey = issueNumber.toString(); + + if (state.proposals[proposalKey]) { + state.proposals[proposalKey].status = "closed"; + state.proposals[proposalKey].closedAt = new Date().toISOString(); + state.proposals[proposalKey].updatedAt = new Date().toISOString(); + state.lastUpdated = new Date().toISOString(); + + await this.saveGameState(state); + } + + return state; + } + + async getProposal(issueNumber: number): Promise { + const state = await this.loadGameState(); + return state.proposals[issueNumber.toString()] || null; + } + + async getAllProposals(): Promise { + const state = await this.loadGameState(); + return Object.values(state.proposals); + } + + async getPlayerScore(username: string): Promise { + const state = await this.loadGameState(); + return state.players[username] || null; + } + + async getAllPlayerScores(): Promise { + const state = await this.loadGameState(); + return Object.values(state.players).sort((a, b) => b.points - a.points); + } + + async updateScoreboard(): Promise { + const players = await this.getAllPlayerScores(); + + let scoreboard = "# Scoreboard\n\n"; + scoreboard += + "User | Points | Proposals | Passed | Votes Cast | Last Active\n"; + scoreboard += + "---- | ------ | --------- | ------ | ---------- | ------------\n"; + + for (const player of players) { + const lastActive = new Date(player.lastActive).toLocaleDateString(); + scoreboard += `@${player.username} | ${player.points} | ${player.proposalsSubmitted} | ${player.proposalsPassed} | ${player.votesCast} | ${lastActive}\n`; + } + + return scoreboard; + } + + async canMergeProposal(issueNumber: number): Promise<{ + canMerge: boolean; + reason?: string; + requiredVotes: number; + currentVotes: number; + requiredMajority: number; + currentMajority: number; + }> { + const proposal = await this.getProposal(issueNumber); + + if (!proposal) { + return { + canMerge: false, + reason: "Proposal not found", + requiredVotes: this.config.requiredVotes, + currentVotes: 0, + requiredMajority: this.config.requiredMajority, + currentMajority: 0, + }; + } + + const totalVotes = Object.keys(proposal.votes).length; + const forVotes = Object.values(proposal.votes).filter( + (v) => v === "FOR", + ).length; + const againstVotes = Object.values(proposal.votes).filter( + (v) => v === "AGAINST", + ).length; + + const hasEnoughVotes = totalVotes >= proposal.requiredVotes; + const hasMajority = + forVotes / (forVotes + againstVotes) >= proposal.requiredMajority; + + return { + canMerge: hasEnoughVotes && hasMajority, + reason: + hasEnoughVotes && hasMajority + ? undefined + : "Insufficient votes or majority", + requiredVotes: proposal.requiredVotes, + currentVotes: totalVotes, + requiredMajority: proposal.requiredMajority, + currentMajority: forVotes / (forVotes + againstVotes), + }; + } +} diff --git a/src/services/GitHubService.ts b/src/services/GitHubService.ts new file mode 100644 index 0000000..c3bbeed --- /dev/null +++ b/src/services/GitHubService.ts @@ -0,0 +1,354 @@ +import { Octokit } from "@octokit/rest"; +import { + type GitHubComment, + GitHubCommentSchema, + type GitHubConfig, + type GitHubEvent, + GitHubEventSchema, + type GitHubIssue, + GitHubIssueSchema, +} from "../types"; + +export class GitHubService { + private octokit: Octokit; + private config: GitHubConfig; + + constructor(config: GitHubConfig) { + this.config = config; + this.octokit = new Octokit({ + auth: config.token, + }); + } + + async getIssue(issueNumber: number): Promise { + const response = await this.octokit.issues.get({ + owner: this.config.owner, + repo: this.config.repo, + issue_number: issueNumber, + }); + + return GitHubIssueSchema.parse(response.data); + } + + async getIssueComments(issueNumber: number): Promise { + const response = await this.octokit.issues.listComments({ + owner: this.config.owner, + repo: this.config.repo, + issue_number: issueNumber, + }); + + return response.data.map((comment) => GitHubCommentSchema.parse(comment)); + } + + async addComment(issueNumber: number, body: string): Promise { + await this.octokit.issues.createComment({ + owner: this.config.owner, + repo: this.config.repo, + issue_number: issueNumber, + body, + }); + } + + async updateIssue( + issueNumber: number, + updates: { + title?: string; + body?: string; + state?: "open" | "closed"; + labels?: string[]; + }, + ): Promise { + await this.octokit.issues.update({ + owner: this.config.owner, + repo: this.config.repo, + issue_number: issueNumber, + ...updates, + }); + } + + async addLabels(issueNumber: number, labels: string[]): Promise { + await this.octokit.issues.addLabels({ + owner: this.config.owner, + repo: this.config.repo, + issue_number: issueNumber, + labels, + }); + } + + async removeLabel(issueNumber: number, label: string): Promise { + await this.octokit.issues.removeLabel({ + owner: this.config.owner, + repo: this.config.repo, + issue_number: issueNumber, + name: label, + }); + } + + async getPullRequest(prNumber: number): Promise<{ + number: number; + title: string; + state: string; + mergeable: boolean | null; + merged: boolean; + }> { + const response = await this.octokit.pulls.get({ + owner: this.config.owner, + repo: this.config.repo, + pull_number: prNumber, + }); + + return response.data; + } + + async mergePullRequest( + prNumber: number, + mergeMethod: "merge" | "squash" | "rebase" = "merge", + ): Promise { + await this.octokit.pulls.merge({ + owner: this.config.owner, + repo: this.config.repo, + pull_number: prNumber, + merge_method: mergeMethod, + }); + } + + async updateBranch(branch: string, ref: string): Promise { + await this.octokit.git.updateRef({ + owner: this.config.owner, + repo: this.config.repo, + ref: `heads/${branch}`, + sha: ref, + }); + } + + async createFile( + path: string, + message: string, + content: string, + branch?: string, + ): Promise { + await this.octokit.repos.createOrUpdateFileContents({ + owner: this.config.owner, + repo: this.config.repo, + path, + message, + content: Buffer.from(content).toString("base64"), + branch: branch || this.config.baseBranch, + }); + } + + async updateFile( + path: string, + message: string, + content: string, + sha: string, + branch?: string, + ): Promise { + await this.octokit.repos.createOrUpdateFileContents({ + owner: this.config.owner, + repo: this.config.repo, + path, + message, + content: Buffer.from(content).toString("base64"), + sha, + branch: branch || this.config.baseBranch, + }); + } + + async getFile( + path: string, + branch?: string, + ): Promise<{ content: string; sha: string } | null> { + try { + const response = await this.octokit.repos.getContent({ + owner: this.config.owner, + repo: this.config.repo, + path, + ref: branch || this.config.baseBranch, + }); + + if (Array.isArray(response.data)) { + return null; + } + + if (response.data.type === "file" && "content" in response.data) { + return { + content: Buffer.from(response.data.content, "base64").toString( + "utf-8", + ), + sha: response.data.sha, + }; + } + + return null; + } catch (error: unknown) { + if ( + error && + typeof error === "object" && + "status" in error && + error.status === 404 + ) { + return null; + } + throw error; + } + } + + parseEvent(eventPath: string): GitHubEvent { + const fs = require("node:fs"); + const eventData = JSON.parse(fs.readFileSync(eventPath, "utf-8")); + return GitHubEventSchema.parse(eventData); + } + + async enableBranchProtection( + branch: string, + requiredReviews: number = 1, + ): Promise { + await this.octokit.repos.updateBranchProtection({ + owner: this.config.owner, + repo: this.config.repo, + branch, + required_status_checks: null, + enforce_admins: false, + required_pull_request_reviews: { + required_approving_review_count: requiredReviews, + dismiss_stale_reviews: true, + require_code_owner_reviews: false, + }, + restrictions: null, + }); + } + + async disableBranchProtection(branch: string): Promise { + await this.octokit.repos.deleteBranchProtection({ + owner: this.config.owner, + repo: this.config.repo, + branch, + }); + } + + async createStatusCheck( + sha: string, + context: string, + state: "pending" | "success" | "failure" | "error", + description: string, + targetUrl?: string, + ): Promise { + await this.octokit.repos.createCommitStatus({ + owner: this.config.owner, + repo: this.config.repo, + sha, + state, + target_url: targetUrl, + description, + context, + }); + } + + async updateStatusCheck( + sha: string, + context: string, + state: "pending" | "success" | "failure" | "error", + description: string, + targetUrl?: string, + ): Promise { + await this.createStatusCheck(sha, context, state, description, targetUrl); + } + + async getCommitSha(issueNumber: number): Promise { + try { + // Try to get the PR associated with this issue + const response = await this.octokit.pulls.list({ + owner: this.config.owner, + repo: this.config.repo, + state: "open", + }); + + // Find PR that references this issue + const pr = response.data.find( + (pr) => + pr.body?.includes(`#${issueNumber}`) || + pr.title.includes(`#${issueNumber}`), + ); + + if (pr) { + return pr.head.sha; + } + + // If no PR found, try to get the latest commit from the default branch + const defaultBranch = await this.octokit.repos.get({ + owner: this.config.owner, + repo: this.config.repo, + }); + + const branchResponse = await this.octokit.repos.getBranch({ + owner: this.config.owner, + repo: this.config.repo, + branch: defaultBranch.data.default_branch, + }); + + return branchResponse.data.commit.sha; + } catch (error) { + console.warn( + `Could not get commit SHA for issue #${issueNumber}:`, + error, + ); + return null; + } + } + + getConfig(): GitHubConfig { + return this.config; + } + + async getPullRequestByIssue(issueNumber: number): Promise<{ + number: number; + title: string; + body: string | null; + state: string; + } | null> { + try { + const response = await this.octokit.pulls.list({ + owner: this.config.owner, + repo: this.config.repo, + state: "open", + }); + + // Find PR that references this issue + const pr = response.data.find( + (pr) => + pr.body?.includes(`#${issueNumber}`) || + pr.title.includes(`#${issueNumber}`), + ); + + if (pr) { + return { + number: pr.number, + title: pr.title, + body: pr.body, + state: pr.state, + }; + } + + return null; + } catch (error) { + console.warn( + `Could not get PR for issue #${issueNumber}:`, + error, + ); + return null; + } + } + + async updatePullRequestBody( + prNumber: number, + body: string, + ): Promise { + await this.octokit.pulls.update({ + owner: this.config.owner, + repo: this.config.repo, + pull_number: prNumber, + body, + }); + } +} diff --git a/src/services/StateProcessor.ts b/src/services/StateProcessor.ts new file mode 100644 index 0000000..7351d12 --- /dev/null +++ b/src/services/StateProcessor.ts @@ -0,0 +1,416 @@ +import { + EventLogEntry, + ComputedGameState, + ComputedPlayerState, + ComputedProposalState, + ComputedRuleState, + GameEvent, +} from "../types/events"; +import { EventLogService } from "./EventLogService"; + +export class StateProcessor { + private eventLogService: EventLogService; + + constructor(eventLogService: EventLogService) { + this.eventLogService = eventLogService; + } + + /** + * Compute the current game state from all events + */ + async computeCurrentState(): Promise { + const events = await this.eventLogService.readAllEvents(); + + const state: ComputedGameState = { + players: {}, + proposals: {}, + rules: {}, + current_turn: 1, + game_started_at: new Date().toISOString(), + last_updated: new Date().toISOString(), + event_count: events.length, + }; + + // Process events in chronological order + for (const entry of events) { + this.processEvent(state, entry); + } + + // Update last_updated timestamp + if (events.length > 0) { + state.last_updated = events[events.length - 1].event.timestamp; + } + + return state; + } + + /** + * Process a single event and update the state + */ + private processEvent(state: ComputedGameState, entry: EventLogEntry): void { + const event = entry.event; + + switch (event.type) { + case "game_started": + this.processGameStarted(state, event); + break; + case "proposal_created": + this.processProposalCreated(state, event); + break; + case "vote_cast": + this.processVoteCast(state, event); + break; + case "proposal_resolved": + this.processProposalResolved(state, event); + break; + case "rule_enacted": + this.processRuleEnacted(state, event); + break; + case "rule_repealed": + this.processRuleRepealed(state, event); + break; + case "rule_amended": + this.processRuleAmended(state, event); + break; + case "rule_transmuted": + this.processRuleTransmuted(state, event); + break; + case "player_joined": + this.processPlayerJoined(state, event); + break; + case "points_awarded": + this.processPointsAwarded(state, event); + break; + case "turn_advanced": + this.processTurnAdvanced(state, event); + break; + default: + console.warn(`Unknown event type: ${(event as any).type}`); + } + } + + private processGameStarted(state: ComputedGameState, event: GameEvent & { type: "game_started" }): void { + state.game_started_at = event.timestamp; + state.current_turn = 1; + + // Initialize rules from the initial set + for (const ruleId of event.initial_rules) { + state.rules[ruleId] = { + rule_id: ruleId, + title: `Rule ${ruleId}`, + content: `Initial rule ${ruleId}`, + rule_type: ruleId.startsWith("1") ? "immutable" : "mutable", + status: "active", + author: "system", + enacted_at: event.timestamp, + amendment_history: [], + transmutation_history: [], + }; + } + } + + private processProposalCreated(state: ComputedGameState, event: GameEvent & { type: "proposal_created" }): void { + // Ensure player exists + this.ensurePlayerExists(state, event.proposer, event.timestamp); + + // Create proposal + state.proposals[event.issue_number.toString()] = { + issue_number: event.issue_number, + title: event.title, + proposer: event.proposer, + status: "open", + votes: {}, + created_at: event.timestamp, + }; + + // Update player stats + state.players[event.proposer].proposals_submitted++; + state.players[event.proposer].last_active = event.timestamp; + } + + private processVoteCast(state: ComputedGameState, event: GameEvent & { type: "vote_cast" }): void { + // Ensure player exists + this.ensurePlayerExists(state, event.voter, event.timestamp); + + // Update proposal votes + const proposalKey = event.issue_number.toString(); + if (state.proposals[proposalKey]) { + state.proposals[proposalKey].votes[event.voter] = event.vote; + } + + // Update player stats + state.players[event.voter].votes_cast++; + state.players[event.voter].last_active = event.timestamp; + } + + private processProposalResolved(state: ComputedGameState, event: GameEvent & { type: "proposal_resolved" }): void { + const proposalKey = event.issue_number.toString(); + if (state.proposals[proposalKey]) { + state.proposals[proposalKey].status = event.status; + state.proposals[proposalKey].resolved_at = event.timestamp; + state.proposals[proposalKey].final_vote_count = event.final_votes; + + // Update proposer stats if passed + if (event.status === "passed") { + const proposer = state.proposals[proposalKey].proposer; + if (state.players[proposer]) { + state.players[proposer].proposals_passed++; + } + } + } + } + + private processRuleEnacted(state: ComputedGameState, event: GameEvent & { type: "rule_enacted" }): void { + state.rules[event.rule_id] = { + rule_id: event.rule_id, + title: event.title, + content: event.content, + rule_type: event.rule_type, + status: "active", + author: event.author, + enacted_at: event.timestamp, + amendment_history: [], + transmutation_history: [], + }; + } + + private processRuleRepealed(state: ComputedGameState, event: GameEvent & { type: "rule_repealed" }): void { + if (state.rules[event.rule_id]) { + state.rules[event.rule_id].status = "repealed"; + state.rules[event.rule_id].repealed_at = event.timestamp; + } + } + + private processRuleAmended(state: ComputedGameState, event: GameEvent & { type: "rule_amended" }): void { + if (state.rules[event.rule_id]) { + // Add to amendment history + state.rules[event.rule_id].amendment_history.push({ + content: event.new_content, + amended_by: event.amended_by, + amended_at: event.timestamp, + event_id: event.id, + }); + + // Update current content + state.rules[event.rule_id].content = event.new_content; + } + } + + private processRuleTransmuted(state: ComputedGameState, event: GameEvent & { type: "rule_transmuted" }): void { + if (state.rules[event.rule_id]) { + // Add to transmutation history + state.rules[event.rule_id].transmutation_history.push({ + old_type: event.old_type, + new_type: event.new_type, + transmuted_by: event.transmuted_by, + transmuted_at: event.timestamp, + event_id: event.id, + }); + + // Update current type + state.rules[event.rule_id].rule_type = event.new_type; + } + } + + private processPlayerJoined(state: ComputedGameState, event: GameEvent & { type: "player_joined" }): void { + this.ensurePlayerExists(state, event.username, event.timestamp); + } + + private processPointsAwarded(state: ComputedGameState, event: GameEvent & { type: "points_awarded" }): void { + // Ensure player exists + this.ensurePlayerExists(state, event.player, event.timestamp); + + // Award points + state.players[event.player].points += event.points; + state.players[event.player].last_active = event.timestamp; + + // Add to point history + state.players[event.player].point_history.push({ + points: event.points, + reason: event.reason, + timestamp: event.timestamp, + event_id: event.id, + }); + } + + private processTurnAdvanced(state: ComputedGameState, event: GameEvent & { type: "turn_advanced" }): void { + state.current_turn = event.new_turn; + } + + private ensurePlayerExists(state: ComputedGameState, username: string, timestamp: string): void { + if (!state.players[username]) { + state.players[username] = { + username, + points: 0, + proposals_submitted: 0, + proposals_passed: 0, + votes_cast: 0, + first_seen: timestamp, + last_active: timestamp, + point_history: [], + }; + } + } + + /** + * Get current state for a specific player + */ + async getPlayerState(username: string): Promise { + const state = await this.computeCurrentState(); + return state.players[username] || null; + } + + /** + * Get current state for a specific proposal + */ + async getProposalState(issueNumber: number): Promise { + const state = await this.computeCurrentState(); + return state.proposals[issueNumber.toString()] || null; + } + + /** + * Get current state for a specific rule + */ + async getRuleState(ruleId: string): Promise { + const state = await this.computeCurrentState(); + return state.rules[ruleId] || null; + } + + /** + * Get all active rules + */ + async getActiveRules(): Promise { + const state = await this.computeCurrentState(); + return Object.values(state.rules).filter(rule => rule.status === "active"); + } + + /** + * Get all players sorted by points + */ + async getPlayerLeaderboard(): Promise { + const state = await this.computeCurrentState(); + return Object.values(state.players).sort((a, b) => b.points - a.points); + } + + /** + * Get proposal statistics + */ + async getProposalStats(): Promise<{ + total: number; + open: number; + passed: number; + failed: number; + closed: number; + }> { + const state = await this.computeCurrentState(); + const proposals = Object.values(state.proposals); + + return { + total: proposals.length, + open: proposals.filter(p => p.status === "open").length, + passed: proposals.filter(p => p.status === "passed").length, + failed: proposals.filter(p => p.status === "failed").length, + closed: proposals.filter(p => p.status === "closed").length, + }; + } + + /** + * Check if a proposal can be merged based on current votes and rules + */ + async canMergeProposal(issueNumber: number, config: { + requiredVotes: number; + requiredMajority: number; + }): Promise<{ + canMerge: boolean; + reason?: string; + currentVotes: number; + forVotes: number; + againstVotes: number; + majority: number; + }> { + const proposal = await this.getProposalState(issueNumber); + + if (!proposal) { + return { + canMerge: false, + reason: "Proposal not found", + currentVotes: 0, + forVotes: 0, + againstVotes: 0, + majority: 0, + }; + } + + if (proposal.status !== "open") { + return { + canMerge: false, + reason: `Proposal is ${proposal.status}`, + currentVotes: 0, + forVotes: 0, + againstVotes: 0, + majority: 0, + }; + } + + const votes = Object.values(proposal.votes); + const forVotes = votes.filter(v => v === "FOR").length; + const againstVotes = votes.filter(v => v === "AGAINST").length; + const totalVotes = forVotes + againstVotes; + const majority = totalVotes > 0 ? forVotes / totalVotes : 0; + + if (totalVotes < config.requiredVotes) { + return { + canMerge: false, + reason: `Insufficient votes (${totalVotes}/${config.requiredVotes})`, + currentVotes: totalVotes, + forVotes, + againstVotes, + majority, + }; + } + + if (majority < config.requiredMajority) { + return { + canMerge: false, + reason: `Insufficient majority (${(majority * 100).toFixed(1)}% < ${(config.requiredMajority * 100).toFixed(1)}%)`, + currentVotes: totalVotes, + forVotes, + againstVotes, + majority, + }; + } + + return { + canMerge: true, + currentVotes: totalVotes, + forVotes, + againstVotes, + majority, + }; + } + + /** + * Compute state up to a specific point in time (for historical analysis) + */ + async computeStateAtTime(timestamp: string): Promise { + const allEvents = await this.eventLogService.readAllEvents(); + const eventsUpToTime = allEvents.filter(entry => entry.event.timestamp <= timestamp); + + const state: ComputedGameState = { + players: {}, + proposals: {}, + rules: {}, + current_turn: 1, + game_started_at: new Date().toISOString(), + last_updated: timestamp, + event_count: eventsUpToTime.length, + }; + + // Process events in chronological order + for (const entry of eventsUpToTime) { + this.processEvent(state, entry); + } + + return state; + } +} \ No newline at end of file diff --git a/src/services/VoteProcessor.ts b/src/services/VoteProcessor.ts new file mode 100644 index 0000000..7983316 --- /dev/null +++ b/src/services/VoteProcessor.ts @@ -0,0 +1,203 @@ +import type { GitHubComment, VoteType } from "../types"; + +const VOTE_PATTERNS = [ + /^VOTE:\s*(FOR|AGAINST)$/i, + /^I vote (FOR|AGAINST)$/i, + /^My vote is (FOR|AGAINST)$/i, + /^Voting (FOR|AGAINST)$/i, + /^āœ… (FOR|AGAINST)$/i, + /^āŒ (FOR|AGAINST)$/i, +]; + +export function parseVoteFromComment(comment: GitHubComment): { + voter: string; + vote: VoteType; +} | null { + const body = comment.body.trim(); + + for (const pattern of VOTE_PATTERNS) { + const match = body.match(pattern); + if (match) { + const vote = match[1].toUpperCase() as VoteType; + return { + voter: comment.user.login, + vote, + }; + } + } + + return null; +} + +export function isValidVote(vote: string): vote is VoteType { + return vote === "FOR" || vote === "AGAINST"; +} + +export function formatVoteSummary(votes: Record): string { + const forVotes = Object.values(votes).filter((v) => v === "FOR").length; + const againstVotes = Object.values(votes).filter( + (v) => v === "AGAINST", + ).length; + const totalVotes = forVotes + againstVotes; + + if (totalVotes === 0) { + return "No votes cast yet."; + } + + const forVoters = Object.entries(votes) + .filter(([_, vote]) => vote === "FOR") + .map(([voter, _]) => `@${voter}`) + .join(", "); + + const againstVoters = Object.entries(votes) + .filter(([_, vote]) => vote === "AGAINST") + .map(([voter, _]) => `@${voter}`) + .join(", "); + + let summary = `**Vote Summary:** ${forVotes} FOR, ${againstVotes} AGAINST\n\n`; + + if (forVotes > 0) { + summary += `**FOR:** ${forVoters}\n`; + } + + if (againstVotes > 0) { + summary += `**AGAINST:** ${againstVoters}\n`; + } + + const majority = forVotes / totalVotes; + summary += `\n**Majority:** ${(majority * 100).toFixed(1)}% FOR`; + + return summary; +} + +export function formatVoteInstructions(): string { + return `## How to Vote + +To vote on this proposal, comment with one of the following formats: + +- \`VOTE: FOR\` or \`VOTE: AGAINST\` +- \`I vote FOR\` or \`I vote AGAINST\` +- \`My vote is FOR\` or \`My vote is AGAINST\` +- \`Voting FOR\` or \`Voting AGAINST\` +- \`āœ… FOR\` or \`āŒ AGAINST\` + +**Note:** Only one vote per person is allowed. Your most recent vote will be counted.`; +} + +export function formatProposalStatus(proposal: { + status: string; + requiredVotes: number; + requiredMajority: number; + votes: Record; + mergeCheck?: { + canMerge: boolean; + reason?: string; + requiredVotes: number; + currentVotes: number; + requiredMajority: number; + currentMajority: number; + }; +}): string { + const totalVotes = Object.keys(proposal.votes).length; + const forVotes = Object.values(proposal.votes).filter( + (v) => v === "FOR", + ).length; + const againstVotes = Object.values(proposal.votes).filter( + (v) => v === "AGAINST", + ).length; + + let status = `## Proposal Status: ${proposal.status.toUpperCase()}\n\n`; + status += `- **Required Votes:** ${proposal.requiredVotes}\n`; + status += `- **Current Votes:** ${totalVotes}\n`; + status += `- **Required Majority:** ${(proposal.requiredMajority * 100).toFixed(1)}%\n`; + + if (totalVotes > 0) { + const currentMajority = forVotes / (forVotes + againstVotes); + status += `- **Current Majority:** ${(currentMajority * 100).toFixed(1)}% FOR\n`; + } + + // Add detailed mergeability information + if (proposal.mergeCheck) { + status += `\n## Merge Status\n`; + if (proposal.mergeCheck.canMerge) { + status += `āœ… **Ready to merge!**\n`; + } else { + status += `āŒ **Cannot merge**\n`; + if (proposal.mergeCheck.reason) { + status += `\n**Reason:** ${proposal.mergeCheck.reason}\n`; + } + + // Add specific details about what's missing + if ( + proposal.mergeCheck.currentVotes < proposal.mergeCheck.requiredVotes + ) { + const missingVotes = + proposal.mergeCheck.requiredVotes - proposal.mergeCheck.currentVotes; + status += `- Need ${missingVotes} more vote(s) (${proposal.mergeCheck.currentVotes}/${proposal.mergeCheck.requiredVotes})\n`; + } + + if ( + proposal.mergeCheck.currentMajority < + proposal.mergeCheck.requiredMajority + ) { + const currentPercent = ( + proposal.mergeCheck.currentMajority * 100 + ).toFixed(1); + const requiredPercent = ( + proposal.mergeCheck.requiredMajority * 100 + ).toFixed(1); + status += `- Insufficient majority: ${currentPercent}% FOR (need ${requiredPercent}%)\n`; + } + } + } + + if (proposal.status === "open") { + if (totalVotes < proposal.requiredVotes) { + status += `\nā³ **Waiting for ${ + proposal.requiredVotes - totalVotes + } more vote(s)**`; + } else { + const currentMajority = forVotes / (forVotes + againstVotes); + if (currentMajority < proposal.requiredMajority) { + status += `\nāŒ **Insufficient majority** (need ${( + proposal.requiredMajority * 100 + ).toFixed(1)}%, have ${(currentMajority * 100).toFixed(1)}%)`; + } else { + status += `\nāœ… **Ready to merge!**`; + } + } + } else if (proposal.status === "passed") { + status += `\nšŸŽ‰ **Proposal PASSED!**`; + } else if (proposal.status === "failed") { + status += `\nāŒ **Proposal FAILED**`; + } + + return status; +} + +export function generateVoteTable(votes: Record): string { + const forVotes = Object.entries(votes) + .filter(([_, vote]) => vote === "FOR") + .map(([voter, _]) => voter); + const againstVotes = Object.entries(votes) + .filter(([_, vote]) => vote === "AGAINST") + .map(([voter, _]) => voter); + + const totalVotes = forVotes.length + againstVotes.length; + const forCount = forVotes.length; + const againstCount = againstVotes.length; + + let table = "## Vote Summary\n\n"; + table += "| Vote | Count | Voters |\n"; + table += "|------|-------|--------|\n"; + table += `| āœ… FOR | ${forCount} | ${forVotes.length > 0 ? forVotes.map(v => `@${v}`).join(", ") : "None"} |\n`; + table += `| āŒ AGAINST | ${againstCount} | ${againstVotes.length > 0 ? againstVotes.map(v => `@${v}`).join(", ") : "None"} |\n`; + table += `| **Total** | **${totalVotes}** | |\n\n`; + + if (totalVotes > 0) { + const majority = forCount / totalVotes; + table += `**Current Majority:** ${(majority * 100).toFixed(1)}% FOR\n\n`; + } + + return table; +} diff --git a/src/test/GameStateService.test.ts b/src/test/GameStateService.test.ts new file mode 100644 index 0000000..48337b7 --- /dev/null +++ b/src/test/GameStateService.test.ts @@ -0,0 +1,632 @@ +import * as fs from "node:fs"; +import * as yaml from "js-yaml"; +import { GameStateService } from "../services/GameStateService"; +import type { + GameConfig, + GameState, + PlayerScore, + ProposalVotes, +} from "../types"; + +// Mock fs and yaml modules +jest.mock("fs"); +jest.mock("js-yaml"); + +const mockFs = fs as jest.Mocked; +const mockYaml = yaml as jest.Mocked; + +describe("GameStateService", () => { + let gameStateService: GameStateService; + let mockConfig: GameConfig; + + beforeEach(() => { + jest.clearAllMocks(); + + mockConfig = { + requiredVotes: 3, + requiredMajority: 0.5, + pointsForSuccessfulProposal: 3, + pointsForVotingWithMajority: 2, + pointsForParticipation: 1, + autoMergeEnabled: true, + branchProtectionEnabled: true, + }; + + gameStateService = new GameStateService("test-game-state.yaml", mockConfig); + }); + + describe("loadGameState", () => { + it("should load existing game state successfully", async () => { + const mockState: GameState = { + players: {}, + proposals: {}, + gameRules: [], + currentTurn: 1, + lastUpdated: "2023-01-01T00:00:00Z", + }; + + mockFs.existsSync.mockReturnValue(true); + mockFs.readFileSync.mockReturnValue("mock yaml content"); + mockYaml.load.mockReturnValue(mockState); + + const result = await gameStateService.loadGameState(); + + expect(mockFs.existsSync).toHaveBeenCalledWith("test-game-state.yaml"); + expect(mockFs.readFileSync).toHaveBeenCalledWith( + "test-game-state.yaml", + "utf-8", + ); + expect(mockYaml.load).toHaveBeenCalledWith("mock yaml content"); + expect(result).toEqual(mockState); + }); + + it("should create initial game state when file does not exist", async () => { + mockFs.existsSync.mockReturnValue(false); + + const result = await gameStateService.loadGameState(); + + expect(mockFs.existsSync).toHaveBeenCalledWith("test-game-state.yaml"); + expect(result).toEqual({ + players: {}, + proposals: {}, + gameRules: [], + currentTurn: 1, + lastUpdated: expect.any(String), + }); + }); + + it("should handle parsing errors gracefully", async () => { + mockFs.existsSync.mockReturnValue(true); + mockFs.readFileSync.mockReturnValue("invalid yaml"); + mockYaml.load.mockImplementation(() => { + throw new Error("YAML parsing error"); + }); + + const result = await gameStateService.loadGameState(); + + expect(result).toEqual({ + players: {}, + proposals: {}, + gameRules: [], + currentTurn: 1, + lastUpdated: expect.any(String), + }); + }); + }); + + describe("saveGameState", () => { + it("should save game state successfully", async () => { + const mockState: GameState = { + players: {}, + proposals: {}, + gameRules: [], + currentTurn: 1, + lastUpdated: "2023-01-01T00:00:00Z", + }; + + mockYaml.dump.mockReturnValue("serialized yaml"); + mockFs.writeFileSync.mockReset(); // Ensure no error is thrown + + await gameStateService.saveGameState(mockState); + + expect(mockYaml.dump).toHaveBeenCalledWith(mockState); + expect(mockFs.writeFileSync).toHaveBeenCalledWith( + "test-game-state.yaml", + "serialized yaml", + "utf-8", + ); + }); + + it("should handle save errors", async () => { + const mockState: GameState = { + players: {}, + proposals: {}, + gameRules: [], + currentTurn: 1, + lastUpdated: "2023-01-01T00:00:00Z", + }; + + mockYaml.dump.mockReturnValue("serialized yaml"); + mockFs.writeFileSync.mockImplementation(() => { + throw new Error("Write error"); + }); + + await expect(gameStateService.saveGameState(mockState)).rejects.toThrow( + "Write error", + ); + mockFs.writeFileSync.mockReset(); // Reset after this test + }); + }); + + describe("processVote", () => { + it("should process a new vote and create proposal", async () => { + const _mockState: GameState = { + players: {}, + proposals: {}, + gameRules: [], + currentTurn: 1, + lastUpdated: "2023-01-01T00:00:00Z", + }; + + mockFs.existsSync.mockReturnValue(false); + mockYaml.dump.mockReturnValue("serialized yaml"); + + const result = await gameStateService.processVote( + 123, + "testuser", + "FOR", + "Test Proposal", + "proposer", + ); + + expect(result.proposalUpdated).toBe(true); + expect(result.statusChanged).toBe(false); + expect(result.state.proposals["123"]).toEqual({ + issueNumber: 123, + title: "Test Proposal", + proposer: "proposer", + votes: { testuser: "FOR" }, + status: "open", + createdAt: expect.any(String), + updatedAt: expect.any(String), + requiredVotes: 3, + requiredMajority: 0.5, + }); + }); + + it("should update existing proposal with new vote", async () => { + const existingProposal: ProposalVotes = { + issueNumber: 123, + title: "Test Proposal", + proposer: "proposer", + votes: { user1: "FOR" }, + status: "open", + createdAt: "2023-01-01T00:00:00Z", + updatedAt: "2023-01-01T00:00:00Z", + requiredVotes: 3, + requiredMajority: 0.5, + }; + + const mockState: GameState = { + players: {}, + proposals: { "123": existingProposal }, + gameRules: [], + currentTurn: 1, + lastUpdated: "2023-01-01T00:00:00Z", + }; + + mockFs.existsSync.mockReturnValue(true); + mockFs.readFileSync.mockReturnValue("mock yaml content"); + mockYaml.load.mockReturnValue(mockState); + mockYaml.dump.mockReturnValue("serialized yaml"); + + const result = await gameStateService.processVote( + 123, + "user2", + "AGAINST", + "Test Proposal", + "proposer", + ); + + expect(result.proposalUpdated).toBe(true); + expect(result.statusChanged).toBe(false); + expect(result.state.proposals["123"].votes).toEqual({ + user1: "FOR", + user2: "AGAINST", + }); + }); + + it("should change status when proposal passes", async () => { + const existingProposal: ProposalVotes = { + issueNumber: 123, + title: "Test Proposal", + proposer: "proposer", + votes: { user1: "FOR", user2: "FOR" }, + status: "open", + createdAt: "2023-01-01T00:00:00Z", + updatedAt: "2023-01-01T00:00:00Z", + requiredVotes: 3, + requiredMajority: 0.5, + }; + + const mockState: GameState = { + players: {}, + proposals: { "123": existingProposal }, + gameRules: [], + currentTurn: 1, + lastUpdated: "2023-01-01T00:00:00Z", + }; + + mockFs.existsSync.mockReturnValue(true); + mockFs.readFileSync.mockReturnValue("mock yaml content"); + mockYaml.load.mockReturnValue(mockState); + mockYaml.dump.mockReturnValue("serialized yaml"); + + const result = await gameStateService.processVote( + 123, + "user3", + "FOR", + "Test Proposal", + "proposer", + ); + + expect(result.proposalUpdated).toBe(true); + expect(result.statusChanged).toBe(true); + expect(result.state.proposals["123"].status).toBe("passed"); + expect(result.state.currentTurn).toBe(2); + }); + + it("should change status when proposal fails", async () => { + const existingProposal: ProposalVotes = { + issueNumber: 123, + title: "Test Proposal", + proposer: "proposer", + votes: { user1: "AGAINST", user2: "AGAINST" }, + status: "open", + createdAt: "2023-01-01T00:00:00Z", + updatedAt: "2023-01-01T00:00:00Z", + requiredVotes: 3, + requiredMajority: 0.5, + }; + + const mockState: GameState = { + players: {}, + proposals: { "123": existingProposal }, + gameRules: [], + currentTurn: 1, + lastUpdated: "2023-01-01T00:00:00Z", + }; + + mockFs.existsSync.mockReturnValue(true); + mockFs.readFileSync.mockReturnValue("mock yaml content"); + mockYaml.load.mockReturnValue(mockState); + mockYaml.dump.mockReturnValue("serialized yaml"); + + const result = await gameStateService.processVote( + 123, + "user3", + "AGAINST", + "Test Proposal", + "proposer", + ); + + expect(result.proposalUpdated).toBe(true); + expect(result.statusChanged).toBe(true); + expect(result.state.proposals["123"].status).toBe("failed"); + expect(result.state.currentTurn).toBe(2); + }); + }); + + describe("closeProposal", () => { + it("should close an existing proposal", async () => { + const existingProposal: ProposalVotes = { + issueNumber: 123, + title: "Test Proposal", + proposer: "proposer", + votes: { user1: "FOR" }, + status: "open", + createdAt: "2023-01-01T00:00:00Z", + updatedAt: "2023-01-01T00:00:00Z", + requiredVotes: 3, + requiredMajority: 0.5, + }; + + const mockState: GameState = { + players: {}, + proposals: { "123": existingProposal }, + gameRules: [], + currentTurn: 1, + lastUpdated: "2023-01-01T00:00:00Z", + }; + + mockFs.existsSync.mockReturnValue(true); + mockFs.readFileSync.mockReturnValue("mock yaml content"); + mockYaml.load.mockReturnValue(mockState); + mockYaml.dump.mockReturnValue("serialized yaml"); + + const result = await gameStateService.closeProposal(123); + + expect(result.proposals["123"].status).toBe("closed"); + expect(result.proposals["123"].closedAt).toBeDefined(); + }); + + it("should handle non-existent proposal gracefully", async () => { + const mockState: GameState = { + players: {}, + proposals: {}, + gameRules: [], + currentTurn: 1, + lastUpdated: expect.any(String), + }; + + mockFs.existsSync.mockReturnValue(false); + + const result = await gameStateService.closeProposal(999); + + expect(result).toEqual(mockState); + }); + }); + + describe("getProposal", () => { + it("should return existing proposal", async () => { + const existingProposal: ProposalVotes = { + issueNumber: 123, + title: "Test Proposal", + proposer: "proposer", + votes: { user1: "FOR" }, + status: "open", + createdAt: "2023-01-01T00:00:00Z", + updatedAt: "2023-01-01T00:00:00Z", + requiredVotes: 3, + requiredMajority: 0.5, + }; + + const mockState: GameState = { + players: {}, + proposals: { "123": existingProposal }, + gameRules: [], + currentTurn: 1, + lastUpdated: "2023-01-01T00:00:00Z", + }; + + mockFs.existsSync.mockReturnValue(true); + mockFs.readFileSync.mockReturnValue("mock yaml content"); + mockYaml.load.mockReturnValue(mockState); + + const result = await gameStateService.getProposal(123); + + expect(result).toEqual(existingProposal); + }); + + it("should return null for non-existent proposal", async () => { + mockFs.existsSync.mockReturnValue(false); + + const result = await gameStateService.getProposal(999); + + expect(result).toBeNull(); + }); + }); + + describe("getAllProposals", () => { + it("should return all proposals", async () => { + const proposal1: ProposalVotes = { + issueNumber: 123, + title: "Test Proposal 1", + proposer: "proposer1", + votes: { user1: "FOR" }, + status: "open", + createdAt: "2023-01-01T00:00:00Z", + updatedAt: "2023-01-01T00:00:00Z", + requiredVotes: 3, + requiredMajority: 0.5, + }; + + const proposal2: ProposalVotes = { + issueNumber: 124, + title: "Test Proposal 2", + proposer: "proposer2", + votes: { user2: "AGAINST" }, + status: "passed", + createdAt: "2023-01-01T00:00:00Z", + updatedAt: "2023-01-01T00:00:00Z", + requiredVotes: 3, + requiredMajority: 0.5, + }; + + const mockState: GameState = { + players: {}, + proposals: { "123": proposal1, "124": proposal2 }, + gameRules: [], + currentTurn: 1, + lastUpdated: "2023-01-01T00:00:00Z", + }; + + mockFs.existsSync.mockReturnValue(true); + mockFs.readFileSync.mockReturnValue("mock yaml content"); + mockYaml.load.mockReturnValue(mockState); + + const result = await gameStateService.getAllProposals(); + + expect(result).toHaveLength(2); + expect(result).toContainEqual(proposal1); + expect(result).toContainEqual(proposal2); + }); + }); + + describe("getPlayerScore", () => { + it("should return existing player score", async () => { + const playerScore: PlayerScore = { + username: "testuser", + points: 10, + proposalsSubmitted: 2, + proposalsPassed: 1, + votesCast: 5, + lastActive: "2023-01-01T00:00:00Z", + }; + + const mockState: GameState = { + players: { testuser: playerScore }, + proposals: {}, + gameRules: [], + currentTurn: 1, + lastUpdated: "2023-01-01T00:00:00Z", + }; + + mockFs.existsSync.mockReturnValue(true); + mockFs.readFileSync.mockReturnValue("mock yaml content"); + mockYaml.load.mockReturnValue(mockState); + + const result = await gameStateService.getPlayerScore("testuser"); + + expect(result).toEqual(playerScore); + }); + + it("should return null for non-existent player", async () => { + mockFs.existsSync.mockReturnValue(false); + + const result = await gameStateService.getPlayerScore("nonexistent"); + + expect(result).toBeNull(); + }); + }); + + describe("getAllPlayerScores", () => { + it("should return all player scores sorted by points", async () => { + const player1: PlayerScore = { + username: "user1", + points: 10, + proposalsSubmitted: 2, + proposalsPassed: 1, + votesCast: 5, + lastActive: "2023-01-01T00:00:00Z", + }; + + const player2: PlayerScore = { + username: "user2", + points: 15, + proposalsSubmitted: 3, + proposalsPassed: 2, + votesCast: 8, + lastActive: "2023-01-01T00:00:00Z", + }; + + const mockState: GameState = { + players: { user1: player1, user2: player2 }, + proposals: {}, + gameRules: [], + currentTurn: 1, + lastUpdated: "2023-01-01T00:00:00Z", + }; + + mockFs.existsSync.mockReturnValue(true); + mockFs.readFileSync.mockReturnValue("mock yaml content"); + mockYaml.load.mockReturnValue(mockState); + + const result = await gameStateService.getAllPlayerScores(); + + expect(result).toHaveLength(2); + expect(result[0]).toEqual(player2); // Higher points first + expect(result[1]).toEqual(player1); + }); + }); + + describe("updateScoreboard", () => { + it("should generate scoreboard markdown", async () => { + const player1: PlayerScore = { + username: "user1", + points: 10, + proposalsSubmitted: 2, + proposalsPassed: 1, + votesCast: 5, + lastActive: "2023-01-01T00:00:00Z", + }; + + const player2: PlayerScore = { + username: "user2", + points: 15, + proposalsSubmitted: 3, + proposalsPassed: 2, + votesCast: 8, + lastActive: "2023-01-01T00:00:00Z", + }; + + const mockState: GameState = { + players: { user1: player1, user2: player2 }, + proposals: {}, + gameRules: [], + currentTurn: 1, + lastUpdated: "2023-01-01T00:00:00Z", + }; + + mockFs.existsSync.mockReturnValue(true); + mockFs.readFileSync.mockReturnValue("mock yaml content"); + mockYaml.load.mockReturnValue(mockState); + + const result = await gameStateService.updateScoreboard(); + + expect(result).toContain("# Scoreboard"); + expect(result).toContain("@user2"); + expect(result).toContain("@user1"); + expect(result).toContain("15"); + expect(result).toContain("10"); + }); + }); + + describe("canMergeProposal", () => { + it("should return true for mergeable proposal", async () => { + const proposal: ProposalVotes = { + issueNumber: 123, + title: "Test Proposal", + proposer: "proposer", + votes: { user1: "FOR", user2: "FOR", user3: "FOR" }, + status: "open", + createdAt: "2023-01-01T00:00:00Z", + updatedAt: "2023-01-01T00:00:00Z", + requiredVotes: 3, + requiredMajority: 0.5, + }; + + const mockState: GameState = { + players: {}, + proposals: { "123": proposal }, + gameRules: [], + currentTurn: 1, + lastUpdated: "2023-01-01T00:00:00Z", + }; + + mockFs.existsSync.mockReturnValue(true); + mockFs.readFileSync.mockReturnValue("mock yaml content"); + mockYaml.load.mockReturnValue(mockState); + + const result = await gameStateService.canMergeProposal(123); + + expect(result.canMerge).toBe(true); + expect(result.requiredVotes).toBe(3); + expect(result.currentVotes).toBe(3); + expect(result.requiredMajority).toBe(0.5); + expect(result.currentMajority).toBe(1.0); + }); + + it("should return false for non-mergeable proposal", async () => { + const proposal: ProposalVotes = { + issueNumber: 123, + title: "Test Proposal", + proposer: "proposer", + votes: { user1: "FOR", user2: "AGAINST" }, + status: "open", + createdAt: "2023-01-01T00:00:00Z", + updatedAt: "2023-01-01T00:00:00Z", + requiredVotes: 3, + requiredMajority: 0.5, + }; + + const mockState: GameState = { + players: {}, + proposals: { "123": proposal }, + gameRules: [], + currentTurn: 1, + lastUpdated: "2023-01-01T00:00:00Z", + }; + + mockFs.existsSync.mockReturnValue(true); + mockFs.readFileSync.mockReturnValue("mock yaml content"); + mockYaml.load.mockReturnValue(mockState); + + const result = await gameStateService.canMergeProposal(123); + + expect(result.canMerge).toBe(false); + expect(result.reason).toBe("Insufficient votes or majority"); + expect(result.currentVotes).toBe(2); + expect(result.currentMajority).toBe(0.5); + }); + + it("should return false for non-existent proposal", async () => { + mockFs.existsSync.mockReturnValue(false); + + const result = await gameStateService.canMergeProposal(999); + + expect(result.canMerge).toBe(false); + expect(result.reason).toBe("Proposal not found"); + }); + }); +}); diff --git a/src/test/GitHubService.test.ts b/src/test/GitHubService.test.ts new file mode 100644 index 0000000..b103636 --- /dev/null +++ b/src/test/GitHubService.test.ts @@ -0,0 +1,448 @@ +import { GitHubService } from "../services/GitHubService"; +import type { + GitHubComment, + GitHubConfig, + GitHubEvent, + GitHubIssue, +} from "../types"; + +// Mock the Octokit library +jest.mock("@octokit/rest", () => { + return { + Octokit: jest.fn().mockImplementation(() => ({ + issues: { + get: jest.fn(), + listComments: jest.fn(), + createComment: jest.fn(), + update: jest.fn(), + addLabels: jest.fn(), + removeLabel: jest.fn(), + }, + pulls: { + get: jest.fn(), + merge: jest.fn(), + }, + git: { + updateRef: jest.fn(), + }, + repos: { + createOrUpdateFileContents: jest.fn(), + getContent: jest.fn(), + updateBranchProtection: jest.fn(), + deleteBranchProtection: jest.fn(), + }, + })), + }; +}); + +describe("GitHubService", () => { + let githubService: GitHubService; + let mockOctokit: { + issues: { + get: jest.Mock; + update: jest.Mock; + addLabels: jest.Mock; + removeLabel: jest.Mock; + createComment: jest.Mock; + listComments: jest.Mock; + }; + pulls: { + get: jest.Mock; + merge: jest.Mock; + }; + repos: { + getContent: jest.Mock; + createOrUpdateFileContents: jest.Mock; + updateBranch: jest.Mock; + getBranch: jest.Mock; + updateBranchProtection: jest.Mock; + deleteBranchProtection: jest.Mock; + }; + }; + + const mockConfig: GitHubConfig = { + token: "test-token", + owner: "test-owner", + repo: "test-repo", + baseBranch: "main", + }; + + beforeEach(() => { + jest.clearAllMocks(); + githubService = new GitHubService(mockConfig); + mockOctokit = (githubService as unknown as { octokit: typeof mockOctokit }) + .octokit; + }); + + describe("constructor", () => { + it("should initialize with correct configuration", () => { + expect(githubService).toBeInstanceOf(GitHubService); + }); + }); + + describe("getIssue", () => { + it("should fetch an issue successfully", async () => { + const mockIssue: GitHubIssue = { + number: 123, + title: "Test Issue", + body: "Test body", + state: "open", + user: { login: "testuser", id: 1, type: "User" }, + created_at: "2023-01-01T00:00:00Z", + updated_at: "2023-01-01T00:00:00Z", + closed_at: null, + labels: [], + }; + + mockOctokit.issues.get.mockResolvedValue({ data: mockIssue }); + + const result = await githubService.getIssue(123); + + expect(mockOctokit.issues.get).toHaveBeenCalledWith({ + owner: "test-owner", + repo: "test-repo", + issue_number: 123, + }); + expect(result).toEqual(mockIssue); + }); + + it("should handle API errors", async () => { + mockOctokit.issues.get.mockRejectedValue(new Error("API Error")); + + await expect(githubService.getIssue(123)).rejects.toThrow("API Error"); + }); + }); + + describe("getIssueComments", () => { + it("should fetch issue comments successfully", async () => { + const mockComments: GitHubComment[] = [ + { + id: 1, + body: "Test comment", + user: { login: "testuser", id: 1, type: "User" }, + created_at: "2023-01-01T00:00:00Z", + updated_at: "2023-01-01T00:00:00Z", + }, + ]; + + mockOctokit.issues.listComments.mockResolvedValue({ data: mockComments }); + + const result = await githubService.getIssueComments(123); + + expect(mockOctokit.issues.listComments).toHaveBeenCalledWith({ + owner: "test-owner", + repo: "test-repo", + issue_number: 123, + }); + expect(result).toEqual(mockComments); + }); + }); + + describe("addComment", () => { + it("should add a comment successfully", async () => { + mockOctokit.issues.createComment.mockResolvedValue({}); + + await githubService.addComment(123, "Test comment"); + + expect(mockOctokit.issues.createComment).toHaveBeenCalledWith({ + owner: "test-owner", + repo: "test-repo", + issue_number: 123, + body: "Test comment", + }); + }); + }); + + describe("updateIssue", () => { + it("should update an issue successfully", async () => { + mockOctokit.issues.update.mockResolvedValue({}); + + const updates = { + title: "Updated Title", + state: "closed" as const, + }; + + await githubService.updateIssue(123, updates); + + expect(mockOctokit.issues.update).toHaveBeenCalledWith({ + owner: "test-owner", + repo: "test-repo", + issue_number: 123, + ...updates, + }); + }); + }); + + describe("addLabels", () => { + it("should add labels successfully", async () => { + mockOctokit.issues.addLabels.mockResolvedValue({}); + + await githubService.addLabels(123, ["label1", "label2"]); + + expect(mockOctokit.issues.addLabels).toHaveBeenCalledWith({ + owner: "test-owner", + repo: "test-repo", + issue_number: 123, + labels: ["label1", "label2"], + }); + }); + }); + + describe("removeLabel", () => { + it("should remove a label successfully", async () => { + mockOctokit.issues.removeLabel.mockResolvedValue({}); + + await githubService.removeLabel(123, "label1"); + + expect(mockOctokit.issues.removeLabel).toHaveBeenCalledWith({ + owner: "test-owner", + repo: "test-repo", + issue_number: 123, + name: "label1", + }); + }); + }); + + describe("mergePullRequest", () => { + it("should merge a PR successfully", async () => { + mockOctokit.pulls.merge.mockResolvedValue({}); + + await githubService.mergePullRequest(123, "squash"); + + expect(mockOctokit.pulls.merge).toHaveBeenCalledWith({ + owner: "test-owner", + repo: "test-repo", + pull_number: 123, + merge_method: "squash", + }); + }); + + it("should use default merge method", async () => { + mockOctokit.pulls.merge.mockResolvedValue({}); + + await githubService.mergePullRequest(123); + + expect(mockOctokit.pulls.merge).toHaveBeenCalledWith({ + owner: "test-owner", + repo: "test-repo", + pull_number: 123, + merge_method: "merge", + }); + }); + }); + + describe("getFile", () => { + it("should get file content successfully", async () => { + const mockResponse = { + data: { + type: "file", + content: Buffer.from("test content").toString("base64"), + sha: "abc123", + }, + }; + + mockOctokit.repos.getContent.mockResolvedValue(mockResponse); + + const result = await githubService.getFile("test.md"); + + expect(mockOctokit.repos.getContent).toHaveBeenCalledWith({ + owner: "test-owner", + repo: "test-repo", + path: "test.md", + ref: "main", + }); + expect(result).toEqual({ + content: "test content", + sha: "abc123", + }); + }); + + it("should return null for 404 errors", async () => { + const error = new Error("Not Found") as Error & { status: number }; + error.status = 404; + mockOctokit.repos.getContent.mockRejectedValue(error); + + const result = await githubService.getFile("nonexistent.md"); + + expect(result).toBeNull(); + }); + + it("should throw for other errors", async () => { + mockOctokit.repos.getContent.mockRejectedValue(new Error("API Error")); + + await expect(githubService.getFile("test.md")).rejects.toThrow( + "API Error", + ); + }); + + it("should return null for directory responses", async () => { + const mockResponse = { + data: [{ type: "file", name: "file1.md" }], + }; + + mockOctokit.repos.getContent.mockResolvedValue(mockResponse); + + const result = await githubService.getFile("test/"); + + expect(result).toBeNull(); + }); + }); + + describe("createFile", () => { + it("should create a file successfully", async () => { + mockOctokit.repos.createOrUpdateFileContents.mockResolvedValue({}); + + await githubService.createFile( + "test.md", + "Create test file", + "test content", + ); + + expect(mockOctokit.repos.createOrUpdateFileContents).toHaveBeenCalledWith( + { + owner: "test-owner", + repo: "test-repo", + path: "test.md", + message: "Create test file", + content: Buffer.from("test content").toString("base64"), + branch: "main", + }, + ); + }); + + it("should use custom branch", async () => { + mockOctokit.repos.createOrUpdateFileContents.mockResolvedValue({}); + + await githubService.createFile( + "test.md", + "Create test file", + "test content", + "develop", + ); + + expect(mockOctokit.repos.createOrUpdateFileContents).toHaveBeenCalledWith( + { + owner: "test-owner", + repo: "test-repo", + path: "test.md", + message: "Create test file", + content: Buffer.from("test content").toString("base64"), + branch: "develop", + }, + ); + }); + }); + + describe("updateFile", () => { + it("should update a file successfully", async () => { + mockOctokit.repos.createOrUpdateFileContents.mockResolvedValue({}); + + await githubService.updateFile( + "test.md", + "Update test file", + "new content", + "abc123", + ); + + expect(mockOctokit.repos.createOrUpdateFileContents).toHaveBeenCalledWith( + { + owner: "test-owner", + repo: "test-repo", + path: "test.md", + message: "Update test file", + content: Buffer.from("new content").toString("base64"), + sha: "abc123", + branch: "main", + }, + ); + }); + }); + + describe("parseEvent", () => { + it("should parse GitHub event successfully", () => { + const mockEvent: GitHubEvent = { + action: "created", + issue: { + number: 123, + title: "Test Issue", + body: "Test body", + state: "open", + user: { login: "testuser", id: 1, type: "User" }, + created_at: "2023-01-01T00:00:00Z", + updated_at: "2023-01-01T00:00:00Z", + closed_at: null, + labels: [], + }, + repository: { + owner: { login: "test-owner", id: 1, type: "Organization" }, + name: "test-repo", + full_name: "test-owner/test-repo", + }, + }; + + // Mock fs.readFileSync + const fs = require("node:fs"); + jest.spyOn(fs, "readFileSync").mockReturnValue(JSON.stringify(mockEvent)); + + const result = githubService.parseEvent("/tmp/test-event.json"); + + expect(result).toEqual(mockEvent); + }); + }); + + describe("enableBranchProtection", () => { + it("should enable branch protection successfully", async () => { + mockOctokit.repos.updateBranchProtection.mockResolvedValue({}); + + await githubService.enableBranchProtection("main", 2); + + expect(mockOctokit.repos.updateBranchProtection).toHaveBeenCalledWith({ + owner: "test-owner", + repo: "test-repo", + branch: "main", + required_status_checks: null, + enforce_admins: false, + required_pull_request_reviews: { + required_approving_review_count: 2, + dismiss_stale_reviews: true, + require_code_owner_reviews: false, + }, + restrictions: null, + }); + }); + + it("should use default required reviews", async () => { + mockOctokit.repos.updateBranchProtection.mockResolvedValue({}); + + await githubService.enableBranchProtection("main"); + + expect(mockOctokit.repos.updateBranchProtection).toHaveBeenCalledWith({ + owner: "test-owner", + repo: "test-repo", + branch: "main", + required_status_checks: null, + enforce_admins: false, + required_pull_request_reviews: { + required_approving_review_count: 1, + dismiss_stale_reviews: true, + require_code_owner_reviews: false, + }, + restrictions: null, + }); + }); + }); + + describe("disableBranchProtection", () => { + it("should disable branch protection successfully", async () => { + mockOctokit.repos.deleteBranchProtection.mockResolvedValue({}); + + await githubService.disableBranchProtection("main"); + + expect(mockOctokit.repos.deleteBranchProtection).toHaveBeenCalledWith({ + owner: "test-owner", + repo: "test-repo", + branch: "main", + }); + }); + }); +}); diff --git a/src/test/VoteProcessor.test.ts b/src/test/VoteProcessor.test.ts new file mode 100644 index 0000000..7f982ab --- /dev/null +++ b/src/test/VoteProcessor.test.ts @@ -0,0 +1,375 @@ +import { + formatProposalStatus, + formatVoteInstructions, + formatVoteSummary, + isValidVote, + parseVoteFromComment, +} from "../services/VoteProcessor"; +import type { GitHubComment, VoteType } from "../types"; + +describe("VoteProcessor", () => { + const mockComment = ( + body: string, + user: string = "testuser", + ): GitHubComment => ({ + id: 1, + body, + user: { + login: user, + id: 1, + type: "User", + }, + created_at: "2023-01-01T00:00:00Z", + updated_at: "2023-01-01T00:00:00Z", + }); + + describe("parseVoteFromComment", () => { + it("should parse VOTE: FOR format", () => { + const comment = mockComment("VOTE: FOR"); + const result = parseVoteFromComment(comment); + + expect(result).toEqual({ + voter: "testuser", + vote: "FOR", + }); + }); + + it("should parse VOTE: AGAINST format", () => { + const comment = mockComment("VOTE: AGAINST"); + const result = parseVoteFromComment(comment); + + expect(result).toEqual({ + voter: "testuser", + vote: "AGAINST", + }); + }); + + it('should parse "I vote FOR" format', () => { + const comment = mockComment("I vote FOR"); + const result = parseVoteFromComment(comment); + + expect(result).toEqual({ + voter: "testuser", + vote: "FOR", + }); + }); + + it('should parse "My vote is AGAINST" format', () => { + const comment = mockComment("My vote is AGAINST"); + const result = parseVoteFromComment(comment); + + expect(result).toEqual({ + voter: "testuser", + vote: "AGAINST", + }); + }); + + it('should parse "Voting FOR" format', () => { + const comment = mockComment("Voting FOR"); + const result = parseVoteFromComment(comment); + + expect(result).toEqual({ + voter: "testuser", + vote: "FOR", + }); + }); + + it("should parse emoji formats", () => { + const forComment = mockComment("āœ… FOR"); + const againstComment = mockComment("āŒ AGAINST"); + + expect(parseVoteFromComment(forComment)).toEqual({ + voter: "testuser", + vote: "FOR", + }); + + expect(parseVoteFromComment(againstComment)).toEqual({ + voter: "testuser", + vote: "AGAINST", + }); + }); + + it("should handle case insensitive votes", () => { + const comment = mockComment("vote: for"); + const result = parseVoteFromComment(comment); + + expect(result).toEqual({ + voter: "testuser", + vote: "FOR", + }); + }); + + it("should return null for invalid vote formats", () => { + const invalidComments = [ + "Just a comment", + "I agree", + "VOTE: maybe", + "I vote maybe", + "This is not a vote", + ]; + + invalidComments.forEach((body) => { + const comment = mockComment(body); + const result = parseVoteFromComment(comment); + expect(result).toBeNull(); + }); + }); + + it("should handle whitespace", () => { + const comment = mockComment(" VOTE: FOR "); + const result = parseVoteFromComment(comment); + + expect(result).toEqual({ + voter: "testuser", + vote: "FOR", + }); + }); + }); + + describe("isValidVote", () => { + it("should return true for valid votes", () => { + expect(isValidVote("FOR")).toBe(true); + expect(isValidVote("AGAINST")).toBe(true); + }); + + it("should return false for invalid votes", () => { + expect(isValidVote("maybe")).toBe(false); + expect(isValidVote("abstain")).toBe(false); + expect(isValidVote("")).toBe(false); + }); + }); + + describe("formatVoteSummary", () => { + it("should format empty votes", () => { + const result = formatVoteSummary({}); + expect(result).toBe("No votes cast yet."); + }); + + it("should format votes with only FOR votes", () => { + const votes = { + user1: "FOR" as VoteType, + user2: "FOR" as VoteType, + }; + + const result = formatVoteSummary(votes); + expect(result).toContain("**Vote Summary:** 2 FOR, 0 AGAINST"); + expect(result).toContain("**FOR:** @user1, @user2"); + expect(result).toContain("**Majority:** 100.0% FOR"); + }); + + it("should format votes with only AGAINST votes", () => { + const votes = { + user1: "AGAINST" as VoteType, + user2: "AGAINST" as VoteType, + }; + + const result = formatVoteSummary(votes); + expect(result).toContain("**Vote Summary:** 0 FOR, 2 AGAINST"); + expect(result).toContain("**AGAINST:** @user1, @user2"); + expect(result).toContain("**Majority:** 0.0% FOR"); + }); + + it("should format mixed votes", () => { + const votes = { + user1: "FOR" as VoteType, + user2: "AGAINST" as VoteType, + user3: "FOR" as VoteType, + }; + + const result = formatVoteSummary(votes); + expect(result).toContain("**Vote Summary:** 2 FOR, 1 AGAINST"); + expect(result).toContain("**FOR:** @user1, @user3"); + expect(result).toContain("**AGAINST:** @user2"); + expect(result).toContain("**Majority:** 66.7% FOR"); + }); + }); + + describe("formatVoteInstructions", () => { + it("should return formatted instructions", () => { + const result = formatVoteInstructions(); + + expect(result).toContain("## How to Vote"); + expect(result).toContain("VOTE: FOR"); + expect(result).toContain("VOTE: AGAINST"); + expect(result).toContain("I vote FOR"); + expect(result).toContain("I vote AGAINST"); + expect(result).toContain("āœ… FOR"); + expect(result).toContain("āŒ AGAINST"); + }); + }); + + describe("formatProposalStatus", () => { + const mockProposal = { + status: "open", + requiredVotes: 3, + requiredMajority: 0.5, + votes: {} as Record, + }; + + it("should format open proposal with no votes", () => { + const result = formatProposalStatus(mockProposal); + + expect(result).toContain("## Proposal Status: OPEN"); + expect(result).toContain("**Required Votes:** 3"); + expect(result).toContain("**Current Votes:** 0"); + expect(result).toContain("**Required Majority:** 50.0%"); + expect(result).toContain("ā³ **Waiting for 3 more vote(s)**"); + }); + + it("should format open proposal with insufficient votes", () => { + const proposal = { + ...mockProposal, + votes: { + user1: "FOR" as VoteType, + user2: "FOR" as VoteType, + }, + }; + + const result = formatProposalStatus(proposal); + + expect(result).toContain("**Current Votes:** 2"); + expect(result).toContain("ā³ **Waiting for 1 more vote(s)**"); + }); + + it("should format open proposal with insufficient majority", () => { + const proposal = { + ...mockProposal, + votes: { + user1: "FOR" as VoteType, + user2: "AGAINST" as VoteType, + user3: "AGAINST" as VoteType, + }, + }; + + const result = formatProposalStatus(proposal); + + expect(result).toContain("**Current Votes:** 3"); + expect(result).toContain("**Current Majority:** 33.3% FOR"); + expect(result).toContain("āŒ **Insufficient majority**"); + }); + + it("should format ready to merge proposal", () => { + const proposal = { + ...mockProposal, + votes: { + user1: "FOR" as VoteType, + user2: "FOR" as VoteType, + user3: "FOR" as VoteType, + }, + }; + + const result = formatProposalStatus(proposal); + + expect(result).toContain("**Current Votes:** 3"); + expect(result).toContain("**Current Majority:** 100.0% FOR"); + expect(result).toContain("āœ… **Ready to merge!**"); + }); + + it("should format passed proposal", () => { + const proposal = { + ...mockProposal, + status: "passed", + votes: { + user1: "FOR" as VoteType, + user2: "FOR" as VoteType, + user3: "FOR" as VoteType, + }, + }; + + const result = formatProposalStatus(proposal); + + expect(result).toContain("## Proposal Status: PASSED"); + expect(result).toContain("šŸŽ‰ **Proposal PASSED!**"); + }); + + it("should format failed proposal", () => { + const proposal = { + ...mockProposal, + status: "failed", + votes: { + user1: "AGAINST" as VoteType, + user2: "AGAINST" as VoteType, + user3: "AGAINST" as VoteType, + }, + }; + + const result = formatProposalStatus(proposal); + + expect(result).toContain("## Proposal Status: FAILED"); + expect(result).toContain("āŒ **Proposal FAILED**"); + }); + + it("should include merge check information when provided", () => { + const proposal = { + ...mockProposal, + votes: { + user1: "FOR" as VoteType, + user2: "FOR" as VoteType, + }, + mergeCheck: { + canMerge: false, + reason: "Insufficient votes or majority", + requiredVotes: 3, + currentVotes: 2, + requiredMajority: 0.5, + currentMajority: 1.0, + }, + }; + + const result = formatProposalStatus(proposal); + + expect(result).toContain("## Merge Status"); + expect(result).toContain("āŒ **Cannot merge**"); + expect(result).toContain("**Reason:** Insufficient votes or majority"); + expect(result).toContain("Need 1 more vote(s) (2/3)"); + }); + + it("should show ready to merge when merge check passes", () => { + const proposal = { + ...mockProposal, + votes: { + user1: "FOR" as VoteType, + user2: "FOR" as VoteType, + user3: "FOR" as VoteType, + }, + mergeCheck: { + canMerge: true, + requiredVotes: 3, + currentVotes: 3, + requiredMajority: 0.5, + currentMajority: 1.0, + }, + }; + + const result = formatProposalStatus(proposal); + + expect(result).toContain("## Merge Status"); + expect(result).toContain("āœ… **Ready to merge!**"); + }); + + it("should show insufficient majority details", () => { + const proposal = { + ...mockProposal, + votes: { + user1: "FOR" as VoteType, + user2: "AGAINST" as VoteType, + user3: "AGAINST" as VoteType, + }, + mergeCheck: { + canMerge: false, + reason: "Insufficient votes or majority", + requiredVotes: 3, + currentVotes: 3, + requiredMajority: 0.5, + currentMajority: 0.33, + }, + }; + + const result = formatProposalStatus(proposal); + + expect(result).toContain("## Merge Status"); + expect(result).toContain("āŒ **Cannot merge**"); + expect(result).toContain("Insufficient majority: 33.3% FOR (need 50.0%)"); + }); + }); +}); diff --git a/src/test/generate-rules-summary.test.ts b/src/test/generate-rules-summary.test.ts new file mode 100644 index 0000000..bdab683 --- /dev/null +++ b/src/test/generate-rules-summary.test.ts @@ -0,0 +1,80 @@ +import { execSync } from "node:child_process"; +import * as fs from "node:fs"; +import * as path from "node:path"; + +describe("Rules Summary Generation", () => { + const summaryPath = path.join(__dirname, "../../RULES_SUMMARY.md"); + const originalSummary = fs.existsSync(summaryPath) + ? fs.readFileSync(summaryPath, "utf-8") + : ""; + + afterAll(() => { + // Restore original summary if it existed + if (originalSummary) { + fs.writeFileSync(summaryPath, originalSummary); + } else if (fs.existsSync(summaryPath)) { + fs.unlinkSync(summaryPath); + } + }); + + it("should generate a rules summary file", () => { + // Run the generator + execSync("npm run generate-rules", { stdio: "pipe" }); + + // Check that the file was created + expect(fs.existsSync(summaryPath)).toBe(true); + + // Read the generated content + const content = fs.readFileSync(summaryPath, "utf-8"); + + // Should contain the expected sections + expect(content).toContain("# Nomic Rules Summary"); + expect(content).toContain("## Rule Statistics"); + expect(content).toContain("## Tag Index"); + expect(content).toContain("## Detailed Rule List"); + + // Check that it contains rule information + expect(content).toContain("Rule [101]"); + expect(content).toContain("Rule [102]"); + expect(content).toContain("Immutable"); + expect(content).toContain("Mutable"); + + // Check for auto-generation timestamp + expect(content).toContain("This file was automatically generated on"); + }); + + it("should include statistics about rules", () => { + execSync("npm run generate-rules", { stdio: "pipe" }); + const content = fs.readFileSync(summaryPath, "utf-8"); + + // Should contain statistics + expect(content).toMatch(/Total Rules.*\d+/); + expect(content).toMatch(/Immutable Rules.*\d+/); + expect(content).toMatch(/Mutable Rules.*\d+/); + expect(content).toMatch(/Active Rules.*\d+/); + }); + + it("should categorize rules correctly", () => { + execSync("npm run generate-rules", { stdio: "pipe" }); + const content = fs.readFileSync(summaryPath, "utf-8"); + + // Should have separate sections for immutable and mutable rules + expect(content).toContain("Immutable Rules"); + expect(content).toContain("Mutable Rules"); + + // Should list rules in each category + expect(content).toContain("Rule [101]"); + expect(content).toContain("Rule [202]"); + }); + + it("should include detailed rule information", () => { + execSync("npm run generate-rules", { stdio: "pipe" }); + const content = fs.readFileSync(summaryPath, "utf-8"); + + // Should include metadata for each rule + expect(content).toContain("**Type**:"); + expect(content).toContain("**Status**:"); + expect(content).toContain("**Author**:"); + expect(content).toContain("**Tags**:"); + }); +}); diff --git a/src/test/index.test.ts b/src/test/index.test.ts new file mode 100644 index 0000000..352e965 --- /dev/null +++ b/src/test/index.test.ts @@ -0,0 +1,241 @@ +import { GameStateService } from "../services/GameStateService"; +import { GitHubService } from "../services/GitHubService"; + +// Mock the services +jest.mock("../services/GitHubService"); +jest.mock("../services/GameStateService"); +jest.mock("../services/VoteProcessor"); + +const MockedGitHubService = GitHubService as jest.MockedClass< + typeof GitHubService +>; +const MockedGameStateService = GameStateService as jest.MockedClass< + typeof GameStateService +>; + +describe("Application Entry Point", () => { + let mockGitHubService: jest.Mocked; + let mockGameStateService: jest.Mocked; + + beforeEach(() => { + jest.clearAllMocks(); + + // Mock environment variables + process.env.GITHUB_TOKEN = "test-token"; + process.env.GITHUB_REPOSITORY = "test-owner/test-repo"; + process.env.GITHUB_EVENT_PATH = "/tmp/test-event.json"; + process.env.REQUIRED_VOTES = "3"; + process.env.REQUIRED_MAJORITY = "0.5"; + process.env.POINTS_FOR_SUCCESSFUL_PROPOSAL = "3"; + process.env.POINTS_FOR_VOTING_WITH_MAJORITY = "2"; + process.env.POINTS_FOR_PARTICIPATION = "1"; + process.env.AUTO_MERGE_ENABLED = "true"; + process.env.BRANCH_PROTECTION_ENABLED = "true"; + + mockGitHubService = new MockedGitHubService({ + token: "test-token", + owner: "test-owner", + repo: "test-repo", + baseBranch: "main", + }) as jest.Mocked; + + mockGameStateService = new MockedGameStateService("game-state.yaml", { + requiredVotes: 3, + requiredMajority: 0.5, + pointsForSuccessfulProposal: 3, + pointsForVotingWithMajority: 2, + pointsForParticipation: 1, + autoMergeEnabled: true, + branchProtectionEnabled: true, + }) as jest.Mocked; + }); + + afterEach(() => { + delete process.env.GITHUB_TOKEN; + delete process.env.GITHUB_REPOSITORY; + delete process.env.GITHUB_EVENT_PATH; + delete process.env.REQUIRED_VOTES; + delete process.env.REQUIRED_MAJORITY; + delete process.env.POINTS_FOR_SUCCESSFUL_PROPOSAL; + delete process.env.POINTS_FOR_VOTING_WITH_MAJORITY; + delete process.env.POINTS_FOR_PARTICIPATION; + delete process.env.AUTO_MERGE_ENABLED; + delete process.env.BRANCH_PROTECTION_ENABLED; + }); + + describe("Configuration Loading", () => { + it("should load configuration from environment variables", () => { + // This would test the configuration loading logic + // Since the main index.ts file is not exported as a module, + // we'll test the configuration parsing logic here + + const config = { + github: { + token: process.env.GITHUB_TOKEN, + owner: "test-owner", + repo: "test-repo", + baseBranch: "main", + }, + game: { + requiredVotes: parseInt(process.env.REQUIRED_VOTES || "3"), + requiredMajority: parseFloat(process.env.REQUIRED_MAJORITY || "0.5"), + pointsForSuccessfulProposal: parseInt( + process.env.POINTS_FOR_SUCCESSFUL_PROPOSAL || "3", + ), + pointsForVotingWithMajority: parseInt( + process.env.POINTS_FOR_VOTING_WITH_MAJORITY || "2", + ), + pointsForParticipation: parseInt( + process.env.POINTS_FOR_PARTICIPATION || "1", + ), + autoMergeEnabled: process.env.AUTO_MERGE_ENABLED === "true", + branchProtectionEnabled: + process.env.BRANCH_PROTECTION_ENABLED === "true", + }, + }; + + expect(config.github.token).toBe("test-token"); + expect(config.github.owner).toBe("test-owner"); + expect(config.github.repo).toBe("test-repo"); + expect(config.game.requiredVotes).toBe(3); + expect(config.game.requiredMajority).toBe(0.5); + expect(config.game.autoMergeEnabled).toBe(true); + expect(config.game.branchProtectionEnabled).toBe(true); + }); + + it("should use default values when environment variables are missing", () => { + delete process.env.REQUIRED_VOTES; + delete process.env.REQUIRED_MAJORITY; + delete process.env.AUTO_MERGE_ENABLED; + + const config = { + game: { + requiredVotes: parseInt(process.env.REQUIRED_VOTES || "3"), + requiredMajority: parseFloat(process.env.REQUIRED_MAJORITY || "0.5"), + autoMergeEnabled: process.env.AUTO_MERGE_ENABLED === "true", + }, + }; + + expect(config.game.requiredVotes).toBe(3); + expect(config.game.requiredMajority).toBe(0.5); + expect(config.game.autoMergeEnabled).toBe(false); + }); + }); + + describe("Service Integration", () => { + it("should create services with correct configuration", () => { + const githubConfig = { + token: "test-token", + owner: "test-owner", + repo: "test-repo", + baseBranch: "main", + }; + + const gameConfig = { + requiredVotes: 3, + requiredMajority: 0.5, + pointsForSuccessfulProposal: 3, + pointsForVotingWithMajority: 2, + pointsForParticipation: 1, + autoMergeEnabled: true, + branchProtectionEnabled: true, + }; + + const githubService = new GitHubService(githubConfig); + const gameStateService = new GameStateService( + "game-state.yaml", + gameConfig, + ); + + expect(githubService).toBeInstanceOf(GitHubService); + expect(gameStateService).toBeInstanceOf(GameStateService); + }); + }); + + describe("Error Handling", () => { + it("should handle missing GitHub token", () => { + delete process.env.GITHUB_TOKEN; + + expect(() => { + if (!process.env.GITHUB_TOKEN) { + throw new Error("GITHUB_TOKEN environment variable is required"); + } + }).toThrow("GITHUB_TOKEN environment variable is required"); + }); + + it("should handle missing GitHub repository", () => { + delete process.env.GITHUB_REPOSITORY; + + expect(() => { + const repository = process.env.GITHUB_REPOSITORY; + if (!repository) { + throw new Error("GITHUB_REPOSITORY environment variable is required"); + } + const [_owner, _repo] = repository.split("/"); + }).toThrow("GITHUB_REPOSITORY environment variable is required"); + }); + + it("should handle invalid repository format", () => { + process.env.GITHUB_REPOSITORY = "invalid-format"; + + expect(() => { + const repository = process.env.GITHUB_REPOSITORY; + if (!repository) { + throw new Error('GITHUB_REPOSITORY must be in format "owner/repo"'); + } + const [_owner, _repo] = repository.split("/"); + if (!_owner || !_repo) { + throw new Error('GITHUB_REPOSITORY must be in format "owner/repo"'); + } + }).toThrow('GITHUB_REPOSITORY must be in format "owner/repo"'); + }); + }); + + describe("Vote Processing Flow", () => { + it("should process votes correctly", async () => { + // Mock the vote processing flow + const mockEvent = { + action: "created", + issue: { + number: 123, + title: "Test Proposal", + body: "This is a test proposal", + state: "open" as const, + user: { login: "proposer", id: 1, type: "User" }, + created_at: "2023-01-01T00:00:00Z", + updated_at: "2023-01-01T00:00:00Z", + closed_at: null, + labels: [], + }, + repository: { + owner: { login: "test-owner", id: 1, type: "Organization" }, + name: "test-repo", + full_name: "test-owner/test-repo", + }, + }; + + mockGitHubService.parseEvent.mockReturnValue(mockEvent); + mockGitHubService.getIssueComments.mockResolvedValue([]); + mockGameStateService.processVote.mockResolvedValue({ + proposalUpdated: true, + statusChanged: false, + state: { + players: {}, + proposals: {}, + gameRules: [], + currentTurn: 1, + lastUpdated: "2023-01-01T00:00:00Z", + }, + }); + + // Simulate the main processing flow + const event = mockGitHubService.parseEvent("/tmp/test-event.json"); + const comments = await mockGitHubService.getIssueComments( + event.issue.number, + ); + + expect(event).toEqual(mockEvent); + expect(comments).toEqual([]); + }); + }); +}); diff --git a/src/test/setup.ts b/src/test/setup.ts new file mode 100644 index 0000000..5c1497e --- /dev/null +++ b/src/test/setup.ts @@ -0,0 +1,18 @@ +// Test setup file +import { jest } from "@jest/globals"; + +// Mock console methods to reduce noise in tests +global.console = { + ...console, + log: jest.fn(), + debug: jest.fn(), + info: jest.fn(), + warn: jest.fn(), + error: jest.fn(), +}; + +// Mock environment variables +process.env.GITHUB_TOKEN = "test-token"; +process.env.GITHUB_REPOSITORY = "test-owner/test-repo"; +process.env.GITHUB_EVENT_PATH = "/tmp/test-event.json"; +process.env.GITHUB_EVENT_NAME = "issue_comment"; diff --git a/src/types/events.ts b/src/types/events.ts new file mode 100644 index 0000000..b8bcc21 --- /dev/null +++ b/src/types/events.ts @@ -0,0 +1,211 @@ +import { z } from "zod"; + +// Base event schema +export const BaseEventSchema = z.object({ + id: z.string(), // UUID for each event + timestamp: z.string(), // ISO timestamp + turn: z.number().optional(), // Game turn when event occurred +}); + +// Event type schemas +export const ProposalCreatedEventSchema = BaseEventSchema.extend({ + type: z.literal("proposal_created"), + issue_number: z.number(), + title: z.string(), + proposer: z.string(), + body: z.string().optional(), +}); + +export const VoteCastEventSchema = BaseEventSchema.extend({ + type: z.literal("vote_cast"), + issue_number: z.number(), + voter: z.string(), + vote: z.enum(["FOR", "AGAINST"]), + comment_id: z.number().optional(), +}); + +export const ProposalResolvedEventSchema = BaseEventSchema.extend({ + type: z.literal("proposal_resolved"), + issue_number: z.number(), + status: z.enum(["passed", "failed", "closed"]), + final_votes: z.object({ + for: z.number(), + against: z.number(), + total: z.number(), + }), +}); + +export const RuleEnactedEventSchema = BaseEventSchema.extend({ + type: z.literal("rule_enacted"), + rule_id: z.string(), + title: z.string(), + content: z.string(), + rule_type: z.enum(["mutable", "immutable"]), + author: z.string(), + proposal_issue: z.number(), +}); + +export const RuleRepealedEventSchema = BaseEventSchema.extend({ + type: z.literal("rule_repealed"), + rule_id: z.string(), + repealed_by: z.string(), + proposal_issue: z.number(), +}); + +export const RuleAmendedEventSchema = BaseEventSchema.extend({ + type: z.literal("rule_amended"), + rule_id: z.string(), + new_content: z.string(), + amended_by: z.string(), + proposal_issue: z.number(), +}); + +export const RuleTransmutedEventSchema = BaseEventSchema.extend({ + type: z.literal("rule_transmuted"), + rule_id: z.string(), + old_type: z.enum(["mutable", "immutable"]), + new_type: z.enum(["mutable", "immutable"]), + transmuted_by: z.string(), + proposal_issue: z.number(), +}); + +export const PlayerJoinedEventSchema = BaseEventSchema.extend({ + type: z.literal("player_joined"), + username: z.string(), + joined_via: z.enum(["proposal", "vote", "comment"]), + first_action_issue: z.number().optional(), +}); + +export const PointsAwardedEventSchema = BaseEventSchema.extend({ + type: z.literal("points_awarded"), + player: z.string(), + points: z.number(), + reason: z.string(), + related_issue: z.number().optional(), + related_event_id: z.string().optional(), +}); + +export const TurnAdvancedEventSchema = BaseEventSchema.extend({ + type: z.literal("turn_advanced"), + new_turn: z.number(), + triggered_by_issue: z.number(), +}); + +export const GameStartedEventSchema = BaseEventSchema.extend({ + type: z.literal("game_started"), + initial_rules: z.array(z.string()), + config: z.object({ + required_votes: z.number(), + required_majority: z.number(), + points_for_successful_proposal: z.number(), + points_for_voting_with_majority: z.number(), + points_for_participation: z.number(), + }), +}); + +// Union of all event types +export const GameEventSchema = z.discriminatedUnion("type", [ + ProposalCreatedEventSchema, + VoteCastEventSchema, + ProposalResolvedEventSchema, + RuleEnactedEventSchema, + RuleRepealedEventSchema, + RuleAmendedEventSchema, + RuleTransmutedEventSchema, + PlayerJoinedEventSchema, + PointsAwardedEventSchema, + TurnAdvancedEventSchema, + GameStartedEventSchema, +]); + +// Type exports +export type BaseEvent = z.infer; +export type ProposalCreatedEvent = z.infer; +export type VoteCastEvent = z.infer; +export type ProposalResolvedEvent = z.infer; +export type RuleEnactedEvent = z.infer; +export type RuleRepealedEvent = z.infer; +export type RuleAmendedEvent = z.infer; +export type RuleTransmutedEvent = z.infer; +export type PlayerJoinedEvent = z.infer; +export type PointsAwardedEvent = z.infer; +export type TurnAdvancedEvent = z.infer; +export type GameStartedEvent = z.infer; + +export type GameEvent = z.infer; + +// Event log entry (for JSONL format) +export interface EventLogEntry { + event: GameEvent; + metadata?: { + github_event_id?: string; + github_sha?: string; + workflow_run_id?: string; + }; +} + +// Computed state types (derived from events) +export interface ComputedGameState { + players: Record; + proposals: Record; + rules: Record; + current_turn: number; + game_started_at: string; + last_updated: string; + event_count: number; +} + +export interface ComputedPlayerState { + username: string; + points: number; + proposals_submitted: number; + proposals_passed: number; + votes_cast: number; + first_seen: string; + last_active: string; + point_history: Array<{ + points: number; + reason: string; + timestamp: string; + event_id: string; + }>; +} + +export interface ComputedProposalState { + issue_number: number; + title: string; + proposer: string; + status: "open" | "passed" | "failed" | "closed"; + votes: Record; + created_at: string; + resolved_at?: string; + final_vote_count?: { + for: number; + against: number; + total: number; + }; +} + +export interface ComputedRuleState { + rule_id: string; + title: string; + content: string; + rule_type: "mutable" | "immutable"; + status: "active" | "repealed"; + author: string; + enacted_at: string; + repealed_at?: string; + amendment_history: Array<{ + content: string; + amended_by: string; + amended_at: string; + event_id: string; + }>; + transmutation_history: Array<{ + old_type: "mutable" | "immutable"; + new_type: "mutable" | "immutable"; + transmuted_by: string; + transmuted_at: string; + event_id: string; + }>; +} \ No newline at end of file diff --git a/src/types/index.ts b/src/types/index.ts new file mode 100644 index 0000000..6059c9e --- /dev/null +++ b/src/types/index.ts @@ -0,0 +1,120 @@ +import { z } from "zod"; + +// GitHub API types +export const GitHubUserSchema = z.object({ + login: z.string(), + id: z.number(), + type: z.string(), +}); + +export const GitHubCommentSchema = z.object({ + id: z.number(), + body: z.string(), + user: GitHubUserSchema, + created_at: z.string(), + updated_at: z.string(), +}); + +export const GitHubIssueSchema = z.object({ + number: z.number(), + title: z.string(), + body: z.string(), + state: z.enum(["open", "closed"]), + user: GitHubUserSchema, + created_at: z.string(), + updated_at: z.string(), + closed_at: z.string().nullable(), + labels: z.array(z.object({ name: z.string() })), +}); + +export const GitHubEventSchema = z.object({ + action: z.string(), + issue: GitHubIssueSchema, + repository: z.object({ + owner: GitHubUserSchema, + name: z.string(), + full_name: z.string(), + }), +}); + +// Game types +export const VoteType = z.enum(["FOR", "AGAINST"]); +export type VoteType = z.infer; + +export const VoteSchema = z.object({ + voter: z.string(), + vote: VoteType, + timestamp: z.string(), +}); + +export const ProposalVotesSchema = z.object({ + issueNumber: z.number(), + title: z.string(), + proposer: z.string(), + votes: z.record(z.string(), VoteType), + status: z.enum(["open", "passed", "failed", "closed"]), + createdAt: z.string(), + updatedAt: z.string(), + closedAt: z.string().optional(), + requiredVotes: z.number().default(3), + requiredMajority: z.number().default(0.5), +}); + +export const PlayerScoreSchema = z.object({ + username: z.string(), + points: z.number(), + proposalsSubmitted: z.number(), + proposalsPassed: z.number(), + votesCast: z.number(), + lastActive: z.string(), +}); + +export const GameStateSchema = z.object({ + players: z.record(z.string(), PlayerScoreSchema), + proposals: z.record(z.string(), ProposalVotesSchema), + gameRules: z.array(z.string()), + currentTurn: z.number(), + lastUpdated: z.string(), +}); + +// Rule types +export const RuleSchema = z.object({ + id: z.string(), + title: z.string(), + content: z.string(), + author: z.string(), + status: z.enum(["proposed", "active", "repealed"]), + type: z.enum(["mutable", "immutable"]), + tags: z.array(z.string()), + createdAt: z.string(), + updatedAt: z.string(), + repealedAt: z.string().optional(), +}); + +export type GitHubUser = z.infer; +export type GitHubComment = z.infer; +export type GitHubIssue = z.infer; +export type GitHubEvent = z.infer; +export type Vote = z.infer; +export type ProposalVotes = z.infer; +export type PlayerScore = z.infer; +export type GameState = z.infer; +export type Rule = z.infer; + +// Configuration types +export interface GameConfig { + requiredVotes: number; + requiredMajority: number; + pointsForSuccessfulProposal: number; + pointsForVotingWithMajority: number; + pointsForParticipation: number; + autoMergeEnabled: boolean; + branchProtectionEnabled: boolean; +} + +export interface GitHubConfig { + token: string; + owner: string; + repo: string; + baseBranch: string; +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..5917347 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,23 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "commonjs", + "lib": ["ES2022"], + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "resolveJsonModule": true, + "moduleResolution": "node", + "allowSyntheticDefaultImports": true, + "experimentalDecorators": true, + "emitDecoratorMetadata": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist", "**/*.test.ts", "**/*.spec.ts"] +}